mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-20 17:07:18 +00:00
chore: create alerts ux improvements and api integration (#9165)
This commit is contained in:
parent
8b21ba5db9
commit
1a1ef5aff8
28
frontend/src/api/alerts/createAlertRule.ts
Normal file
28
frontend/src/api/alerts/createAlertRule.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import {
|
||||||
|
AlertRuleV2,
|
||||||
|
PostableAlertRuleV2,
|
||||||
|
} from 'types/api/alerts/alertTypesV2';
|
||||||
|
|
||||||
|
export interface CreateAlertRuleResponse {
|
||||||
|
data: AlertRuleV2;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createAlertRule = async (
|
||||||
|
props: PostableAlertRuleV2,
|
||||||
|
): Promise<SuccessResponse<CreateAlertRuleResponse> | ErrorResponse> => {
|
||||||
|
const response = await axios.post(`/rules`, {
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createAlertRule;
|
||||||
28
frontend/src/api/alerts/testAlertRule.ts
Normal file
28
frontend/src/api/alerts/testAlertRule.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||||
|
|
||||||
|
export interface TestAlertRuleResponse {
|
||||||
|
data: {
|
||||||
|
alertCount: number;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const testAlertRule = async (
|
||||||
|
props: PostableAlertRuleV2,
|
||||||
|
): Promise<SuccessResponse<TestAlertRuleResponse> | ErrorResponse> => {
|
||||||
|
const response = await axios.post(`/testRule`, {
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data.data,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default testAlertRule;
|
||||||
@ -2,6 +2,8 @@ import { Form, Row } from 'antd';
|
|||||||
import logEvent from 'api/common/logEvent';
|
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 { 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';
|
||||||
@ -125,6 +127,15 @@ function CreateRules(): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showNewCreateAlertsPageFlag = showNewCreateAlertsPage();
|
||||||
|
|
||||||
|
if (
|
||||||
|
showNewCreateAlertsPageFlag &&
|
||||||
|
alertType !== AlertTypes.ANOMALY_BASED_ALERT
|
||||||
|
) {
|
||||||
|
return <CreateAlertV2 alertType={alertType} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormAlertRules
|
<FormAlertRules
|
||||||
alertType={alertType}
|
alertType={alertType}
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
import './styles.scss';
|
import './styles.scss';
|
||||||
|
|
||||||
import { Button, Tooltip } from 'antd';
|
import { Button, Tooltip } from 'antd';
|
||||||
|
import getAllChannels from 'api/channels/getAll';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Activity, ChartLine } from 'lucide-react';
|
import { ChartLine } from 'lucide-react';
|
||||||
|
import { useQuery } from 'react-query';
|
||||||
|
import { SuccessResponseV2 } from 'types/api';
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
|
import { Channels } from 'types/api/channels/getAll';
|
||||||
|
import APIError from 'types/api/error';
|
||||||
|
|
||||||
import { useCreateAlertState } from '../context';
|
import { useCreateAlertState } from '../context';
|
||||||
import AdvancedOptions from '../EvaluationSettings/AdvancedOptions';
|
import AdvancedOptions from '../EvaluationSettings/AdvancedOptions';
|
||||||
@ -17,6 +22,16 @@ function AlertCondition(): JSX.Element {
|
|||||||
const { alertType, setAlertType } = useCreateAlertState();
|
const { alertType, setAlertType } = useCreateAlertState();
|
||||||
const showCondensedLayoutFlag = showCondensedLayout();
|
const showCondensedLayoutFlag = showCondensedLayout();
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading: isLoadingChannels,
|
||||||
|
isError: isErrorChannels,
|
||||||
|
refetch: refreshChannels,
|
||||||
|
} = useQuery<SuccessResponseV2<Channels[]>, APIError>(['getChannels'], {
|
||||||
|
queryFn: () => getAllChannels(),
|
||||||
|
});
|
||||||
|
const channels = data?.data || [];
|
||||||
|
|
||||||
const showMultipleTabs =
|
const showMultipleTabs =
|
||||||
alertType === AlertTypes.ANOMALY_BASED_ALERT ||
|
alertType === AlertTypes.ANOMALY_BASED_ALERT ||
|
||||||
alertType === AlertTypes.METRICS_BASED_ALERT;
|
alertType === AlertTypes.METRICS_BASED_ALERT;
|
||||||
@ -27,15 +42,16 @@ function AlertCondition(): JSX.Element {
|
|||||||
icon: <ChartLine size={14} data-testid="threshold-view" />,
|
icon: <ChartLine size={14} data-testid="threshold-view" />,
|
||||||
value: AlertTypes.METRICS_BASED_ALERT,
|
value: AlertTypes.METRICS_BASED_ALERT,
|
||||||
},
|
},
|
||||||
...(showMultipleTabs
|
// Hide anomaly tab for now
|
||||||
? [
|
// ...(showMultipleTabs
|
||||||
{
|
// ? [
|
||||||
label: 'Anomaly',
|
// {
|
||||||
icon: <Activity size={14} data-testid="anomaly-view" />,
|
// label: 'Anomaly',
|
||||||
value: AlertTypes.ANOMALY_BASED_ALERT,
|
// icon: <Activity size={14} data-testid="anomaly-view" />,
|
||||||
},
|
// value: AlertTypes.ANOMALY_BASED_ALERT,
|
||||||
]
|
// },
|
||||||
: []),
|
// ]
|
||||||
|
// : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleAlertTypeChange = (value: AlertTypes): void => {
|
const handleAlertTypeChange = (value: AlertTypes): void => {
|
||||||
@ -76,8 +92,22 @@ function AlertCondition(): JSX.Element {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{alertType !== AlertTypes.ANOMALY_BASED_ALERT && <AlertThreshold />}
|
{alertType !== AlertTypes.ANOMALY_BASED_ALERT && (
|
||||||
{alertType === AlertTypes.ANOMALY_BASED_ALERT && <AnomalyThreshold />}
|
<AlertThreshold
|
||||||
|
channels={channels}
|
||||||
|
isLoadingChannels={isLoadingChannels}
|
||||||
|
isErrorChannels={isErrorChannels}
|
||||||
|
refreshChannels={refreshChannels}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{alertType === AlertTypes.ANOMALY_BASED_ALERT && (
|
||||||
|
<AnomalyThreshold
|
||||||
|
channels={channels}
|
||||||
|
isLoadingChannels={isLoadingChannels}
|
||||||
|
isErrorChannels={isErrorChannels}
|
||||||
|
refreshChannels={refreshChannels}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{showCondensedLayoutFlag ? (
|
{showCondensedLayoutFlag ? (
|
||||||
<div className="condensed-advanced-options-container">
|
<div className="condensed-advanced-options-container">
|
||||||
<AdvancedOptions />
|
<AdvancedOptions />
|
||||||
|
|||||||
@ -1,14 +1,10 @@
|
|||||||
import './styles.scss';
|
import './styles.scss';
|
||||||
|
import '../EvaluationSettings/styles.scss';
|
||||||
|
|
||||||
import { Button, Select, Typography } from 'antd';
|
import { Button, Select, Tooltip, Typography } from 'antd';
|
||||||
import getAllChannels from 'api/channels/getAll';
|
|
||||||
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 { useQuery } from 'react-query';
|
|
||||||
import { SuccessResponseV2 } from 'types/api';
|
|
||||||
import { Channels } from 'types/api/channels/getAll';
|
|
||||||
import APIError from 'types/api/error';
|
|
||||||
|
|
||||||
import { useCreateAlertState } from '../context';
|
import { useCreateAlertState } from '../context';
|
||||||
import {
|
import {
|
||||||
@ -21,27 +17,30 @@ import {
|
|||||||
import EvaluationSettings from '../EvaluationSettings/EvaluationSettings';
|
import EvaluationSettings from '../EvaluationSettings/EvaluationSettings';
|
||||||
import { showCondensedLayout } from '../utils';
|
import { showCondensedLayout } from '../utils';
|
||||||
import ThresholdItem from './ThresholdItem';
|
import ThresholdItem from './ThresholdItem';
|
||||||
import { UpdateThreshold } from './types';
|
import { AnomalyAndThresholdProps, UpdateThreshold } from './types';
|
||||||
import {
|
import {
|
||||||
getCategoryByOptionId,
|
getCategoryByOptionId,
|
||||||
getCategorySelectOptionByName,
|
getCategorySelectOptionByName,
|
||||||
|
getMatchTypeTooltip,
|
||||||
getQueryNames,
|
getQueryNames,
|
||||||
|
RoutingPolicyBanner,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
function AlertThreshold(): JSX.Element {
|
function AlertThreshold({
|
||||||
|
channels,
|
||||||
|
isLoadingChannels,
|
||||||
|
isErrorChannels,
|
||||||
|
refreshChannels,
|
||||||
|
}: AnomalyAndThresholdProps): JSX.Element {
|
||||||
const {
|
const {
|
||||||
alertState,
|
alertState,
|
||||||
thresholdState,
|
thresholdState,
|
||||||
setThresholdState,
|
setThresholdState,
|
||||||
|
notificationSettings,
|
||||||
|
setNotificationSettings,
|
||||||
} = useCreateAlertState();
|
} = useCreateAlertState();
|
||||||
const { data, isLoading: isLoadingChannels } = useQuery<
|
|
||||||
SuccessResponseV2<Channels[]>,
|
|
||||||
APIError
|
|
||||||
>(['getChannels'], {
|
|
||||||
queryFn: () => getAllChannels(),
|
|
||||||
});
|
|
||||||
const showCondensedLayoutFlag = showCondensedLayout();
|
const showCondensedLayoutFlag = showCondensedLayout();
|
||||||
const channels = data?.data || [];
|
|
||||||
|
|
||||||
const { currentQuery } = useQueryBuilder();
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
@ -85,6 +84,65 @@ function AlertThreshold(): JSX.Element {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onTooltipOpenChange = (open: boolean): void => {
|
||||||
|
// Stop propagation of click events on tooltip text to dropdown
|
||||||
|
if (open) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const tooltipElement = document.querySelector(
|
||||||
|
'.copyable-tooltip .ant-tooltip-inner',
|
||||||
|
);
|
||||||
|
if (tooltipElement) {
|
||||||
|
tooltipElement.addEventListener(
|
||||||
|
'click',
|
||||||
|
(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
tooltipElement.addEventListener(
|
||||||
|
'mousedown',
|
||||||
|
(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const matchTypeOptionsWithTooltips = THRESHOLD_MATCH_TYPE_OPTIONS.map(
|
||||||
|
(option) => ({
|
||||||
|
...option,
|
||||||
|
label: (
|
||||||
|
<Tooltip
|
||||||
|
title={getMatchTypeTooltip(option.value, thresholdState.operator)}
|
||||||
|
placement="left"
|
||||||
|
overlayClassName="copyable-tooltip"
|
||||||
|
overlayStyle={{
|
||||||
|
maxWidth: '450px',
|
||||||
|
minWidth: '400px',
|
||||||
|
}}
|
||||||
|
overlayInnerStyle={{
|
||||||
|
padding: '12px 16px',
|
||||||
|
userSelect: 'text',
|
||||||
|
WebkitUserSelect: 'text',
|
||||||
|
MozUserSelect: 'text',
|
||||||
|
msUserSelect: 'text',
|
||||||
|
}}
|
||||||
|
mouseEnterDelay={0.2}
|
||||||
|
trigger={['hover', 'click']}
|
||||||
|
destroyTooltipOnHide={false}
|
||||||
|
onOpenChange={onTooltipOpenChange}
|
||||||
|
>
|
||||||
|
<span style={{ display: 'block', width: '100%' }}>{option.label}</span>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const evaluationWindowContext = showCondensedLayoutFlag ? (
|
const evaluationWindowContext = showCondensedLayoutFlag ? (
|
||||||
<EvaluationSettings />
|
<EvaluationSettings />
|
||||||
) : (
|
) : (
|
||||||
@ -114,8 +172,7 @@ function AlertThreshold(): JSX.Element {
|
|||||||
style={{ width: 80 }}
|
style={{ width: 80 }}
|
||||||
options={queryNames}
|
options={queryNames}
|
||||||
/>
|
/>
|
||||||
</div>
|
<Typography.Text className="sentence-text">is</Typography.Text>
|
||||||
<div className="alert-condition-sentence">
|
|
||||||
<Select
|
<Select
|
||||||
value={thresholdState.operator}
|
value={thresholdState.operator}
|
||||||
onChange={(value): void => {
|
onChange={(value): void => {
|
||||||
@ -124,7 +181,7 @@ function AlertThreshold(): JSX.Element {
|
|||||||
payload: value,
|
payload: value,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
style={{ width: 120 }}
|
style={{ width: 180 }}
|
||||||
options={THRESHOLD_OPERATOR_OPTIONS}
|
options={THRESHOLD_OPERATOR_OPTIONS}
|
||||||
/>
|
/>
|
||||||
<Typography.Text className="sentence-text">
|
<Typography.Text className="sentence-text">
|
||||||
@ -138,8 +195,8 @@ function AlertThreshold(): JSX.Element {
|
|||||||
payload: value,
|
payload: value,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
style={{ width: 140 }}
|
style={{ width: 180 }}
|
||||||
options={THRESHOLD_MATCH_TYPE_OPTIONS}
|
options={matchTypeOptionsWithTooltips}
|
||||||
/>
|
/>
|
||||||
<Typography.Text className="sentence-text">
|
<Typography.Text className="sentence-text">
|
||||||
during the {evaluationWindowContext}
|
during the {evaluationWindowContext}
|
||||||
@ -158,6 +215,8 @@ function AlertThreshold(): JSX.Element {
|
|||||||
channels={channels}
|
channels={channels}
|
||||||
isLoadingChannels={isLoadingChannels}
|
isLoadingChannels={isLoadingChannels}
|
||||||
units={categorySelectOptions}
|
units={categorySelectOptions}
|
||||||
|
isErrorChannels={isErrorChannels}
|
||||||
|
refreshChannels={refreshChannels}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<Button
|
<Button
|
||||||
@ -169,6 +228,11 @@ function AlertThreshold(): JSX.Element {
|
|||||||
Add Threshold
|
Add Threshold
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<RoutingPolicyBanner
|
||||||
|
notificationSettings={notificationSettings}
|
||||||
|
setNotificationSettings={setNotificationSettings}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Select, Typography } from 'antd';
|
import { Select, Typography } from 'antd';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { useCreateAlertState } from '../context';
|
import { useCreateAlertState } from '../context';
|
||||||
@ -10,10 +11,26 @@ import {
|
|||||||
ANOMALY_THRESHOLD_OPERATOR_OPTIONS,
|
ANOMALY_THRESHOLD_OPERATOR_OPTIONS,
|
||||||
ANOMALY_TIME_DURATION_OPTIONS,
|
ANOMALY_TIME_DURATION_OPTIONS,
|
||||||
} from '../context/constants';
|
} from '../context/constants';
|
||||||
import { getQueryNames } from './utils';
|
import { AnomalyAndThresholdProps } from './types';
|
||||||
|
import {
|
||||||
|
getQueryNames,
|
||||||
|
NotificationChannelsNotFoundContent,
|
||||||
|
RoutingPolicyBanner,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
function AnomalyThreshold(): JSX.Element {
|
function AnomalyThreshold({
|
||||||
const { thresholdState, setThresholdState } = useCreateAlertState();
|
channels,
|
||||||
|
isLoadingChannels,
|
||||||
|
isErrorChannels,
|
||||||
|
refreshChannels,
|
||||||
|
}: AnomalyAndThresholdProps): JSX.Element {
|
||||||
|
const { user } = useAppContext();
|
||||||
|
const {
|
||||||
|
thresholdState,
|
||||||
|
setThresholdState,
|
||||||
|
notificationSettings,
|
||||||
|
setNotificationSettings,
|
||||||
|
} = useCreateAlertState();
|
||||||
|
|
||||||
const { currentQuery } = useQueryBuilder();
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
@ -27,7 +44,11 @@ function AnomalyThreshold(): JSX.Element {
|
|||||||
return options;
|
return options;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateThreshold = (id: string, field: string, value: string): void => {
|
const updateThreshold = (
|
||||||
|
id: string,
|
||||||
|
field: string,
|
||||||
|
value: string | string[],
|
||||||
|
): void => {
|
||||||
setThresholdState({
|
setThresholdState({
|
||||||
type: 'SET_THRESHOLDS',
|
type: 'SET_THRESHOLDS',
|
||||||
payload: thresholdState.thresholds.map((t) =>
|
payload: thresholdState.thresholds.map((t) =>
|
||||||
@ -53,7 +74,6 @@ function AnomalyThreshold(): JSX.Element {
|
|||||||
payload: value,
|
payload: value,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
style={{ width: 80 }}
|
|
||||||
options={queryNames}
|
options={queryNames}
|
||||||
/>
|
/>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
@ -71,12 +91,11 @@ function AnomalyThreshold(): JSX.Element {
|
|||||||
payload: value,
|
payload: value,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
style={{ width: 80 }}
|
|
||||||
options={ANOMALY_TIME_DURATION_OPTIONS}
|
options={ANOMALY_TIME_DURATION_OPTIONS}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Sentence 2 */}
|
|
||||||
<div className="alert-condition-sentence">
|
<div className="alert-condition-sentence">
|
||||||
|
{/* Sentence 2 */}
|
||||||
<Typography.Text data-testid="threshold-text" className="sentence-text">
|
<Typography.Text data-testid="threshold-text" className="sentence-text">
|
||||||
is
|
is
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@ -90,7 +109,6 @@ function AnomalyThreshold(): JSX.Element {
|
|||||||
value.toString(),
|
value.toString(),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
style={{ width: 80 }}
|
|
||||||
options={deviationOptions}
|
options={deviationOptions}
|
||||||
/>
|
/>
|
||||||
<Typography.Text data-testid="deviations-text" className="sentence-text">
|
<Typography.Text data-testid="deviations-text" className="sentence-text">
|
||||||
@ -105,7 +123,6 @@ function AnomalyThreshold(): JSX.Element {
|
|||||||
payload: value,
|
payload: value,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
style={{ width: 80 }}
|
|
||||||
options={ANOMALY_THRESHOLD_OPERATOR_OPTIONS}
|
options={ANOMALY_THRESHOLD_OPERATOR_OPTIONS}
|
||||||
/>
|
/>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
@ -123,7 +140,6 @@ function AnomalyThreshold(): JSX.Element {
|
|||||||
payload: value,
|
payload: value,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
style={{ width: 80 }}
|
|
||||||
options={ANOMALY_THRESHOLD_MATCH_TYPE_OPTIONS}
|
options={ANOMALY_THRESHOLD_MATCH_TYPE_OPTIONS}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -141,7 +157,6 @@ function AnomalyThreshold(): JSX.Element {
|
|||||||
payload: value,
|
payload: value,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
style={{ width: 80 }}
|
|
||||||
options={ANOMALY_ALGORITHM_OPTIONS}
|
options={ANOMALY_ALGORITHM_OPTIONS}
|
||||||
/>
|
/>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
@ -159,14 +174,58 @@ function AnomalyThreshold(): JSX.Element {
|
|||||||
payload: value,
|
payload: value,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
style={{ width: 80 }}
|
|
||||||
options={ANOMALY_SEASONALITY_OPTIONS}
|
options={ANOMALY_SEASONALITY_OPTIONS}
|
||||||
/>
|
/>
|
||||||
|
{notificationSettings.routingPolicies ? (
|
||||||
|
<>
|
||||||
|
<Typography.Text
|
||||||
|
data-testid="seasonality-text"
|
||||||
|
className="sentence-text"
|
||||||
|
>
|
||||||
|
seasonality to
|
||||||
|
</Typography.Text>
|
||||||
|
<Select
|
||||||
|
value={thresholdState.thresholds[0].channels}
|
||||||
|
onChange={(value): void =>
|
||||||
|
updateThreshold(thresholdState.thresholds[0].id, 'channels', value)
|
||||||
|
}
|
||||||
|
style={{ width: 350 }}
|
||||||
|
options={channels.map((channel) => ({
|
||||||
|
value: channel.id,
|
||||||
|
label: channel.name,
|
||||||
|
}))}
|
||||||
|
mode="multiple"
|
||||||
|
placeholder="Select notification channels"
|
||||||
|
showSearch
|
||||||
|
maxTagCount={2}
|
||||||
|
maxTagPlaceholder={(omittedValues): string =>
|
||||||
|
`+${omittedValues.length} more`
|
||||||
|
}
|
||||||
|
maxTagTextLength={10}
|
||||||
|
filterOption={(input, option): boolean =>
|
||||||
|
option?.label?.toLowerCase().includes(input.toLowerCase()) || false
|
||||||
|
}
|
||||||
|
status={isErrorChannels ? 'error' : undefined}
|
||||||
|
disabled={isLoadingChannels}
|
||||||
|
notFoundContent={
|
||||||
|
<NotificationChannelsNotFoundContent
|
||||||
|
user={user}
|
||||||
|
refreshChannels={refreshChannels}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
<Typography.Text data-testid="seasonality-text" className="sentence-text">
|
<Typography.Text data-testid="seasonality-text" className="sentence-text">
|
||||||
seasonality
|
seasonality
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<RoutingPolicyBanner
|
||||||
|
notificationSettings={notificationSettings}
|
||||||
|
setNotificationSettings={setNotificationSettings}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
import { Button, Input, Select, Space, Tooltip, Typography } from 'antd';
|
import { Button, Input, Select, Tooltip, Typography } from 'antd';
|
||||||
import { ChartLine, CircleX } from 'lucide-react';
|
import { CircleX, Trash } from 'lucide-react';
|
||||||
|
import { useAppContext } from 'providers/App/App';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { useCreateAlertState } from '../context';
|
||||||
|
import { AlertThresholdOperator } from '../context/types';
|
||||||
import { ThresholdItemProps } from './types';
|
import { ThresholdItemProps } from './types';
|
||||||
|
import { NotificationChannelsNotFoundContent } from './utils';
|
||||||
|
|
||||||
function ThresholdItem({
|
function ThresholdItem({
|
||||||
threshold,
|
threshold,
|
||||||
@ -11,7 +15,12 @@ function ThresholdItem({
|
|||||||
showRemoveButton,
|
showRemoveButton,
|
||||||
channels,
|
channels,
|
||||||
units,
|
units,
|
||||||
|
isErrorChannels,
|
||||||
|
refreshChannels,
|
||||||
|
isLoadingChannels,
|
||||||
}: ThresholdItemProps): JSX.Element {
|
}: ThresholdItemProps): JSX.Element {
|
||||||
|
const { user } = useAppContext();
|
||||||
|
const { thresholdState, notificationSettings } = useCreateAlertState();
|
||||||
const [showRecoveryThreshold, setShowRecoveryThreshold] = useState(false);
|
const [showRecoveryThreshold, setShowRecoveryThreshold] = useState(false);
|
||||||
|
|
||||||
const yAxisUnitSelect = useMemo(() => {
|
const yAxisUnitSelect = useMemo(() => {
|
||||||
@ -45,6 +54,31 @@ function ThresholdItem({
|
|||||||
return component;
|
return component;
|
||||||
}, [units, threshold.unit, updateThreshold, threshold.id]);
|
}, [units, threshold.unit, updateThreshold, threshold.id]);
|
||||||
|
|
||||||
|
const getOperatorSymbol = (): string => {
|
||||||
|
switch (thresholdState.operator) {
|
||||||
|
case AlertThresholdOperator.IS_ABOVE:
|
||||||
|
return '>';
|
||||||
|
case AlertThresholdOperator.IS_BELOW:
|
||||||
|
return '<';
|
||||||
|
case AlertThresholdOperator.IS_EQUAL_TO:
|
||||||
|
return '=';
|
||||||
|
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
|
||||||
|
return '!=';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// const addRecoveryThreshold = (): void => {
|
||||||
|
// setShowRecoveryThreshold(true);
|
||||||
|
// updateThreshold(threshold.id, 'recoveryThresholdValue', 0);
|
||||||
|
// };
|
||||||
|
|
||||||
|
const removeRecoveryThreshold = (): void => {
|
||||||
|
setShowRecoveryThreshold(false);
|
||||||
|
updateThreshold(threshold.id, 'recoveryThresholdValue', null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={threshold.id} className="threshold-item">
|
<div key={threshold.id} className="threshold-item">
|
||||||
<div className="threshold-row">
|
<div className="threshold-row">
|
||||||
@ -54,80 +88,111 @@ function ThresholdItem({
|
|||||||
style={{ backgroundColor: threshold.color }}
|
style={{ backgroundColor: threshold.color }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Space className="threshold-controls">
|
<div className="threshold-controls">
|
||||||
<div className="threshold-inputs">
|
|
||||||
<Input.Group>
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter threshold name"
|
placeholder="Enter threshold name"
|
||||||
value={threshold.label}
|
value={threshold.label}
|
||||||
onChange={(e): void =>
|
onChange={(e): void =>
|
||||||
updateThreshold(threshold.id, 'label', e.target.value)
|
updateThreshold(threshold.id, 'label', e.target.value)
|
||||||
}
|
}
|
||||||
style={{ width: 260 }}
|
style={{ width: 200 }}
|
||||||
/>
|
/>
|
||||||
|
<Typography.Text className="sentence-text">on value</Typography.Text>
|
||||||
|
<Typography.Text className="sentence-text highlighted-text">
|
||||||
|
{getOperatorSymbol()}
|
||||||
|
</Typography.Text>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter threshold value"
|
placeholder="Enter threshold value"
|
||||||
value={threshold.thresholdValue}
|
value={threshold.thresholdValue}
|
||||||
onChange={(e): void =>
|
onChange={(e): void =>
|
||||||
updateThreshold(threshold.id, 'thresholdValue', e.target.value)
|
updateThreshold(threshold.id, 'thresholdValue', e.target.value)
|
||||||
}
|
}
|
||||||
style={{ width: 210 }}
|
style={{ width: 100 }}
|
||||||
|
type="number"
|
||||||
/>
|
/>
|
||||||
{yAxisUnitSelect}
|
{yAxisUnitSelect}
|
||||||
</Input.Group>
|
{!notificationSettings.routingPolicies && (
|
||||||
</div>
|
<>
|
||||||
<Typography.Text className="sentence-text">to</Typography.Text>
|
<Typography.Text className="sentence-text">send to</Typography.Text>
|
||||||
<Select
|
<Select
|
||||||
value={threshold.channels}
|
value={threshold.channels}
|
||||||
onChange={(value): void =>
|
onChange={(value): void =>
|
||||||
updateThreshold(threshold.id, 'channels', value)
|
updateThreshold(threshold.id, 'channels', value)
|
||||||
}
|
}
|
||||||
style={{ width: 260 }}
|
style={{ width: 350 }}
|
||||||
options={channels.map((channel) => ({
|
options={channels.map((channel) => ({
|
||||||
value: channel.id,
|
value: channel.name,
|
||||||
label: channel.name,
|
label: channel.name,
|
||||||
}))}
|
}))}
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
placeholder="Select notification channels"
|
placeholder="Select notification channels"
|
||||||
|
showSearch
|
||||||
|
maxTagCount={2}
|
||||||
|
maxTagPlaceholder={(omittedValues): string =>
|
||||||
|
`+${omittedValues.length} more`
|
||||||
|
}
|
||||||
|
maxTagTextLength={10}
|
||||||
|
filterOption={(input, option): boolean =>
|
||||||
|
option?.label?.toLowerCase().includes(input.toLowerCase()) || false
|
||||||
|
}
|
||||||
|
status={isErrorChannels ? 'error' : undefined}
|
||||||
|
disabled={isLoadingChannels}
|
||||||
|
notFoundContent={
|
||||||
|
<NotificationChannelsNotFoundContent
|
||||||
|
user={user}
|
||||||
|
refreshChannels={refreshChannels}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{showRecoveryThreshold && (
|
||||||
|
<>
|
||||||
|
<Typography.Text className="sentence-text">recover on</Typography.Text>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter recovery threshold value"
|
||||||
|
value={threshold.recoveryThresholdValue ?? ''}
|
||||||
|
onChange={(e): void =>
|
||||||
|
updateThreshold(threshold.id, 'recoveryThresholdValue', e.target.value)
|
||||||
|
}
|
||||||
|
style={{ width: 100 }}
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<Tooltip title="Remove recovery threshold">
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
icon={<Trash size={16} />}
|
||||||
|
onClick={removeRecoveryThreshold}
|
||||||
|
className="icon-btn"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Button.Group>
|
<Button.Group>
|
||||||
{!showRecoveryThreshold && (
|
{/* TODO: Add recovery threshold back once the functionality is implemented */}
|
||||||
|
{/* {!showRecoveryThreshold && (
|
||||||
|
<Tooltip title="Add recovery threshold">
|
||||||
<Button
|
<Button
|
||||||
type="default"
|
type="default"
|
||||||
icon={<ChartLine size={16} />}
|
icon={<ChartLine size={16} />}
|
||||||
className="icon-btn"
|
className="icon-btn"
|
||||||
onClick={(): void => setShowRecoveryThreshold(true)}
|
onClick={addRecoveryThreshold}
|
||||||
/>
|
/>
|
||||||
)}
|
</Tooltip>
|
||||||
|
)} */}
|
||||||
{showRemoveButton && (
|
{showRemoveButton && (
|
||||||
|
<Tooltip title="Remove threshold">
|
||||||
<Button
|
<Button
|
||||||
type="default"
|
type="default"
|
||||||
icon={<CircleX size={16} />}
|
icon={<CircleX size={16} />}
|
||||||
onClick={(): void => removeThreshold(threshold.id)}
|
onClick={(): void => removeThreshold(threshold.id)}
|
||||||
className="icon-btn"
|
className="icon-btn"
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Button.Group>
|
</Button.Group>
|
||||||
</Space>
|
|
||||||
</div>
|
</div>
|
||||||
{showRecoveryThreshold && (
|
</div>
|
||||||
<Input.Group className="recovery-threshold-input-group">
|
|
||||||
<Input
|
|
||||||
placeholder="Recovery threshold"
|
|
||||||
disabled
|
|
||||||
style={{ width: 260 }}
|
|
||||||
className="recovery-threshold-label"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder="Enter recovery threshold value"
|
|
||||||
value={threshold.recoveryThresholdValue}
|
|
||||||
onChange={(e): void =>
|
|
||||||
updateThreshold(threshold.id, 'recoveryThresholdValue', e.target.value)
|
|
||||||
}
|
|
||||||
style={{ width: 210 }}
|
|
||||||
/>
|
|
||||||
</Input.Group>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
|
|
||||||
import { CreateAlertProvider } from '../../context';
|
import { CreateAlertProvider } from '../../context';
|
||||||
import AlertCondition from '../AlertCondition';
|
import AlertCondition from '../AlertCondition';
|
||||||
@ -105,7 +106,7 @@ const renderAlertCondition = (
|
|||||||
return render(
|
return render(
|
||||||
<MemoryRouter initialEntries={initialEntries}>
|
<MemoryRouter initialEntries={initialEntries}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<CreateAlertProvider>
|
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||||
<AlertCondition />
|
<AlertCondition />
|
||||||
</CreateAlertProvider>
|
</CreateAlertProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
@ -126,9 +127,10 @@ describe('AlertCondition', () => {
|
|||||||
|
|
||||||
// Verify default alertType is METRICS_BASED_ALERT (shows AlertThreshold component)
|
// Verify default alertType is METRICS_BASED_ALERT (shows AlertThreshold component)
|
||||||
expect(screen.getByTestId(ALERT_THRESHOLD_TEST_ID)).toBeInTheDocument();
|
expect(screen.getByTestId(ALERT_THRESHOLD_TEST_ID)).toBeInTheDocument();
|
||||||
expect(
|
// TODO: uncomment this when anomaly tab is implemented
|
||||||
screen.queryByTestId(ANOMALY_THRESHOLD_TEST_ID),
|
// expect(
|
||||||
).not.toBeInTheDocument();
|
// screen.queryByTestId(ANOMALY_THRESHOLD_TEST_ID),
|
||||||
|
// ).not.toBeInTheDocument();
|
||||||
|
|
||||||
// Verify threshold tab is active by default
|
// Verify threshold tab is active by default
|
||||||
const thresholdTab = screen.getByText(THRESHOLD_TAB_TEXT);
|
const thresholdTab = screen.getByText(THRESHOLD_TAB_TEXT);
|
||||||
@ -136,7 +138,8 @@ describe('AlertCondition', () => {
|
|||||||
|
|
||||||
// Verify both tabs are visible (METRICS_BASED_ALERT supports multiple tabs)
|
// Verify both tabs are visible (METRICS_BASED_ALERT supports multiple tabs)
|
||||||
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
|
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
|
||||||
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
// TODO: uncomment this when anomaly tab is implemented
|
||||||
|
// expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders threshold tab by default', () => {
|
it('renders threshold tab by default', () => {
|
||||||
@ -151,7 +154,8 @@ describe('AlertCondition', () => {
|
|||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders anomaly tab when alert type supports multiple tabs', () => {
|
// TODO: Unskip this when anomaly tab is implemented
|
||||||
|
it.skip('renders anomaly tab when alert type supports multiple tabs', () => {
|
||||||
renderAlertCondition();
|
renderAlertCondition();
|
||||||
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
||||||
expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
|
expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
|
||||||
@ -165,7 +169,8 @@ describe('AlertCondition', () => {
|
|||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows AnomalyThreshold component when alert type is anomaly based', () => {
|
// TODO: Unskip this when anomaly tab is implemented
|
||||||
|
it.skip('shows AnomalyThreshold component when alert type is anomaly based', () => {
|
||||||
renderAlertCondition();
|
renderAlertCondition();
|
||||||
|
|
||||||
// Click on anomaly tab to switch to anomaly-based alert
|
// Click on anomaly tab to switch to anomaly-based alert
|
||||||
@ -176,7 +181,8 @@ describe('AlertCondition', () => {
|
|||||||
expect(screen.queryByTestId(ALERT_THRESHOLD_TEST_ID)).not.toBeInTheDocument();
|
expect(screen.queryByTestId(ALERT_THRESHOLD_TEST_ID)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('switches between threshold and anomaly tabs', () => {
|
// TODO: Unskip this when anomaly tab is implemented
|
||||||
|
it.skip('switches between threshold and anomaly tabs', () => {
|
||||||
renderAlertCondition();
|
renderAlertCondition();
|
||||||
|
|
||||||
// Initially shows threshold component
|
// Initially shows threshold component
|
||||||
@ -201,7 +207,8 @@ describe('AlertCondition', () => {
|
|||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies active tab styling correctly', () => {
|
// TODO: Unskip this when anomaly tab is implemented
|
||||||
|
it.skip('applies active tab styling correctly', () => {
|
||||||
renderAlertCondition();
|
renderAlertCondition();
|
||||||
|
|
||||||
const thresholdTab = screen.getByText(THRESHOLD_TAB_TEXT);
|
const thresholdTab = screen.getByText(THRESHOLD_TAB_TEXT);
|
||||||
@ -222,21 +229,21 @@ describe('AlertCondition', () => {
|
|||||||
it('shows multiple tabs for METRICS_BASED_ALERT', () => {
|
it('shows multiple tabs for METRICS_BASED_ALERT', () => {
|
||||||
renderAlertCondition('METRIC_BASED_ALERT');
|
renderAlertCondition('METRIC_BASED_ALERT');
|
||||||
|
|
||||||
// Both tabs should be visible
|
// TODO: uncomment this when anomaly tab is implemented
|
||||||
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
|
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
|
||||||
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
// expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
||||||
expect(screen.getByTestId(THRESHOLD_VIEW_TEST_ID)).toBeInTheDocument();
|
expect(screen.getByTestId(THRESHOLD_VIEW_TEST_ID)).toBeInTheDocument();
|
||||||
expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
|
// expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows multiple tabs for ANOMALY_BASED_ALERT', () => {
|
it('shows multiple tabs for ANOMALY_BASED_ALERT', () => {
|
||||||
renderAlertCondition('ANOMALY_BASED_ALERT');
|
renderAlertCondition('ANOMALY_BASED_ALERT');
|
||||||
|
|
||||||
// Both tabs should be visible
|
|
||||||
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
|
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
|
||||||
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId(THRESHOLD_VIEW_TEST_ID)).toBeInTheDocument();
|
expect(screen.getByTestId(THRESHOLD_VIEW_TEST_ID)).toBeInTheDocument();
|
||||||
expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
|
// TODO: uncomment this when anomaly tab is implemented
|
||||||
|
// expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
||||||
|
// expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows only threshold tab for LOGS_BASED_ALERT', () => {
|
it('shows only threshold tab for LOGS_BASED_ALERT', () => {
|
||||||
|
|||||||
@ -3,11 +3,23 @@
|
|||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
import { Channels } from 'types/api/channels/getAll';
|
import { Channels } from 'types/api/channels/getAll';
|
||||||
|
|
||||||
import { CreateAlertProvider } from '../../context';
|
import { CreateAlertProvider } from '../../context';
|
||||||
import AlertThreshold from '../AlertThreshold';
|
import AlertThreshold from '../AlertThreshold';
|
||||||
|
|
||||||
|
const mockChannels: Channels[] = [];
|
||||||
|
const mockRefreshChannels = jest.fn();
|
||||||
|
const mockIsLoadingChannels = false;
|
||||||
|
const mockIsErrorChannels = false;
|
||||||
|
const mockProps = {
|
||||||
|
channels: mockChannels,
|
||||||
|
isLoadingChannels: mockIsLoadingChannels,
|
||||||
|
isErrorChannels: mockIsErrorChannels,
|
||||||
|
refreshChannels: mockRefreshChannels,
|
||||||
|
};
|
||||||
|
|
||||||
jest.mock('uplot', () => {
|
jest.mock('uplot', () => {
|
||||||
const paths = {
|
const paths = {
|
||||||
spline: jest.fn(),
|
spline: jest.fn(),
|
||||||
@ -99,7 +111,7 @@ jest.mock('container/NewWidget/RightContainer/alertFomatCategories', () => ({
|
|||||||
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',
|
||||||
IS_ABOVE: 'IS ABOVE',
|
IS_ABOVE: 'ABOVE',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const createTestQueryClient = (): QueryClient =>
|
const createTestQueryClient = (): QueryClient =>
|
||||||
@ -116,8 +128,8 @@ const renderAlertThreshold = (): ReturnType<typeof render> => {
|
|||||||
return render(
|
return render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<CreateAlertProvider>
|
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||||
<AlertThreshold />
|
<AlertThreshold {...mockProps} />
|
||||||
</CreateAlertProvider>
|
</CreateAlertProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</MemoryRouter>,
|
</MemoryRouter>,
|
||||||
@ -125,7 +137,10 @@ const renderAlertThreshold = (): ReturnType<typeof render> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const verifySelectRenders = (title: string): void => {
|
const verifySelectRenders = (title: string): void => {
|
||||||
const select = screen.getByTitle(title);
|
let select = screen.queryByTitle(title);
|
||||||
|
if (!select) {
|
||||||
|
select = screen.getByText(title);
|
||||||
|
}
|
||||||
expect(select).toBeInTheDocument();
|
expect(select).toBeInTheDocument();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import {
|
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
|
||||||
INITIAL_ALERT_STATE,
|
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
|
||||||
INITIAL_ALERT_THRESHOLD_STATE,
|
import * as appHooks from 'providers/App/App';
|
||||||
} from 'container/CreateAlertV2/context/constants';
|
|
||||||
|
|
||||||
import * as context from '../../context';
|
import * as context from '../../context';
|
||||||
import AnomalyThreshold from '../AnomalyThreshold';
|
import AnomalyThreshold from '../AnomalyThreshold';
|
||||||
|
|
||||||
|
jest.spyOn(appHooks, 'useAppContext').mockReturnValue(getAppContextMockState());
|
||||||
|
|
||||||
jest.mock('uplot', () => {
|
jest.mock('uplot', () => {
|
||||||
const paths = {
|
const paths = {
|
||||||
spline: jest.fn(),
|
spline: jest.fn(),
|
||||||
@ -23,12 +24,12 @@ jest.mock('uplot', () => {
|
|||||||
|
|
||||||
const mockSetAlertState = jest.fn();
|
const mockSetAlertState = jest.fn();
|
||||||
const mockSetThresholdState = jest.fn();
|
const mockSetThresholdState = jest.fn();
|
||||||
jest.spyOn(context, 'useCreateAlertState').mockReturnValue({
|
jest.spyOn(context, 'useCreateAlertState').mockReturnValue(
|
||||||
alertState: INITIAL_ALERT_STATE,
|
createMockAlertContextState({
|
||||||
setAlertState: mockSetAlertState,
|
|
||||||
thresholdState: INITIAL_ALERT_THRESHOLD_STATE,
|
|
||||||
setThresholdState: mockSetThresholdState,
|
setThresholdState: mockSetThresholdState,
|
||||||
} as any);
|
setAlertState: mockSetAlertState,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Mock useQueryBuilder hook
|
// Mock useQueryBuilder hook
|
||||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||||
@ -54,7 +55,14 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const renderAnomalyThreshold = (): ReturnType<typeof render> =>
|
const renderAnomalyThreshold = (): ReturnType<typeof render> =>
|
||||||
render(<AnomalyThreshold />);
|
render(
|
||||||
|
<AnomalyThreshold
|
||||||
|
channels={[]}
|
||||||
|
isLoadingChannels={false}
|
||||||
|
isErrorChannels={false}
|
||||||
|
refreshChannels={jest.fn()}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
describe('AnomalyThreshold', () => {
|
describe('AnomalyThreshold', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@ -2,15 +2,37 @@
|
|||||||
/* 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 { DefaultOptionType } from 'antd/es/select';
|
import { DefaultOptionType } from 'antd/es/select';
|
||||||
|
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
|
||||||
|
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
|
||||||
|
import * as appHooks from 'providers/App/App';
|
||||||
import { Channels } from 'types/api/channels/getAll';
|
import { Channels } from 'types/api/channels/getAll';
|
||||||
|
|
||||||
|
import * as context from '../../context';
|
||||||
import ThresholdItem from '../ThresholdItem';
|
import ThresholdItem from '../ThresholdItem';
|
||||||
import { ThresholdItemProps } from '../types';
|
import { ThresholdItemProps } from '../types';
|
||||||
|
|
||||||
// Mock the enableRecoveryThreshold utility
|
jest.spyOn(appHooks, 'useAppContext').mockReturnValue(getAppContextMockState());
|
||||||
jest.mock('../../utils', () => ({
|
|
||||||
enableRecoveryThreshold: jest.fn(() => true),
|
jest.mock('uplot', () => {
|
||||||
}));
|
const paths = {
|
||||||
|
spline: jest.fn(),
|
||||||
|
bars: jest.fn(),
|
||||||
|
};
|
||||||
|
const uplotMock: any = jest.fn(() => ({
|
||||||
|
paths,
|
||||||
|
}));
|
||||||
|
uplotMock.paths = paths;
|
||||||
|
return uplotMock;
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockSetAlertState = jest.fn();
|
||||||
|
const mockSetThresholdState = jest.fn();
|
||||||
|
jest.spyOn(context, 'useCreateAlertState').mockReturnValue(
|
||||||
|
createMockAlertContextState({
|
||||||
|
setThresholdState: mockSetThresholdState,
|
||||||
|
setAlertState: mockSetAlertState,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const TEST_CONSTANTS = {
|
const TEST_CONSTANTS = {
|
||||||
THRESHOLD_ID: 'test-threshold-1',
|
THRESHOLD_ID: 'test-threshold-1',
|
||||||
@ -21,6 +43,7 @@ const TEST_CONSTANTS = {
|
|||||||
CHANNEL_2: 'channel-2',
|
CHANNEL_2: 'channel-2',
|
||||||
CHANNEL_3: 'channel-3',
|
CHANNEL_3: 'channel-3',
|
||||||
EMAIL_CHANNEL_NAME: 'Email Channel',
|
EMAIL_CHANNEL_NAME: 'Email Channel',
|
||||||
|
EMAIL_CHANNEL_TRUNCATED: 'Email Chan...',
|
||||||
ENTER_THRESHOLD_NAME: 'Enter threshold name',
|
ENTER_THRESHOLD_NAME: 'Enter threshold name',
|
||||||
ENTER_THRESHOLD_VALUE: 'Enter threshold value',
|
ENTER_THRESHOLD_VALUE: 'Enter threshold value',
|
||||||
ENTER_RECOVERY_THRESHOLD_VALUE: 'Enter recovery threshold value',
|
ENTER_RECOVERY_THRESHOLD_VALUE: 'Enter recovery threshold value',
|
||||||
@ -59,6 +82,8 @@ const defaultProps: ThresholdItemProps = {
|
|||||||
channels: mockChannels,
|
channels: mockChannels,
|
||||||
isLoadingChannels: false,
|
isLoadingChannels: false,
|
||||||
units: mockUnits,
|
units: mockUnits,
|
||||||
|
isErrorChannels: false,
|
||||||
|
refreshChannels: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderThresholdItem = (
|
const renderThresholdItem = (
|
||||||
@ -77,10 +102,11 @@ const verifySelectorWidth = (
|
|||||||
expect(selector.closest('.ant-select')).toHaveStyle(`width: ${expectedWidth}`);
|
expect(selector.closest('.ant-select')).toHaveStyle(`width: ${expectedWidth}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showRecoveryThreshold = (): void => {
|
// TODO: Unskip this when recovery threshold is implemented
|
||||||
const recoveryButton = screen.getByRole('button', { name: '' });
|
// const showRecoveryThreshold = (): void => {
|
||||||
fireEvent.click(recoveryButton);
|
// const recoveryButton = screen.getByRole('button', { name: '' });
|
||||||
};
|
// fireEvent.click(recoveryButton);
|
||||||
|
// };
|
||||||
|
|
||||||
const verifyComponentRendersWithLoading = (): void => {
|
const verifyComponentRendersWithLoading = (): void => {
|
||||||
expect(
|
expect(
|
||||||
@ -122,7 +148,7 @@ describe('ThresholdItem', () => {
|
|||||||
const valueInput = screen.getByPlaceholderText(
|
const valueInput = screen.getByPlaceholderText(
|
||||||
TEST_CONSTANTS.ENTER_THRESHOLD_VALUE,
|
TEST_CONSTANTS.ENTER_THRESHOLD_VALUE,
|
||||||
);
|
);
|
||||||
expect(valueInput).toHaveValue('100');
|
expect(valueInput).toHaveValue(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders unit selector with correct value', () => {
|
it('renders unit selector with correct value', () => {
|
||||||
@ -132,15 +158,6 @@ describe('ThresholdItem', () => {
|
|||||||
expect(screen.getByText('Bytes')).toBeInTheDocument();
|
expect(screen.getByText('Bytes')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders channels selector with correct value', () => {
|
|
||||||
renderThresholdItem();
|
|
||||||
|
|
||||||
// Check for the channels selector by looking for the displayed text
|
|
||||||
expect(
|
|
||||||
screen.getByText(TEST_CONSTANTS.EMAIL_CHANNEL_NAME),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates threshold label when label input changes', () => {
|
it('updates threshold label when label input changes', () => {
|
||||||
const updateThreshold = jest.fn();
|
const updateThreshold = jest.fn();
|
||||||
renderThresholdItem({ updateThreshold });
|
renderThresholdItem({ updateThreshold });
|
||||||
@ -212,38 +229,31 @@ describe('ThresholdItem', () => {
|
|||||||
|
|
||||||
// The remove button is the second button (with circle-x icon)
|
// The remove button is the second button (with circle-x icon)
|
||||||
const buttons = screen.getAllByRole('button');
|
const buttons = screen.getAllByRole('button');
|
||||||
expect(buttons).toHaveLength(2); // Recovery button + remove button
|
expect(buttons).toHaveLength(1); // remove button
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not show remove button when showRemoveButton is false', () => {
|
it('does not show remove button when showRemoveButton is false', () => {
|
||||||
renderThresholdItem({ showRemoveButton: false });
|
renderThresholdItem({ showRemoveButton: false });
|
||||||
|
|
||||||
// Only the recovery button should be present
|
// No buttons should be present
|
||||||
const buttons = screen.getAllByRole('button');
|
const buttons = screen.queryAllByRole('button');
|
||||||
expect(buttons).toHaveLength(1); // Only recovery button
|
expect(buttons).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls removeThreshold when remove button is clicked', () => {
|
it('calls removeThreshold when remove button is clicked', () => {
|
||||||
const removeThreshold = jest.fn();
|
const removeThreshold = jest.fn();
|
||||||
renderThresholdItem({ showRemoveButton: true, removeThreshold });
|
renderThresholdItem({ showRemoveButton: true, removeThreshold });
|
||||||
|
|
||||||
// The remove button is the second button (with circle-x icon)
|
// The remove button is the first button (with circle-x icon)
|
||||||
const buttons = screen.getAllByRole('button');
|
const buttons = screen.getAllByRole('button');
|
||||||
const removeButton = buttons[1]; // Second button is the remove button
|
const removeButton = buttons[0];
|
||||||
fireEvent.click(removeButton);
|
fireEvent.click(removeButton);
|
||||||
|
|
||||||
expect(removeThreshold).toHaveBeenCalledWith(TEST_CONSTANTS.THRESHOLD_ID);
|
expect(removeThreshold).toHaveBeenCalledWith(TEST_CONSTANTS.THRESHOLD_ID);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows recovery threshold button when recovery threshold is enabled', () => {
|
// TODO: Unskip this when recovery threshold is implemented
|
||||||
renderThresholdItem();
|
it.skip('shows recovery threshold inputs when recovery button is clicked', () => {
|
||||||
|
|
||||||
// The recovery button is the first button (with chart-line icon)
|
|
||||||
const buttons = screen.getAllByRole('button');
|
|
||||||
expect(buttons).toHaveLength(1); // Recovery button
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows recovery threshold inputs when recovery button is clicked', () => {
|
|
||||||
renderThresholdItem();
|
renderThresholdItem();
|
||||||
|
|
||||||
// The recovery button is the first button (with chart-line icon)
|
// The recovery button is the first button (with chart-line icon)
|
||||||
@ -251,13 +261,16 @@ describe('ThresholdItem', () => {
|
|||||||
const recoveryButton = buttons[0]; // First button is the recovery button
|
const recoveryButton = buttons[0]; // First button is the recovery button
|
||||||
fireEvent.click(recoveryButton);
|
fireEvent.click(recoveryButton);
|
||||||
|
|
||||||
expect(screen.getByPlaceholderText('Recovery threshold')).toBeInTheDocument();
|
expect(
|
||||||
|
screen.getByPlaceholderText('Enter recovery threshold value'),
|
||||||
|
).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.getByPlaceholderText(TEST_CONSTANTS.ENTER_RECOVERY_THRESHOLD_VALUE),
|
screen.getByPlaceholderText(TEST_CONSTANTS.ENTER_RECOVERY_THRESHOLD_VALUE),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates recovery threshold value when input changes', () => {
|
// TODO: Unskip this when recovery threshold is implemented
|
||||||
|
it.skip('updates recovery threshold value when input changes', () => {
|
||||||
const updateThreshold = jest.fn();
|
const updateThreshold = jest.fn();
|
||||||
renderThresholdItem({ updateThreshold });
|
renderThresholdItem({ updateThreshold });
|
||||||
|
|
||||||
@ -290,22 +303,6 @@ describe('ThresholdItem', () => {
|
|||||||
verifyUnitSelectorDisabled();
|
verifyUnitSelectorDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders channels as multiple select options', () => {
|
|
||||||
renderThresholdItem();
|
|
||||||
|
|
||||||
// Check that channels are rendered as multiple select
|
|
||||||
expect(
|
|
||||||
screen.getByText(TEST_CONSTANTS.EMAIL_CHANNEL_NAME),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Should be able to select multiple channels
|
|
||||||
const channelSelectors = screen.getAllByRole('combobox');
|
|
||||||
const channelSelector = channelSelectors[1]; // Second combobox is the channels selector
|
|
||||||
fireEvent.change(channelSelector, {
|
|
||||||
target: { value: [TEST_CONSTANTS.CHANNEL_1, TEST_CONSTANTS.CHANNEL_2] },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles empty threshold values correctly', () => {
|
it('handles empty threshold values correctly', () => {
|
||||||
const emptyThreshold = {
|
const emptyThreshold = {
|
||||||
...mockThreshold,
|
...mockThreshold,
|
||||||
@ -318,7 +315,7 @@ describe('ThresholdItem', () => {
|
|||||||
renderThresholdItem({ threshold: emptyThreshold });
|
renderThresholdItem({ threshold: emptyThreshold });
|
||||||
|
|
||||||
expect(screen.getByPlaceholderText('Enter threshold name')).toHaveValue('');
|
expect(screen.getByPlaceholderText('Enter threshold name')).toHaveValue('');
|
||||||
expect(screen.getByPlaceholderText('Enter threshold value')).toHaveValue('0');
|
expect(screen.getByPlaceholderText('Enter threshold value')).toHaveValue(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders with correct input widths', () => {
|
it('renders with correct input widths', () => {
|
||||||
@ -331,13 +328,13 @@ describe('ThresholdItem', () => {
|
|||||||
TEST_CONSTANTS.ENTER_THRESHOLD_VALUE,
|
TEST_CONSTANTS.ENTER_THRESHOLD_VALUE,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(labelInput).toHaveStyle('width: 260px');
|
expect(labelInput).toHaveStyle('width: 200px');
|
||||||
expect(valueInput).toHaveStyle('width: 210px');
|
expect(valueInput).toHaveStyle('width: 100px');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders channels selector with correct width', () => {
|
it('renders channels selector with correct width', () => {
|
||||||
renderThresholdItem();
|
renderThresholdItem();
|
||||||
verifySelectorWidth(1, '260px');
|
verifySelectorWidth(1, '350px');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders unit selector with correct width', () => {
|
it('renders unit selector with correct width', () => {
|
||||||
@ -350,37 +347,14 @@ describe('ThresholdItem', () => {
|
|||||||
verifyComponentRendersWithLoading();
|
verifyComponentRendersWithLoading();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders recovery threshold with correct initial value', () => {
|
it.skip('renders recovery threshold with correct initial value', () => {
|
||||||
renderThresholdItem();
|
renderThresholdItem();
|
||||||
showRecoveryThreshold();
|
// showRecoveryThreshold();
|
||||||
|
|
||||||
const recoveryValueInput = screen.getByPlaceholderText(
|
const recoveryValueInput = screen.getByPlaceholderText(
|
||||||
TEST_CONSTANTS.ENTER_RECOVERY_THRESHOLD_VALUE,
|
TEST_CONSTANTS.ENTER_RECOVERY_THRESHOLD_VALUE,
|
||||||
);
|
);
|
||||||
expect(recoveryValueInput).toHaveValue('80');
|
expect(recoveryValueInput).toHaveValue(80);
|
||||||
});
|
|
||||||
|
|
||||||
it('renders recovery threshold label as disabled', () => {
|
|
||||||
renderThresholdItem();
|
|
||||||
showRecoveryThreshold();
|
|
||||||
|
|
||||||
const recoveryLabelInput = screen.getByPlaceholderText('Recovery threshold');
|
|
||||||
expect(recoveryLabelInput).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders correct channel options', () => {
|
|
||||||
renderThresholdItem();
|
|
||||||
|
|
||||||
// Check that channels are rendered
|
|
||||||
expect(
|
|
||||||
screen.getByText(TEST_CONSTANTS.EMAIL_CHANNEL_NAME),
|
|
||||||
).toBeInTheDocument();
|
|
||||||
|
|
||||||
// Should be able to select different channels
|
|
||||||
const channelSelectors = screen.getAllByRole('combobox');
|
|
||||||
const channelSelector = channelSelectors[1]; // Second combobox is the channels selector
|
|
||||||
fireEvent.change(channelSelector, { target: { value: 'channel-2' } });
|
|
||||||
expect(screen.getByText('Slack Channel')).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles threshold without channels', () => {
|
it('handles threshold without channels', () => {
|
||||||
|
|||||||
@ -67,7 +67,7 @@
|
|||||||
padding-right: 72px;
|
padding-right: 72px;
|
||||||
background-color: var(--bg-ink-500);
|
background-color: var(--bg-ink-500);
|
||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
width: fit-content;
|
width: 100%;
|
||||||
|
|
||||||
.alert-condition-sentences {
|
.alert-condition-sentences {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -90,7 +90,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ant-select {
|
.ant-select {
|
||||||
width: 240px !important;
|
width: 240px;
|
||||||
|
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
background-color: var(--bg-ink-300);
|
background-color: var(--bg-ink-300);
|
||||||
@ -148,6 +148,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.ant-input {
|
.ant-input {
|
||||||
background-color: var(--bg-ink-400);
|
background-color: var(--bg-ink-400);
|
||||||
@ -277,6 +278,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.routing-policies-info-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
background-color: #4568dc1a;
|
||||||
|
border: 1px solid var(--bg-robin-500);
|
||||||
|
padding: 8px 16px;
|
||||||
|
|
||||||
|
.ant-typography {
|
||||||
|
color: var(--bg-robin-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-threshold-container {
|
||||||
|
.ant-select {
|
||||||
|
.ant-select-selector {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.condensed-alert-threshold-container,
|
.condensed-alert-threshold-container,
|
||||||
@ -293,7 +317,8 @@
|
|||||||
.ant-btn {
|
.ant-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 240px;
|
min-width: 240px;
|
||||||
|
width: auto;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
background-color: var(--bg-ink-300);
|
background-color: var(--bg-ink-300);
|
||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
@ -301,6 +326,7 @@
|
|||||||
.evaluate-alert-conditions-button-left {
|
.evaluate-alert-conditions-button-left {
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.evaluate-alert-conditions-button-right {
|
.evaluate-alert-conditions-button-right {
|
||||||
@ -308,6 +334,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--bg-vanilla-400);
|
color: var(--bg-vanilla-400);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
.evaluate-alert-conditions-button-right-text {
|
.evaluate-alert-conditions-button-right-text {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@ -318,3 +345,229 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.alert-condition-container {
|
||||||
|
.alert-condition {
|
||||||
|
.alert-condition-tabs {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.explorer-view-option {
|
||||||
|
border-left: 0.5px solid var(--bg-vanilla-300);
|
||||||
|
border-bottom: 0.5px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
&.active-tab {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-vanilla-100) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-threshold-container,
|
||||||
|
.anomaly-threshold-container {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.alert-condition-sentences {
|
||||||
|
.alert-condition-sentence {
|
||||||
|
.sentence-text {
|
||||||
|
color: var(--text-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select {
|
||||||
|
.ant-select-selector {
|
||||||
|
background-color: var(--bg-vanilla-300);
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
color: var(--text-ink-400);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--bg-ink-300);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-selection-item {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-arrow {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.thresholds-section {
|
||||||
|
.threshold-item {
|
||||||
|
.threshold-row {
|
||||||
|
.threshold-controls {
|
||||||
|
.threshold-inputs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input {
|
||||||
|
background-color: var(--bg-vanilla-200);
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--bg-ink-300);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select {
|
||||||
|
.ant-select-selector {
|
||||||
|
background-color: var(--bg-vanilla-200);
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--bg-ink-300);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-selection-item {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-arrow {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recovery-threshold-input-group {
|
||||||
|
.recovery-threshold-btn {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
background-color: var(--bg-vanilla-200) !important;
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input {
|
||||||
|
background-color: var(--bg-vanilla-200);
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--bg-ink-300);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-threshold-btn {
|
||||||
|
border: 1px dashed var(--bg-vanilla-300);
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-ink-300);
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.condensed-evaluation-settings-container {
|
||||||
|
.ant-btn {
|
||||||
|
background-color: var(--bg-vanilla-300);
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
min-width: 240px;
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
.evaluate-alert-conditions-button-left {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.evaluate-alert-conditions-button-right {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.evaluate-alert-conditions-button-right-text {
|
||||||
|
background-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlighted-text {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--bg-robin-400);
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tooltip styles
|
||||||
|
.tooltip-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.tooltip-description {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--bg-robin-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-example {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #8b92a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-link {
|
||||||
|
.tooltip-link-text {
|
||||||
|
color: #1890ff;
|
||||||
|
font-size: 11px;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,14 +1,18 @@
|
|||||||
import { DefaultOptionType } from 'antd/es/select';
|
import { DefaultOptionType } from 'antd/es/select';
|
||||||
import { Channels } from 'types/api/channels/getAll';
|
import { Channels } from 'types/api/channels/getAll';
|
||||||
|
|
||||||
import { Threshold } from '../context/types';
|
import {
|
||||||
|
NotificationSettingsAction,
|
||||||
|
NotificationSettingsState,
|
||||||
|
Threshold,
|
||||||
|
} from '../context/types';
|
||||||
|
|
||||||
export type UpdateThreshold = {
|
export type UpdateThreshold = {
|
||||||
(thresholdId: string, field: 'channels', value: string[]): void;
|
(thresholdId: string, field: 'channels', value: string[]): void;
|
||||||
(
|
(
|
||||||
thresholdId: string,
|
thresholdId: string,
|
||||||
field: Exclude<keyof Threshold, 'channels'>,
|
field: Exclude<keyof Threshold, 'channels'>,
|
||||||
value: string,
|
value: string | number | null,
|
||||||
): void;
|
): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -20,4 +24,20 @@ export interface ThresholdItemProps {
|
|||||||
channels: Channels[];
|
channels: Channels[];
|
||||||
isLoadingChannels: boolean;
|
isLoadingChannels: boolean;
|
||||||
units: DefaultOptionType[];
|
units: DefaultOptionType[];
|
||||||
|
isErrorChannels: boolean;
|
||||||
|
refreshChannels: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnomalyAndThresholdProps {
|
||||||
|
channels: Channels[];
|
||||||
|
isLoadingChannels: boolean;
|
||||||
|
isErrorChannels: boolean;
|
||||||
|
refreshChannels: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoutingPolicyBannerProps {
|
||||||
|
notificationSettings: NotificationSettingsState;
|
||||||
|
setNotificationSettings: (
|
||||||
|
notificationSettings: NotificationSettingsAction,
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,19 @@
|
|||||||
|
import { Button, Flex, Switch, Typography } from 'antd';
|
||||||
import { BaseOptionType, DefaultOptionType, SelectProps } from 'antd/es/select';
|
import { BaseOptionType, DefaultOptionType, SelectProps } from 'antd/es/select';
|
||||||
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
|
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
|
||||||
import { Y_AXIS_CATEGORIES } from 'components/YAxisUnitSelector/constants';
|
import { Y_AXIS_CATEGORIES } from 'components/YAxisUnitSelector/constants';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import {
|
||||||
|
AlertThresholdMatchType,
|
||||||
|
AlertThresholdOperator,
|
||||||
|
} from 'container/CreateAlertV2/context/types';
|
||||||
import { getSelectedQueryOptions } from 'container/FormAlertRules/utils';
|
import { getSelectedQueryOptions } from 'container/FormAlertRules/utils';
|
||||||
|
import { IUser } from 'providers/App/types';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
|
import { USER_ROLES } from 'types/roles';
|
||||||
|
|
||||||
|
import { RoutingPolicyBannerProps } from './types';
|
||||||
|
|
||||||
export function getQueryNames(currentQuery: Query): BaseOptionType[] {
|
export function getQueryNames(currentQuery: Query): BaseOptionType[] {
|
||||||
const involvedQueriesInTraceOperator = getInvolvedQueriesInTraceOperator(
|
const involvedQueriesInTraceOperator = getInvolvedQueriesInTraceOperator(
|
||||||
@ -44,3 +54,360 @@ export function getCategorySelectOptionByName(
|
|||||||
) || []
|
) || []
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getOperatorWord = (op: AlertThresholdOperator): string => {
|
||||||
|
switch (op) {
|
||||||
|
case AlertThresholdOperator.IS_ABOVE:
|
||||||
|
return 'exceed';
|
||||||
|
case AlertThresholdOperator.IS_BELOW:
|
||||||
|
return 'fall below';
|
||||||
|
case AlertThresholdOperator.IS_EQUAL_TO:
|
||||||
|
return 'equal';
|
||||||
|
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
|
||||||
|
return 'not equal';
|
||||||
|
default:
|
||||||
|
return 'exceed';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getThresholdValue = (op: AlertThresholdOperator): number => {
|
||||||
|
switch (op) {
|
||||||
|
case AlertThresholdOperator.IS_ABOVE:
|
||||||
|
return 80;
|
||||||
|
case AlertThresholdOperator.IS_BELOW:
|
||||||
|
return 50;
|
||||||
|
case AlertThresholdOperator.IS_EQUAL_TO:
|
||||||
|
return 100;
|
||||||
|
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
|
||||||
|
return 0;
|
||||||
|
default:
|
||||||
|
return 80;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDataPoints = (
|
||||||
|
matchType: AlertThresholdMatchType,
|
||||||
|
op: AlertThresholdOperator,
|
||||||
|
): number[] => {
|
||||||
|
const dataPointMap: Record<
|
||||||
|
AlertThresholdMatchType,
|
||||||
|
Record<AlertThresholdOperator, number[]>
|
||||||
|
> = {
|
||||||
|
[AlertThresholdMatchType.AT_LEAST_ONCE]: {
|
||||||
|
[AlertThresholdOperator.IS_BELOW]: [60, 45, 40, 55, 35],
|
||||||
|
[AlertThresholdOperator.IS_EQUAL_TO]: [95, 100, 105, 90, 100],
|
||||||
|
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [5, 0, 10, 15, 0],
|
||||||
|
[AlertThresholdOperator.IS_ABOVE]: [75, 85, 90, 78, 95],
|
||||||
|
[AlertThresholdOperator.ABOVE_BELOW]: [75, 85, 90, 78, 95],
|
||||||
|
},
|
||||||
|
[AlertThresholdMatchType.ALL_THE_TIME]: {
|
||||||
|
[AlertThresholdOperator.IS_BELOW]: [45, 40, 35, 42, 38],
|
||||||
|
[AlertThresholdOperator.IS_EQUAL_TO]: [100, 100, 100, 100, 100],
|
||||||
|
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [5, 10, 15, 8, 12],
|
||||||
|
[AlertThresholdOperator.IS_ABOVE]: [85, 87, 90, 88, 95],
|
||||||
|
[AlertThresholdOperator.ABOVE_BELOW]: [85, 87, 90, 88, 95],
|
||||||
|
},
|
||||||
|
[AlertThresholdMatchType.ON_AVERAGE]: {
|
||||||
|
[AlertThresholdOperator.IS_BELOW]: [60, 40, 45, 35, 45],
|
||||||
|
[AlertThresholdOperator.IS_EQUAL_TO]: [95, 105, 100, 95, 105],
|
||||||
|
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [5, 10, 15, 8, 12],
|
||||||
|
[AlertThresholdOperator.IS_ABOVE]: [75, 85, 90, 78, 95],
|
||||||
|
[AlertThresholdOperator.ABOVE_BELOW]: [75, 85, 90, 78, 95],
|
||||||
|
},
|
||||||
|
[AlertThresholdMatchType.IN_TOTAL]: {
|
||||||
|
[AlertThresholdOperator.IS_BELOW]: [8, 5, 10, 12, 8],
|
||||||
|
[AlertThresholdOperator.IS_EQUAL_TO]: [20, 20, 20, 20, 20],
|
||||||
|
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [10, 15, 25, 5, 30],
|
||||||
|
[AlertThresholdOperator.IS_ABOVE]: [10, 15, 25, 5, 30],
|
||||||
|
[AlertThresholdOperator.ABOVE_BELOW]: [10, 15, 25, 5, 30],
|
||||||
|
},
|
||||||
|
[AlertThresholdMatchType.LAST]: {
|
||||||
|
[AlertThresholdOperator.IS_BELOW]: [75, 85, 90, 78, 45],
|
||||||
|
[AlertThresholdOperator.IS_EQUAL_TO]: [75, 85, 90, 78, 100],
|
||||||
|
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [75, 85, 90, 78, 25],
|
||||||
|
[AlertThresholdOperator.IS_ABOVE]: [75, 85, 90, 78, 95],
|
||||||
|
[AlertThresholdOperator.ABOVE_BELOW]: [75, 85, 90, 78, 95],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return dataPointMap[matchType]?.[op] || [75, 85, 90, 78, 95];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTooltipOperatorSymbol = (op: AlertThresholdOperator): string => {
|
||||||
|
const symbolMap: Record<AlertThresholdOperator, string> = {
|
||||||
|
[AlertThresholdOperator.IS_ABOVE]: '>',
|
||||||
|
[AlertThresholdOperator.IS_BELOW]: '<',
|
||||||
|
[AlertThresholdOperator.IS_EQUAL_TO]: '=',
|
||||||
|
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: '!=',
|
||||||
|
[AlertThresholdOperator.ABOVE_BELOW]: '>',
|
||||||
|
};
|
||||||
|
return symbolMap[op] || '>';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTooltipClick = (
|
||||||
|
e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
|
||||||
|
): void => {
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
function TooltipContent({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={handleTooltipClick}
|
||||||
|
onKeyDown={(e): void => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
handleTooltipClick(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="tooltip-content"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipExample({
|
||||||
|
children,
|
||||||
|
dataPoints,
|
||||||
|
operatorSymbol,
|
||||||
|
thresholdValue,
|
||||||
|
matchType,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
dataPoints: number[];
|
||||||
|
operatorSymbol: string;
|
||||||
|
thresholdValue: number;
|
||||||
|
matchType: AlertThresholdMatchType;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="tooltip-example">
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<br />
|
||||||
|
Say, For a 5-minute window (configured in Evaluation settings), 1 min
|
||||||
|
aggregation interval (set up in query) → 5{' '}
|
||||||
|
{matchType === AlertThresholdMatchType.IN_TOTAL
|
||||||
|
? 'error counts'
|
||||||
|
: 'data points'}
|
||||||
|
: [{dataPoints.join(', ')}]<br />
|
||||||
|
With threshold {operatorSymbol} {thresholdValue}: {children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipLink(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="tooltip-link">
|
||||||
|
<a
|
||||||
|
href="https://signoz.io/docs"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="tooltip-link-text"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMatchTypeTooltip = (
|
||||||
|
matchType: AlertThresholdMatchType,
|
||||||
|
operator: AlertThresholdOperator,
|
||||||
|
): React.ReactNode => {
|
||||||
|
const operatorSymbol = getTooltipOperatorSymbol(operator);
|
||||||
|
const operatorWord = getOperatorWord(operator);
|
||||||
|
const thresholdValue = getThresholdValue(operator);
|
||||||
|
const dataPoints = getDataPoints(matchType, operator);
|
||||||
|
const getMatchingPointsCount = (): number =>
|
||||||
|
dataPoints.filter((p) => {
|
||||||
|
switch (operator) {
|
||||||
|
case AlertThresholdOperator.IS_ABOVE:
|
||||||
|
return p > thresholdValue;
|
||||||
|
case AlertThresholdOperator.IS_BELOW:
|
||||||
|
return p < thresholdValue;
|
||||||
|
case AlertThresholdOperator.IS_EQUAL_TO:
|
||||||
|
return p === thresholdValue;
|
||||||
|
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
|
||||||
|
return p !== thresholdValue;
|
||||||
|
default:
|
||||||
|
return p > thresholdValue;
|
||||||
|
}
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
switch (matchType) {
|
||||||
|
case AlertThresholdMatchType.AT_LEAST_ONCE:
|
||||||
|
return (
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="tooltip-description">
|
||||||
|
Data is aggregated at each interval within your evaluation window,
|
||||||
|
creating multiple data points. This option triggers if <span>ANY</span> of
|
||||||
|
those aggregated data points crosses the threshold.
|
||||||
|
</div>
|
||||||
|
<TooltipExample
|
||||||
|
dataPoints={dataPoints}
|
||||||
|
operatorSymbol={operatorSymbol}
|
||||||
|
thresholdValue={thresholdValue}
|
||||||
|
matchType={matchType}
|
||||||
|
>
|
||||||
|
Alert triggers ({getMatchingPointsCount()} points {operatorWord}{' '}
|
||||||
|
{thresholdValue})
|
||||||
|
</TooltipExample>
|
||||||
|
<TooltipLink />
|
||||||
|
</TooltipContent>
|
||||||
|
);
|
||||||
|
|
||||||
|
case AlertThresholdMatchType.ALL_THE_TIME:
|
||||||
|
return (
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="tooltip-description">
|
||||||
|
Data is aggregated at each interval within your evaluation window,
|
||||||
|
creating multiple data points. This option triggers if <span>ALL</span>{' '}
|
||||||
|
aggregated data points cross the threshold.
|
||||||
|
</div>
|
||||||
|
<TooltipExample
|
||||||
|
dataPoints={dataPoints}
|
||||||
|
operatorSymbol={operatorSymbol}
|
||||||
|
thresholdValue={thresholdValue}
|
||||||
|
matchType={matchType}
|
||||||
|
>
|
||||||
|
Alert triggers (all points {operatorWord} {thresholdValue})<br />
|
||||||
|
If any point was {thresholdValue}, no alert would fire
|
||||||
|
</TooltipExample>
|
||||||
|
<TooltipLink />
|
||||||
|
</TooltipContent>
|
||||||
|
);
|
||||||
|
|
||||||
|
case AlertThresholdMatchType.ON_AVERAGE: {
|
||||||
|
const average = (
|
||||||
|
dataPoints.reduce((a, b) => a + b, 0) / dataPoints.length
|
||||||
|
).toFixed(1);
|
||||||
|
return (
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="tooltip-description">
|
||||||
|
Data is aggregated at each interval within your evaluation window,
|
||||||
|
creating multiple data points. This option triggers if the{' '}
|
||||||
|
<span>AVERAGE</span> of all aggregated data points crosses the threshold.
|
||||||
|
</div>
|
||||||
|
<TooltipExample
|
||||||
|
dataPoints={dataPoints}
|
||||||
|
operatorSymbol={operatorSymbol}
|
||||||
|
thresholdValue={thresholdValue}
|
||||||
|
matchType={matchType}
|
||||||
|
>
|
||||||
|
Alert triggers (average = {average})
|
||||||
|
</TooltipExample>
|
||||||
|
<TooltipLink />
|
||||||
|
</TooltipContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case AlertThresholdMatchType.IN_TOTAL: {
|
||||||
|
const total = dataPoints.reduce((a, b) => a + b, 0);
|
||||||
|
return (
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="tooltip-description">
|
||||||
|
Data is aggregated at each interval within your evaluation window,
|
||||||
|
creating multiple data points. This option triggers if the{' '}
|
||||||
|
<span>SUM</span> of all aggregated data points crosses the threshold.
|
||||||
|
</div>
|
||||||
|
<TooltipExample
|
||||||
|
dataPoints={dataPoints}
|
||||||
|
operatorSymbol={operatorSymbol}
|
||||||
|
thresholdValue={thresholdValue}
|
||||||
|
matchType={matchType}
|
||||||
|
>
|
||||||
|
Alert triggers (total = {total})
|
||||||
|
</TooltipExample>
|
||||||
|
<TooltipLink />
|
||||||
|
</TooltipContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
case AlertThresholdMatchType.LAST: {
|
||||||
|
const lastPoint = dataPoints[dataPoints.length - 1];
|
||||||
|
return (
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="tooltip-description">
|
||||||
|
Data is aggregated at each interval within your evaluation window,
|
||||||
|
creating multiple data points. This option triggers based on the{' '}
|
||||||
|
<span>MOST RECENT</span> aggregated data point only.
|
||||||
|
</div>
|
||||||
|
<TooltipExample
|
||||||
|
dataPoints={dataPoints}
|
||||||
|
operatorSymbol={operatorSymbol}
|
||||||
|
thresholdValue={thresholdValue}
|
||||||
|
matchType={matchType}
|
||||||
|
>
|
||||||
|
Alert triggers (last point = {lastPoint})
|
||||||
|
</TooltipExample>
|
||||||
|
<TooltipLink />
|
||||||
|
</TooltipContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NotificationChannelsNotFoundContent({
|
||||||
|
user,
|
||||||
|
refreshChannels,
|
||||||
|
}: {
|
||||||
|
user: IUser;
|
||||||
|
refreshChannels: () => void;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Flex justify="space-between">
|
||||||
|
<Flex gap={4} align="center">
|
||||||
|
<Typography.Text>No channels yet.</Typography.Text>
|
||||||
|
{user?.role === USER_ROLES.ADMIN ? (
|
||||||
|
<Typography.Text>
|
||||||
|
Create one
|
||||||
|
<Button
|
||||||
|
style={{ padding: '0 4px' }}
|
||||||
|
type="link"
|
||||||
|
onClick={(): void => {
|
||||||
|
window.open(ROUTES.CHANNELS_NEW, '_blank');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
here.
|
||||||
|
</Button>
|
||||||
|
</Typography.Text>
|
||||||
|
) : (
|
||||||
|
<Typography.Text>Please ask your admin to create one.</Typography.Text>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
<Button type="text" onClick={refreshChannels}>
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RoutingPolicyBanner({
|
||||||
|
notificationSettings,
|
||||||
|
setNotificationSettings,
|
||||||
|
}: RoutingPolicyBannerProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="routing-policies-info-banner">
|
||||||
|
<Typography.Text>
|
||||||
|
Use <strong>Routing Policies</strong> for dynamic routing
|
||||||
|
</Typography.Text>
|
||||||
|
<Switch
|
||||||
|
checked={notificationSettings.routingPolicies}
|
||||||
|
onChange={(value): void => {
|
||||||
|
setNotificationSettings({
|
||||||
|
type: 'SET_ROUTING_POLICIES',
|
||||||
|
payload: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -38,7 +38,6 @@ function CreateAlertHeader(): JSX.Element {
|
|||||||
<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"
|
||||||
@ -49,15 +48,6 @@ function CreateAlertHeader(): JSX.Element {
|
|||||||
className="alert-header__input title"
|
className="alert-header__input title"
|
||||||
placeholder="Enter alert rule name"
|
placeholder="Enter alert rule name"
|
||||||
/>
|
/>
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={alertState.description}
|
|
||||||
onChange={(e): void =>
|
|
||||||
setAlertState({ type: 'SET_ALERT_DESCRIPTION', payload: e.target.value })
|
|
||||||
}
|
|
||||||
className="alert-header__input description"
|
|
||||||
placeholder="Click to add description..."
|
|
||||||
/>
|
|
||||||
<LabelsInput
|
<LabelsInput
|
||||||
labels={alertState.labels}
|
labels={alertState.labels}
|
||||||
onLabelsChange={(labels: Labels): void =>
|
onLabelsChange={(labels: Labels): void =>
|
||||||
|
|||||||
@ -1,9 +1,21 @@
|
|||||||
/* 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 { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
|
|
||||||
|
import * as useCreateAlertRuleHook from '../../../../hooks/alerts/useCreateAlertRule';
|
||||||
|
import * as useTestAlertRuleHook from '../../../../hooks/alerts/useTestAlertRule';
|
||||||
import { CreateAlertProvider } from '../../context';
|
import { CreateAlertProvider } from '../../context';
|
||||||
import CreateAlertHeader from '../CreateAlertHeader';
|
import CreateAlertHeader from '../CreateAlertHeader';
|
||||||
|
|
||||||
|
jest.spyOn(useCreateAlertRuleHook, 'useCreateAlertRule').mockReturnValue({
|
||||||
|
mutate: jest.fn(),
|
||||||
|
isLoading: false,
|
||||||
|
} as any);
|
||||||
|
jest.spyOn(useTestAlertRuleHook, 'useTestAlertRule').mockReturnValue({
|
||||||
|
mutate: jest.fn(),
|
||||||
|
isLoading: false,
|
||||||
|
} as any);
|
||||||
|
|
||||||
jest.mock('uplot', () => {
|
jest.mock('uplot', () => {
|
||||||
const paths = {
|
const paths = {
|
||||||
spline: jest.fn(),
|
spline: jest.fn(),
|
||||||
@ -27,7 +39,7 @@ jest.mock('react-router-dom', () => ({
|
|||||||
|
|
||||||
const renderCreateAlertHeader = (): ReturnType<typeof render> =>
|
const renderCreateAlertHeader = (): ReturnType<typeof render> =>
|
||||||
render(
|
render(
|
||||||
<CreateAlertProvider>
|
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||||
<CreateAlertHeader />
|
<CreateAlertHeader />
|
||||||
</CreateAlertProvider>,
|
</CreateAlertProvider>,
|
||||||
);
|
);
|
||||||
@ -44,14 +56,6 @@ describe('CreateAlertHeader', () => {
|
|||||||
expect(nameInput).toBeInTheDocument();
|
expect(nameInput).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders description input with placeholder', () => {
|
|
||||||
renderCreateAlertHeader();
|
|
||||||
const descriptionInput = screen.getByPlaceholderText(
|
|
||||||
'Click to add description...',
|
|
||||||
);
|
|
||||||
expect(descriptionInput).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders LabelsInput component', () => {
|
it('renders LabelsInput component', () => {
|
||||||
renderCreateAlertHeader();
|
renderCreateAlertHeader();
|
||||||
expect(screen.getByText('+ Add labels')).toBeInTheDocument();
|
expect(screen.getByText('+ Add labels')).toBeInTheDocument();
|
||||||
@ -65,13 +69,4 @@ describe('CreateAlertHeader', () => {
|
|||||||
|
|
||||||
expect(nameInput).toHaveValue('Test Alert');
|
expect(nameInput).toHaveValue('Test Alert');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates description when typing in description input', () => {
|
|
||||||
renderCreateAlertHeader();
|
|
||||||
const descriptionInput = screen.getByPlaceholderText(
|
|
||||||
'Click to add description...',
|
|
||||||
);
|
|
||||||
fireEvent.change(descriptionInput, { target: { value: 'Test Description' } });
|
|
||||||
expect(descriptionInput).toHaveValue('Test Description');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,21 +3,6 @@
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
color: var(--text-vanilla-100);
|
color: var(--text-vanilla-100);
|
||||||
|
|
||||||
/* Top bar with diagonal stripes */
|
|
||||||
&__tab-bar {
|
|
||||||
height: 32px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background: repeating-linear-gradient(
|
|
||||||
-45deg,
|
|
||||||
#0f0f0f,
|
|
||||||
#0f0f0f 10px,
|
|
||||||
#101010 10px,
|
|
||||||
#101010 20px
|
|
||||||
);
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tab block visuals */
|
/* Tab block visuals */
|
||||||
&__tab {
|
&__tab {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -44,6 +29,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
min-width: 300px;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__input.title {
|
&__input.title {
|
||||||
@ -51,6 +38,8 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: var(--text-vanilla-100);
|
color: var(--text-vanilla-100);
|
||||||
|
width: 100%;
|
||||||
|
min-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__input:focus,
|
&__input:focus,
|
||||||
@ -64,6 +53,15 @@
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: var(--text-vanilla-300);
|
color: var(--text-vanilla-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-vanilla-100);
|
||||||
|
border: 1px solid var(--bg-slate-300);
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.labels-input {
|
.labels-input {
|
||||||
@ -149,3 +147,65 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.alert-header {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
color: var(--text-ink-100);
|
||||||
|
|
||||||
|
&__tab {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
color: var(--text-ink-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tab::before {
|
||||||
|
color: var(--bg-ink-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input.title {
|
||||||
|
color: var(--text-ink-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input.description {
|
||||||
|
color: var(--text-ink-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels-input {
|
||||||
|
&__add-button {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-ink-300);
|
||||||
|
color: var(--bg-ink-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label-pill {
|
||||||
|
background-color: #ad7f581a;
|
||||||
|
color: var(--bg-sienna-400);
|
||||||
|
border: 1px solid var(--bg-sienna-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__remove-button {
|
||||||
|
color: var(--bg-sienna-400);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-ink-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
color: var(--bg-ink-500);
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,17 +1,20 @@
|
|||||||
$top-nav-background-1: #0f0f0f;
|
|
||||||
$top-nav-background-2: #101010;
|
|
||||||
|
|
||||||
.create-alert-v2-container {
|
.create-alert-v2-container {
|
||||||
background-color: var(--bg-ink-500);
|
background-color: var(--bg-ink-500);
|
||||||
|
padding-bottom: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-nav-container {
|
.lightMode {
|
||||||
background: repeating-linear-gradient(
|
.create-alert-v2-container {
|
||||||
-45deg,
|
background-color: var(--bg-vanilla-100);
|
||||||
$top-nav-background-1,
|
}
|
||||||
$top-nav-background-1 10px,
|
}
|
||||||
$top-nav-background-2 10px,
|
|
||||||
$top-nav-background-2 20px
|
.sticky-page-spinner {
|
||||||
);
|
position: fixed;
|
||||||
margin-bottom: 0;
|
inset: 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
z-index: 10000;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,27 +2,32 @@ import './CreateAlertV2.styles.scss';
|
|||||||
|
|
||||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||||
|
|
||||||
import AlertCondition from './AlertCondition';
|
import AlertCondition from './AlertCondition';
|
||||||
import { CreateAlertProvider } from './context';
|
import { CreateAlertProvider } from './context';
|
||||||
|
import { buildInitialAlertDef } from './context/utils';
|
||||||
import CreateAlertHeader from './CreateAlertHeader';
|
import CreateAlertHeader from './CreateAlertHeader';
|
||||||
import EvaluationSettings from './EvaluationSettings';
|
import EvaluationSettings from './EvaluationSettings';
|
||||||
|
import Footer from './Footer';
|
||||||
import NotificationSettings from './NotificationSettings';
|
import NotificationSettings from './NotificationSettings';
|
||||||
import QuerySection from './QuerySection';
|
import QuerySection from './QuerySection';
|
||||||
import { showCondensedLayout } from './utils';
|
import { CreateAlertV2Props } from './types';
|
||||||
|
import { showCondensedLayout, Spinner } from './utils';
|
||||||
|
|
||||||
function CreateAlertV2({
|
function CreateAlertV2({ alertType }: CreateAlertV2Props): JSX.Element {
|
||||||
initialQuery = initialQueriesMap.metrics,
|
const queryToRedirect = buildInitialAlertDef(alertType);
|
||||||
}: {
|
const currentQueryToRedirect = mapQueryDataFromApi(
|
||||||
initialQuery?: Query;
|
queryToRedirect.condition.compositeQuery,
|
||||||
}): JSX.Element {
|
);
|
||||||
useShareBuilderUrl({ defaultValue: initialQuery });
|
|
||||||
|
useShareBuilderUrl({ defaultValue: currentQueryToRedirect });
|
||||||
|
|
||||||
const showCondensedLayoutFlag = showCondensedLayout();
|
const showCondensedLayoutFlag = showCondensedLayout();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CreateAlertProvider>
|
<CreateAlertProvider initialAlertType={alertType}>
|
||||||
|
<Spinner />
|
||||||
<div className="create-alert-v2-container">
|
<div className="create-alert-v2-container">
|
||||||
<CreateAlertHeader />
|
<CreateAlertHeader />
|
||||||
<QuerySection />
|
<QuerySection />
|
||||||
@ -30,6 +35,7 @@ function CreateAlertV2({
|
|||||||
{!showCondensedLayoutFlag ? <EvaluationSettings /> : null}
|
{!showCondensedLayoutFlag ? <EvaluationSettings /> : null}
|
||||||
<NotificationSettings />
|
<NotificationSettings />
|
||||||
</div>
|
</div>
|
||||||
|
<Footer />
|
||||||
</CreateAlertProvider>
|
</CreateAlertProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -114,6 +114,14 @@
|
|||||||
height: 32px;
|
height: 32px;
|
||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
.ant-select-selection-placeholder {
|
.ant-select-selection-placeholder {
|
||||||
font-family: 'Space Mono';
|
font-family: 'Space Mono';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { Collapse, Input, Select, Typography } from 'antd';
|
import { Collapse, Input, Typography } from 'antd';
|
||||||
import { Y_AXIS_CATEGORIES } from 'components/YAxisUnitSelector/constants';
|
|
||||||
|
|
||||||
import { useCreateAlertState } from '../context';
|
import { useCreateAlertState } from '../context';
|
||||||
import AdvancedOptionItem from './AdvancedOptionItem';
|
import AdvancedOptionItem from './AdvancedOptionItem';
|
||||||
@ -8,10 +7,6 @@ import EvaluationCadence from './EvaluationCadence';
|
|||||||
function AdvancedOptions(): JSX.Element {
|
function AdvancedOptions(): JSX.Element {
|
||||||
const { advancedOptions, setAdvancedOptions } = useCreateAlertState();
|
const { advancedOptions, setAdvancedOptions } = useCreateAlertState();
|
||||||
|
|
||||||
const timeOptions = Y_AXIS_CATEGORIES.find(
|
|
||||||
(category) => category.name === 'Time',
|
|
||||||
)?.units.map((unit) => ({ label: unit.name, value: unit.id }));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="advanced-options-container">
|
<div className="advanced-options-container">
|
||||||
<Collapse bordered={false}>
|
<Collapse bordered={false}>
|
||||||
@ -38,24 +33,15 @@ function AdvancedOptions(): JSX.Element {
|
|||||||
}
|
}
|
||||||
value={advancedOptions.sendNotificationIfDataIsMissing.toleranceLimit}
|
value={advancedOptions.sendNotificationIfDataIsMissing.toleranceLimit}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Typography.Text>Minutes</Typography.Text>
|
||||||
style={{ width: 120 }}
|
|
||||||
options={timeOptions}
|
|
||||||
placeholder="Select time unit"
|
|
||||||
onChange={(value): void =>
|
|
||||||
setAdvancedOptions({
|
|
||||||
type: 'SET_SEND_NOTIFICATION_IF_DATA_IS_MISSING',
|
|
||||||
payload: {
|
|
||||||
toleranceLimit:
|
|
||||||
advancedOptions.sendNotificationIfDataIsMissing.toleranceLimit,
|
|
||||||
timeUnit: value as string,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
value={advancedOptions.sendNotificationIfDataIsMissing.timeUnit}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
onToggle={(): void =>
|
||||||
|
setAdvancedOptions({
|
||||||
|
type: 'TOGGLE_SEND_NOTIFICATION_IF_DATA_IS_MISSING',
|
||||||
|
payload: !advancedOptions.sendNotificationIfDataIsMissing.enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<AdvancedOptionItem
|
<AdvancedOptionItem
|
||||||
title="Minimum data required"
|
title="Minimum data required"
|
||||||
@ -80,8 +66,15 @@ function AdvancedOptions(): JSX.Element {
|
|||||||
<Typography.Text>Datapoints</Typography.Text>
|
<Typography.Text>Datapoints</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
onToggle={(): void =>
|
||||||
|
setAdvancedOptions({
|
||||||
|
type: 'TOGGLE_ENFORCE_MINIMUM_DATAPOINTS',
|
||||||
|
payload: !advancedOptions.enforceMinimumDatapoints.enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<AdvancedOptionItem
|
{/* TODO: Add back when the functionality is implemented */}
|
||||||
|
{/* <AdvancedOptionItem
|
||||||
title="Account for data delay"
|
title="Account for data delay"
|
||||||
description="Shift the evaluation window backwards to account for data processing delays."
|
description="Shift the evaluation window backwards to account for data processing delays."
|
||||||
tooltipText="Use when your data takes time to arrive on the platform. For example, if logs typically arrive 5 minutes late, set a 5-minute delay so the alert checks the correct time window."
|
tooltipText="Use when your data takes time to arrive on the platform. For example, if logs typically arrive 5 minutes late, set a 5-minute delay so the alert checks the correct time window."
|
||||||
@ -119,7 +112,7 @@ function AdvancedOptions(): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/> */}
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import './styles.scss';
|
import './styles.scss';
|
||||||
import '../AdvancedOptionItem/styles.scss';
|
import '../AdvancedOptionItem/styles.scss';
|
||||||
|
|
||||||
import { Button, Input, Select, Tooltip, Typography } from 'antd';
|
import { Input, Select, Tooltip, Typography } from 'antd';
|
||||||
import { Info, Plus } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useCreateAlertState } from '../../context';
|
import { useCreateAlertState } from '../../context';
|
||||||
@ -36,10 +36,10 @@ function EvaluationCadence(): JSX.Element {
|
|||||||
);
|
);
|
||||||
}, [advancedOptions.evaluationCadence.mode]);
|
}, [advancedOptions.evaluationCadence.mode]);
|
||||||
|
|
||||||
const showCustomSchedule = (): void => {
|
// const showCustomSchedule = (): void => {
|
||||||
setIsEvaluationCadenceDetailsVisible(true);
|
// setIsEvaluationCadenceDetailsVisible(true);
|
||||||
setIsCustomScheduleButtonVisible(false);
|
// setIsCustomScheduleButtonVisible(false);
|
||||||
};
|
// };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="evaluation-cadence-container">
|
<div className="evaluation-cadence-container">
|
||||||
@ -98,13 +98,14 @@ function EvaluationCadence(): JSX.Element {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Input.Group>
|
</Input.Group>
|
||||||
<Button
|
{/* TODO: Add custom schedule back once the functionality is implemented */}
|
||||||
|
{/* <Button
|
||||||
className="advanced-option-item-button"
|
className="advanced-option-item-button"
|
||||||
onClick={showCustomSchedule}
|
onClick={showCustomSchedule}
|
||||||
>
|
>
|
||||||
<Plus size={12} />
|
<Plus size={12} />
|
||||||
<Typography.Text>Add custom schedule</Typography.Text>
|
<Typography.Text>Add custom schedule</Typography.Text>
|
||||||
</Button>
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -164,6 +164,14 @@
|
|||||||
background-color: var(--bg-ink-300);
|
background-color: var(--bg-ink-300);
|
||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
color: var(--bg-vanilla-100);
|
color: var(--bg-vanilla-100);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -529,6 +537,15 @@
|
|||||||
background-color: var(--bg-vanilla-300);
|
background-color: var(--bg-vanilla-300);
|
||||||
border: 1px solid var(--bg-vanilla-300);
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
color: var(--bg-ink-400);
|
color: var(--bg-ink-400);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--bg-ink-300);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import { useMemo } from 'react';
|
|||||||
|
|
||||||
import { ADVANCED_OPTIONS_TIME_UNIT_OPTIONS } from '../../context/constants';
|
import { ADVANCED_OPTIONS_TIME_UNIT_OPTIONS } from '../../context/constants';
|
||||||
import {
|
import {
|
||||||
CUMULATIVE_WINDOW_DESCRIPTION,
|
getCumulativeWindowDescription,
|
||||||
ROLLING_WINDOW_DESCRIPTION,
|
getRollingWindowDescription,
|
||||||
TIMEZONE_DATA,
|
TIMEZONE_DATA,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
import TimeInput from '../TimeInput';
|
import TimeInput from '../TimeInput';
|
||||||
@ -116,7 +116,9 @@ function EvaluationWindowDetails({
|
|||||||
if (isCurrentHour) {
|
if (isCurrentHour) {
|
||||||
return (
|
return (
|
||||||
<div className="evaluation-window-details">
|
<div className="evaluation-window-details">
|
||||||
<Typography.Text>{CUMULATIVE_WINDOW_DESCRIPTION}</Typography.Text>
|
<Typography.Text>
|
||||||
|
{getCumulativeWindowDescription(evaluationWindow.timeframe)}
|
||||||
|
</Typography.Text>
|
||||||
<Typography.Text>{displayText}</Typography.Text>
|
<Typography.Text>{displayText}</Typography.Text>
|
||||||
<div className="select-group">
|
<div className="select-group">
|
||||||
<Typography.Text>STARTING AT MINUTE</Typography.Text>
|
<Typography.Text>STARTING AT MINUTE</Typography.Text>
|
||||||
@ -134,7 +136,9 @@ function EvaluationWindowDetails({
|
|||||||
if (isCurrentDay) {
|
if (isCurrentDay) {
|
||||||
return (
|
return (
|
||||||
<div className="evaluation-window-details">
|
<div className="evaluation-window-details">
|
||||||
<Typography.Text>{CUMULATIVE_WINDOW_DESCRIPTION}</Typography.Text>
|
<Typography.Text>
|
||||||
|
{getCumulativeWindowDescription(evaluationWindow.timeframe)}
|
||||||
|
</Typography.Text>
|
||||||
<Typography.Text>{displayText}</Typography.Text>
|
<Typography.Text>{displayText}</Typography.Text>
|
||||||
<div className="select-group time-select-group">
|
<div className="select-group time-select-group">
|
||||||
<Typography.Text>STARTING AT</Typography.Text>
|
<Typography.Text>STARTING AT</Typography.Text>
|
||||||
@ -159,7 +163,9 @@ function EvaluationWindowDetails({
|
|||||||
if (isCurrentMonth) {
|
if (isCurrentMonth) {
|
||||||
return (
|
return (
|
||||||
<div className="evaluation-window-details">
|
<div className="evaluation-window-details">
|
||||||
<Typography.Text>{CUMULATIVE_WINDOW_DESCRIPTION}</Typography.Text>
|
<Typography.Text>
|
||||||
|
{getCumulativeWindowDescription(evaluationWindow.timeframe)}
|
||||||
|
</Typography.Text>
|
||||||
<Typography.Text>{displayText}</Typography.Text>
|
<Typography.Text>{displayText}</Typography.Text>
|
||||||
<div className="select-group">
|
<div className="select-group">
|
||||||
<Typography.Text>STARTING ON DAY</Typography.Text>
|
<Typography.Text>STARTING ON DAY</Typography.Text>
|
||||||
@ -192,7 +198,9 @@ function EvaluationWindowDetails({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="evaluation-window-details">
|
<div className="evaluation-window-details">
|
||||||
<Typography.Text>{ROLLING_WINDOW_DESCRIPTION}</Typography.Text>
|
<Typography.Text>
|
||||||
|
{getRollingWindowDescription(evaluationWindow.timeframe)}
|
||||||
|
</Typography.Text>
|
||||||
<Typography.Text>Specify custom duration</Typography.Text>
|
<Typography.Text>Specify custom duration</Typography.Text>
|
||||||
<Typography.Text>{displayText}</Typography.Text>
|
<Typography.Text>{displayText}</Typography.Text>
|
||||||
<div className="select-group">
|
<div className="select-group">
|
||||||
|
|||||||
@ -3,10 +3,10 @@ import classNames from 'classnames';
|
|||||||
import { Check } from 'lucide-react';
|
import { Check } from 'lucide-react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CUMULATIVE_WINDOW_DESCRIPTION,
|
|
||||||
EVALUATION_WINDOW_TIMEFRAME,
|
EVALUATION_WINDOW_TIMEFRAME,
|
||||||
EVALUATION_WINDOW_TYPE,
|
EVALUATION_WINDOW_TYPE,
|
||||||
ROLLING_WINDOW_DESCRIPTION,
|
getCumulativeWindowDescription,
|
||||||
|
getRollingWindowDescription,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
import {
|
import {
|
||||||
CumulativeWindowTimeframes,
|
CumulativeWindowTimeframes,
|
||||||
@ -96,7 +96,9 @@ function EvaluationWindowPopover({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="selection-content">
|
<div className="selection-content">
|
||||||
<Typography.Text>{ROLLING_WINDOW_DESCRIPTION}</Typography.Text>
|
<Typography.Text>
|
||||||
|
{getRollingWindowDescription(evaluationWindow.timeframe)}
|
||||||
|
</Typography.Text>
|
||||||
<Button type="link">Read the docs</Button>
|
<Button type="link">Read the docs</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -108,7 +110,9 @@ function EvaluationWindowPopover({
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div className="selection-content">
|
<div className="selection-content">
|
||||||
<Typography.Text>{CUMULATIVE_WINDOW_DESCRIPTION}</Typography.Text>
|
<Typography.Text>
|
||||||
|
{getCumulativeWindowDescription(evaluationWindow.timeframe)}
|
||||||
|
</Typography.Text>
|
||||||
<Button type="link">Read the docs</Button>
|
<Button type="link">Read the docs</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -28,9 +28,10 @@ describe('AdvancedOptions', () => {
|
|||||||
expect(
|
expect(
|
||||||
screen.queryByText(MINIMUM_DATA_REQUIRED_TEXT),
|
screen.queryByText(MINIMUM_DATA_REQUIRED_TEXT),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
expect(
|
// TODO: Uncomment this when account for data delay is implemented
|
||||||
screen.queryByText(ACCOUNT_FOR_DATA_DELAY_TEXT),
|
// expect(
|
||||||
).not.toBeInTheDocument();
|
// screen.queryByText(ACCOUNT_FOR_DATA_DELAY_TEXT),
|
||||||
|
// ).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be able to expand the advanced options', () => {
|
it('should be able to expand the advanced options', () => {
|
||||||
@ -42,9 +43,10 @@ describe('AdvancedOptions', () => {
|
|||||||
expect(
|
expect(
|
||||||
screen.queryByText(MINIMUM_DATA_REQUIRED_TEXT),
|
screen.queryByText(MINIMUM_DATA_REQUIRED_TEXT),
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
expect(
|
// TODO: Uncomment this when account for data delay is implemented
|
||||||
screen.queryByText(ACCOUNT_FOR_DATA_DELAY_TEXT),
|
// expect(
|
||||||
).not.toBeInTheDocument();
|
// screen.queryByText(ACCOUNT_FOR_DATA_DELAY_TEXT),
|
||||||
|
// ).not.toBeInTheDocument();
|
||||||
|
|
||||||
const collapse = screen.getByRole('button', { name: /ADVANCED OPTIONS/i });
|
const collapse = screen.getByRole('button', { name: /ADVANCED OPTIONS/i });
|
||||||
fireEvent.click(collapse);
|
fireEvent.click(collapse);
|
||||||
@ -52,7 +54,8 @@ describe('AdvancedOptions', () => {
|
|||||||
expect(screen.getByText('How often to check')).toBeInTheDocument();
|
expect(screen.getByText('How often to check')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Alert when data stops coming')).toBeInTheDocument();
|
expect(screen.getByText('Alert when data stops coming')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Minimum data required')).toBeInTheDocument();
|
expect(screen.getByText('Minimum data required')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Account for data delay')).toBeInTheDocument();
|
// TODO: Uncomment this when account for data delay is implemented
|
||||||
|
// expect(screen.getByText('Account for data delay')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('"Alert when data stops coming" works as expected', () => {
|
it('"Alert when data stops coming" works as expected', () => {
|
||||||
@ -112,7 +115,7 @@ describe('AdvancedOptions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('"Account for data delay" works as expected', () => {
|
it.skip('"Account for data delay" works as expected', () => {
|
||||||
render(<AdvancedOptions />);
|
render(<AdvancedOptions />);
|
||||||
|
|
||||||
const collapse = screen.getByRole('button', { name: /ADVANCED OPTIONS/i });
|
const collapse = screen.getByRole('button', { name: /ADVANCED OPTIONS/i });
|
||||||
|
|||||||
@ -64,10 +64,12 @@ describe('EvaluationCadence', () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByPlaceholderText('Enter time')).toHaveValue(1);
|
expect(screen.getByPlaceholderText('Enter time')).toHaveValue(1);
|
||||||
expect(screen.getByText('Minutes')).toBeInTheDocument();
|
expect(screen.getByText('Minutes')).toBeInTheDocument();
|
||||||
expect(screen.getByText(ADD_CUSTOM_SCHEDULE_TEXT)).toBeInTheDocument();
|
// TODO: Uncomment this when add custom schedule button is implemented
|
||||||
|
// expect(screen.getByText(ADD_CUSTOM_SCHEDULE_TEXT)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should hide the input group when add custom schedule button is clicked', () => {
|
// TODO: Unskip this when add custom schedule button is implemented
|
||||||
|
it.skip('should hide the input group when add custom schedule button is clicked', () => {
|
||||||
render(<EvaluationCadence />);
|
render(<EvaluationCadence />);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@ -84,12 +86,14 @@ describe('EvaluationCadence', () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show the edit custom schedule component in default mode', () => {
|
// TODO: Unskip this when add custom schedule button is implemented
|
||||||
|
it.skip('should not show the edit custom schedule component in default mode', () => {
|
||||||
render(<EvaluationCadence />);
|
render(<EvaluationCadence />);
|
||||||
expect(screen.queryByTestId('edit-custom-schedule')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('edit-custom-schedule')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show the custom schedule text when the mode is custom with selected values', () => {
|
// TODO: Unskip this when add custom schedule button is implemented
|
||||||
|
it.skip('should show the custom schedule text when the mode is custom with selected values', () => {
|
||||||
jest.spyOn(alertState, 'useCreateAlertState').mockReturnValueOnce(
|
jest.spyOn(alertState, 'useCreateAlertState').mockReturnValueOnce(
|
||||||
createMockAlertContextState({
|
createMockAlertContextState({
|
||||||
advancedOptions: {
|
advancedOptions: {
|
||||||
@ -118,7 +122,8 @@ describe('EvaluationCadence', () => {
|
|||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show evaluation cadence details component when clicked on add custom schedule button', () => {
|
// TODO: Unskip this when add custom schedule button is implemented
|
||||||
|
it.skip('should show evaluation cadence details component when clicked on add custom schedule button', () => {
|
||||||
render(<EvaluationCadence />);
|
render(<EvaluationCadence />);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@ -24,9 +24,12 @@ describe('EvaluationWindowDetails', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(
|
screen.getAllByText(
|
||||||
'A Rolling Window has a fixed size and shifts its starting point over time based on when the rules are evaluated.',
|
(_, element) =>
|
||||||
),
|
element?.textContent?.includes(
|
||||||
|
'Monitors data over a fixed time period that moves forward continuously',
|
||||||
|
) ?? false,
|
||||||
|
)[0],
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByText('Specify custom duration')).toBeInTheDocument();
|
expect(screen.getByText('Specify custom duration')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Last 5 Minutes')).toBeInTheDocument();
|
expect(screen.getByText('Last 5 Minutes')).toBeInTheDocument();
|
||||||
|
|||||||
@ -125,9 +125,12 @@ describe('EvaluationWindowPopover', () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(
|
screen.getAllByText(
|
||||||
'A Rolling Window has a fixed size and shifts its starting point over time based on when the rules are evaluated.',
|
(_, element) =>
|
||||||
),
|
element?.textContent?.includes(
|
||||||
|
'Monitors data over a fixed time period that moves forward continuously',
|
||||||
|
) ?? false,
|
||||||
|
)[0],
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(
|
expect(
|
||||||
screen.queryByTestId(EVALUATION_WINDOW_DETAILS_TEST_ID),
|
screen.queryByTestId(EVALUATION_WINDOW_DETAILS_TEST_ID),
|
||||||
|
|||||||
@ -26,6 +26,11 @@ export const createMockAlertContextState = (
|
|||||||
setEvaluationWindow: jest.fn(),
|
setEvaluationWindow: jest.fn(),
|
||||||
notificationSettings: INITIAL_NOTIFICATION_SETTINGS_STATE,
|
notificationSettings: INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||||
setNotificationSettings: jest.fn(),
|
setNotificationSettings: jest.fn(),
|
||||||
|
discardAlertRule: jest.fn(),
|
||||||
|
testAlertRule: jest.fn(),
|
||||||
|
isCreatingAlertRule: false,
|
||||||
|
isTestingAlertRule: false,
|
||||||
|
createAlertRule: jest.fn(),
|
||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -62,8 +62,87 @@ export const TIMEZONE_DATA = generateTimezoneData().map((timezone) => ({
|
|||||||
value: timezone.value,
|
value: timezone.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const CUMULATIVE_WINDOW_DESCRIPTION =
|
export const getCumulativeWindowDescription = (timeframe?: string): string => {
|
||||||
'A Cumulative Window has a fixed starting point and expands over time.';
|
let example = '';
|
||||||
|
switch (timeframe) {
|
||||||
|
case 'currentHour':
|
||||||
|
example =
|
||||||
|
'An hourly cumulative window for error count alerts when errors exceed 100. Starting at the top of the hour, it tracks: 20 errors by :15, 55 by :30, 105 by :45 (alert fires).';
|
||||||
|
break;
|
||||||
|
case 'currentDay':
|
||||||
|
example =
|
||||||
|
'A daily cumulative window for sales alerts when total revenue exceeds $10,000. Starting at midnight, it tracks: $2,000 by 9 AM, $5,500 by noon, $11,000 by 3 PM (alert fires).';
|
||||||
|
break;
|
||||||
|
case 'currentMonth':
|
||||||
|
example =
|
||||||
|
'A monthly cumulative window for expense alerts when spending exceeds $50,000. Starting on the 1st, it tracks: $15,000 by the 7th, $32,000 by the 15th, $51,000 by the 22nd (alert fires).';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
example = '';
|
||||||
|
}
|
||||||
|
return `Monitors data accumulated since a fixed starting point. The window grows over time, keeping all historical data from the start.\n\nExample: ${example}`;
|
||||||
|
};
|
||||||
|
|
||||||
export const ROLLING_WINDOW_DESCRIPTION =
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
'A Rolling Window has a fixed size and shifts its starting point over time based on when the rules are evaluated.';
|
export const getRollingWindowDescription = (duration?: string): string => {
|
||||||
|
let timeWindow = '5-minute';
|
||||||
|
let examples = '14:01:00-14:06:00, 14:02:00-14:07:00';
|
||||||
|
|
||||||
|
if (duration) {
|
||||||
|
const match = duration.match(/^(\d+)([mhs])/);
|
||||||
|
if (match) {
|
||||||
|
const value = parseInt(match[1], 10);
|
||||||
|
const unit = match[2];
|
||||||
|
|
||||||
|
if (unit === 'm' && !Number.isNaN(value)) {
|
||||||
|
timeWindow = `${value}-minute`;
|
||||||
|
const endMinutes1 = 1 + value;
|
||||||
|
const endMinutes2 = 2 + value;
|
||||||
|
examples = `14:01:00-14:${String(endMinutes1).padStart(
|
||||||
|
2,
|
||||||
|
'0',
|
||||||
|
)}:00, 14:02:00-14:${String(endMinutes2).padStart(2, '0')}:00`;
|
||||||
|
} else if (unit === 'h' && !Number.isNaN(value)) {
|
||||||
|
timeWindow = `${value}-hour`;
|
||||||
|
const endHour1 = 14 + value;
|
||||||
|
const endHour2 = 14 + value;
|
||||||
|
examples = `14:00:00-${String(endHour1).padStart(
|
||||||
|
2,
|
||||||
|
'0',
|
||||||
|
)}:00:00, 14:01:00-${String(endHour2).padStart(2, '0')}:01:00`;
|
||||||
|
} else if (unit === 's' && !Number.isNaN(value)) {
|
||||||
|
timeWindow = `${value}-second`;
|
||||||
|
examples = `14:01:00-14:01:${String(value).padStart(
|
||||||
|
2,
|
||||||
|
'0',
|
||||||
|
)}, 14:01:01-14:01:${String(1 + value).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
} else if (duration === 'custom') {
|
||||||
|
timeWindow = '5-minute';
|
||||||
|
examples = '14:01:00-14:06:00, 14:02:00-14:07:00';
|
||||||
|
} else if (duration.includes('h')) {
|
||||||
|
const hours = parseInt(duration, 10);
|
||||||
|
if (!Number.isNaN(hours)) {
|
||||||
|
timeWindow = `${hours}-hour`;
|
||||||
|
const endHour = 14 + hours;
|
||||||
|
examples = `14:00:00-${String(endHour).padStart(
|
||||||
|
2,
|
||||||
|
'0',
|
||||||
|
)}:00:00, 14:01:00-${String(endHour).padStart(2, '0')}:01:00`;
|
||||||
|
}
|
||||||
|
} else if (duration.includes('m')) {
|
||||||
|
const minutes = parseInt(duration, 10);
|
||||||
|
if (!Number.isNaN(minutes)) {
|
||||||
|
timeWindow = `${minutes}-minute`;
|
||||||
|
const endMinutes1 = 1 + minutes;
|
||||||
|
const endMinutes2 = 2 + minutes;
|
||||||
|
examples = `14:01:00-14:${String(endMinutes1).padStart(
|
||||||
|
2,
|
||||||
|
'0',
|
||||||
|
)}:00, 14:02:00-14:${String(endMinutes2).padStart(2, '0')}:00`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Monitors data over a fixed time period that moves forward continuously.\n\nExample: A ${timeWindow} rolling window for error rate alerts with 1 minute evaluation cadence. Unlike fixed windows, this checks continuously: ${examples}, etc.`;
|
||||||
|
};
|
||||||
|
|||||||
@ -209,6 +209,16 @@
|
|||||||
|
|
||||||
.ant-select {
|
.ant-select {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
|
|
||||||
|
.ant-select-selector {
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -231,6 +241,14 @@
|
|||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
color: var(--bg-vanilla-100);
|
color: var(--bg-vanilla-100);
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -379,6 +397,14 @@
|
|||||||
background-color: var(--bg-vanilla-300);
|
background-color: var(--bg-vanilla-300);
|
||||||
border: 1px solid var(--bg-vanilla-300);
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
color: var(--bg-ink-400);
|
color: var(--bg-ink-400);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
169
frontend/src/container/CreateAlertV2/Footer/Footer.tsx
Normal file
169
frontend/src/container/CreateAlertV2/Footer/Footer.tsx
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import './styles.scss';
|
||||||
|
|
||||||
|
import { toast } from '@signozhq/sonner';
|
||||||
|
import { Button, Tooltip, Typography } from 'antd';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||||
|
import { Check, Send, X } from 'lucide-react';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { useCreateAlertState } from '../context';
|
||||||
|
import {
|
||||||
|
buildCreateThresholdAlertRulePayload,
|
||||||
|
validateCreateAlertState,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
function Footer(): JSX.Element {
|
||||||
|
const {
|
||||||
|
alertType,
|
||||||
|
alertState: basicAlertState,
|
||||||
|
thresholdState,
|
||||||
|
advancedOptions,
|
||||||
|
evaluationWindow,
|
||||||
|
notificationSettings,
|
||||||
|
discardAlertRule,
|
||||||
|
createAlertRule,
|
||||||
|
isCreatingAlertRule,
|
||||||
|
testAlertRule,
|
||||||
|
isTestingAlertRule,
|
||||||
|
} = useCreateAlertState();
|
||||||
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
const { safeNavigate } = useSafeNavigate();
|
||||||
|
|
||||||
|
const handleDiscard = (): void => discardAlertRule();
|
||||||
|
|
||||||
|
const alertValidationMessage = useMemo(
|
||||||
|
() =>
|
||||||
|
validateCreateAlertState({
|
||||||
|
alertType,
|
||||||
|
basicAlertState,
|
||||||
|
thresholdState,
|
||||||
|
advancedOptions,
|
||||||
|
evaluationWindow,
|
||||||
|
notificationSettings,
|
||||||
|
query: currentQuery,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
alertType,
|
||||||
|
basicAlertState,
|
||||||
|
thresholdState,
|
||||||
|
advancedOptions,
|
||||||
|
evaluationWindow,
|
||||||
|
notificationSettings,
|
||||||
|
currentQuery,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTestNotification = useCallback((): void => {
|
||||||
|
const payload = buildCreateThresholdAlertRulePayload({
|
||||||
|
alertType,
|
||||||
|
basicAlertState,
|
||||||
|
thresholdState,
|
||||||
|
advancedOptions,
|
||||||
|
evaluationWindow,
|
||||||
|
notificationSettings,
|
||||||
|
query: currentQuery,
|
||||||
|
});
|
||||||
|
testAlertRule(payload, {
|
||||||
|
onSuccess: (response) => {
|
||||||
|
if (response.payload?.data?.alertCount === 0) {
|
||||||
|
toast.error(
|
||||||
|
'No alerts found during the evaluation. This happens when rule condition is unsatisfied. You may adjust the rule threshold and retry.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.success('Test notification sent successfully');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
alertType,
|
||||||
|
basicAlertState,
|
||||||
|
thresholdState,
|
||||||
|
advancedOptions,
|
||||||
|
evaluationWindow,
|
||||||
|
notificationSettings,
|
||||||
|
currentQuery,
|
||||||
|
testAlertRule,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleSaveAlert = useCallback((): void => {
|
||||||
|
const payload = buildCreateThresholdAlertRulePayload({
|
||||||
|
alertType,
|
||||||
|
basicAlertState,
|
||||||
|
thresholdState,
|
||||||
|
advancedOptions,
|
||||||
|
evaluationWindow,
|
||||||
|
notificationSettings,
|
||||||
|
query: currentQuery,
|
||||||
|
});
|
||||||
|
createAlertRule(payload, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Alert rule created successfully');
|
||||||
|
safeNavigate('/alerts');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
alertType,
|
||||||
|
basicAlertState,
|
||||||
|
thresholdState,
|
||||||
|
advancedOptions,
|
||||||
|
evaluationWindow,
|
||||||
|
notificationSettings,
|
||||||
|
currentQuery,
|
||||||
|
createAlertRule,
|
||||||
|
safeNavigate,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const disableButtons =
|
||||||
|
isCreatingAlertRule || isTestingAlertRule || !!alertValidationMessage;
|
||||||
|
|
||||||
|
const saveAlertButton = useMemo(() => {
|
||||||
|
let button = (
|
||||||
|
<Button type="primary" onClick={handleSaveAlert} disabled={disableButtons}>
|
||||||
|
<Check size={14} />
|
||||||
|
<Typography.Text>Save Alert Rule</Typography.Text>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
if (alertValidationMessage) {
|
||||||
|
button = <Tooltip title={alertValidationMessage}>{button}</Tooltip>;
|
||||||
|
}
|
||||||
|
return button;
|
||||||
|
}, [alertValidationMessage, disableButtons, handleSaveAlert]);
|
||||||
|
|
||||||
|
const testAlertButton = useMemo(() => {
|
||||||
|
let button = (
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
onClick={handleTestNotification}
|
||||||
|
disabled={disableButtons}
|
||||||
|
>
|
||||||
|
<Send size={14} />
|
||||||
|
<Typography.Text>Test Notification</Typography.Text>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
if (alertValidationMessage) {
|
||||||
|
button = <Tooltip title={alertValidationMessage}>{button}</Tooltip>;
|
||||||
|
}
|
||||||
|
return button;
|
||||||
|
}, [alertValidationMessage, disableButtons, handleTestNotification]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="create-alert-v2-footer">
|
||||||
|
<Button type="text" onClick={handleDiscard} disabled={disableButtons}>
|
||||||
|
<X size={14} /> Discard
|
||||||
|
</Button>
|
||||||
|
<div className="button-group">
|
||||||
|
{testAlertButton}
|
||||||
|
{saveAlertButton}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
3
frontend/src/container/CreateAlertV2/Footer/index.ts
Normal file
3
frontend/src/container/CreateAlertV2/Footer/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import Footer from './Footer';
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
51
frontend/src/container/CreateAlertV2/Footer/styles.scss
Normal file
51
frontend/src/container/CreateAlertV2/Footer/styles.scss
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
.create-alert-v2-footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 63px;
|
||||||
|
right: 0;
|
||||||
|
background-color: var(--bg-ink-500);
|
||||||
|
height: 70px;
|
||||||
|
border-top: 1px solid var(--bg-slate-500);
|
||||||
|
padding: 16px 24px;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn-default {
|
||||||
|
background-color: var(--bg-slate-500);
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.create-alert-v2-footer {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
border-top: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.ant-btn-default {
|
||||||
|
background-color: var(--bg-vanilla-300);
|
||||||
|
border: 1px solid var(--bg-vanilla-400);
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn-primary {
|
||||||
|
.ant-typography {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
frontend/src/container/CreateAlertV2/Footer/types.ts
Normal file
20
frontend/src/container/CreateAlertV2/Footer/types.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AdvancedOptionsState,
|
||||||
|
AlertState,
|
||||||
|
AlertThresholdState,
|
||||||
|
EvaluationWindowState,
|
||||||
|
NotificationSettingsState,
|
||||||
|
} from '../context/types';
|
||||||
|
|
||||||
|
export interface BuildCreateAlertRulePayloadArgs {
|
||||||
|
alertType: AlertTypes;
|
||||||
|
basicAlertState: AlertState;
|
||||||
|
thresholdState: AlertThresholdState;
|
||||||
|
advancedOptions: AdvancedOptionsState;
|
||||||
|
evaluationWindow: EvaluationWindowState;
|
||||||
|
notificationSettings: NotificationSettingsState;
|
||||||
|
query: Query;
|
||||||
|
}
|
||||||
347
frontend/src/container/CreateAlertV2/Footer/utils.tsx
Normal file
347
frontend/src/container/CreateAlertV2/Footer/utils.tsx
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||||
|
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||||
|
import {
|
||||||
|
BasicThreshold,
|
||||||
|
PostableAlertRuleV2,
|
||||||
|
} from 'types/api/alerts/alertTypesV2';
|
||||||
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
|
import { compositeQueryToQueryEnvelope } from 'utils/compositeQueryToQueryEnvelope';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AdvancedOptionsState,
|
||||||
|
EvaluationWindowState,
|
||||||
|
NotificationSettingsState,
|
||||||
|
} from '../context/types';
|
||||||
|
import { BuildCreateAlertRulePayloadArgs } from './types';
|
||||||
|
|
||||||
|
// Get formatted time/unit pairs for create alert api payload
|
||||||
|
function getFormattedTimeValue(timeValue: number, unit: string): string {
|
||||||
|
const unitMap: Record<string, string> = {
|
||||||
|
[UniversalYAxisUnit.SECONDS]: 's',
|
||||||
|
[UniversalYAxisUnit.MINUTES]: 'm',
|
||||||
|
[UniversalYAxisUnit.HOURS]: 'h',
|
||||||
|
[UniversalYAxisUnit.DAYS]: 'd',
|
||||||
|
};
|
||||||
|
return `${timeValue}${unitMap[unit]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate create alert api payload
|
||||||
|
export function validateCreateAlertState(
|
||||||
|
args: BuildCreateAlertRulePayloadArgs,
|
||||||
|
): string | null {
|
||||||
|
const { basicAlertState, thresholdState, notificationSettings } = args;
|
||||||
|
|
||||||
|
// Validate alert name
|
||||||
|
if (!basicAlertState.name) {
|
||||||
|
return 'Please enter an alert name';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate threshold state if routing policies is not enabled
|
||||||
|
for (let i = 0; i < thresholdState.thresholds.length; i++) {
|
||||||
|
const threshold = thresholdState.thresholds[i];
|
||||||
|
if (!threshold.label) {
|
||||||
|
return 'Please enter a label for each threshold';
|
||||||
|
}
|
||||||
|
if (!notificationSettings.routingPolicies && !threshold.channels.length) {
|
||||||
|
return 'Please select at least one channel for each threshold or enable routing policies';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get notification settings props for create alert api payload
|
||||||
|
export function getNotificationSettingsProps(
|
||||||
|
notificationSettings: NotificationSettingsState,
|
||||||
|
): PostableAlertRuleV2['notificationSettings'] {
|
||||||
|
const notificationSettingsProps: PostableAlertRuleV2['notificationSettings'] = {
|
||||||
|
notificationGroupBy: notificationSettings.multipleNotifications || [],
|
||||||
|
alertStates: notificationSettings.reNotification.enabled
|
||||||
|
? notificationSettings.reNotification.conditions
|
||||||
|
: [],
|
||||||
|
notificationPolicy: notificationSettings.routingPolicies,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (notificationSettings.reNotification.enabled) {
|
||||||
|
notificationSettingsProps.renotify = getFormattedTimeValue(
|
||||||
|
notificationSettings.reNotification.value,
|
||||||
|
notificationSettings.reNotification.unit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return notificationSettingsProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get alert on absent props for create alert api payload
|
||||||
|
export function getAlertOnAbsentProps(
|
||||||
|
advancedOptions: AdvancedOptionsState,
|
||||||
|
): Partial<PostableAlertRuleV2['condition']> {
|
||||||
|
if (advancedOptions.sendNotificationIfDataIsMissing.enabled) {
|
||||||
|
return {
|
||||||
|
alertOnAbsent: true,
|
||||||
|
absentFor: advancedOptions.sendNotificationIfDataIsMissing.toleranceLimit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
alertOnAbsent: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get enforce minimum datapoints props for create alert api payload
|
||||||
|
export function getEnforceMinimumDatapointsProps(
|
||||||
|
advancedOptions: AdvancedOptionsState,
|
||||||
|
): Partial<PostableAlertRuleV2['condition']> {
|
||||||
|
if (advancedOptions.enforceMinimumDatapoints.enabled) {
|
||||||
|
return {
|
||||||
|
requireMinPoints: true,
|
||||||
|
requiredNumPoints:
|
||||||
|
advancedOptions.enforceMinimumDatapoints.minimumDatapoints,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
requireMinPoints: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get evaluation props for create alert api payload
|
||||||
|
export function getEvaluationProps(
|
||||||
|
evaluationWindow: EvaluationWindowState,
|
||||||
|
advancedOptions: AdvancedOptionsState,
|
||||||
|
): PostableAlertRuleV2['evaluation'] {
|
||||||
|
const frequency = getFormattedTimeValue(
|
||||||
|
advancedOptions.evaluationCadence.default.value,
|
||||||
|
advancedOptions.evaluationCadence.default.timeUnit,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
evaluationWindow.windowType === 'rolling' &&
|
||||||
|
evaluationWindow.timeframe !== 'custom'
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
kind: evaluationWindow.windowType,
|
||||||
|
spec: {
|
||||||
|
evalWindow: evaluationWindow.timeframe,
|
||||||
|
frequency,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
evaluationWindow.windowType === 'rolling' &&
|
||||||
|
evaluationWindow.timeframe === 'custom'
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
kind: evaluationWindow.windowType,
|
||||||
|
spec: {
|
||||||
|
evalWindow: getFormattedTimeValue(
|
||||||
|
Number(evaluationWindow.startingAt.number),
|
||||||
|
evaluationWindow.startingAt.unit,
|
||||||
|
),
|
||||||
|
frequency,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only cumulative window type left now
|
||||||
|
if (evaluationWindow.timeframe === 'currentHour') {
|
||||||
|
return {
|
||||||
|
kind: evaluationWindow.windowType,
|
||||||
|
spec: {
|
||||||
|
schedule: {
|
||||||
|
type: 'hourly',
|
||||||
|
minute: Number(evaluationWindow.startingAt.number),
|
||||||
|
},
|
||||||
|
frequency,
|
||||||
|
timezone: evaluationWindow.startingAt.timezone,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evaluationWindow.timeframe === 'currentDay') {
|
||||||
|
// time is in the format of "HH:MM:SS"
|
||||||
|
const [hour, minute] = evaluationWindow.startingAt.time.split(':');
|
||||||
|
return {
|
||||||
|
kind: evaluationWindow.windowType,
|
||||||
|
spec: {
|
||||||
|
schedule: {
|
||||||
|
type: 'daily',
|
||||||
|
hour: Number(hour),
|
||||||
|
minute: Number(minute),
|
||||||
|
},
|
||||||
|
frequency,
|
||||||
|
timezone: evaluationWindow.startingAt.timezone,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evaluationWindow.timeframe === 'currentMonth') {
|
||||||
|
// time is in the format of "HH:MM:SS"
|
||||||
|
const [hour, minute] = evaluationWindow.startingAt.time.split(':');
|
||||||
|
return {
|
||||||
|
kind: evaluationWindow.windowType,
|
||||||
|
spec: {
|
||||||
|
schedule: {
|
||||||
|
type: 'monthly',
|
||||||
|
day: Number(evaluationWindow.startingAt.number),
|
||||||
|
hour: Number(hour),
|
||||||
|
minute: Number(minute),
|
||||||
|
},
|
||||||
|
frequency,
|
||||||
|
timezone: evaluationWindow.startingAt.timezone,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: evaluationWindow.windowType,
|
||||||
|
spec: {
|
||||||
|
evalWindow: evaluationWindow.timeframe,
|
||||||
|
frequency,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build Create Threshold Alert Rule Payload
|
||||||
|
export function buildCreateThresholdAlertRulePayload(
|
||||||
|
args: BuildCreateAlertRulePayloadArgs,
|
||||||
|
): PostableAlertRuleV2 {
|
||||||
|
const {
|
||||||
|
alertType,
|
||||||
|
basicAlertState,
|
||||||
|
thresholdState,
|
||||||
|
evaluationWindow,
|
||||||
|
advancedOptions,
|
||||||
|
notificationSettings,
|
||||||
|
query,
|
||||||
|
} = args;
|
||||||
|
|
||||||
|
const compositeQuery = compositeQueryToQueryEnvelope({
|
||||||
|
builderQueries: {
|
||||||
|
...mapQueryDataToApi(query.builder.queryData, 'queryName').data,
|
||||||
|
...mapQueryDataToApi(query.builder.queryFormulas, 'queryName').data,
|
||||||
|
},
|
||||||
|
promQueries: mapQueryDataToApi(query.promql, 'name').data,
|
||||||
|
chQueries: mapQueryDataToApi(query.clickhouse_sql, 'name').data,
|
||||||
|
queryType: query.queryType,
|
||||||
|
panelType: PANEL_TYPES.TIME_SERIES,
|
||||||
|
unit: basicAlertState.yAxisUnit,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Thresholds
|
||||||
|
const thresholds: BasicThreshold[] = thresholdState.thresholds.map(
|
||||||
|
(threshold) => ({
|
||||||
|
name: threshold.label,
|
||||||
|
target: parseFloat(threshold.thresholdValue.toString()),
|
||||||
|
matchType: thresholdState.matchType,
|
||||||
|
op: thresholdState.operator,
|
||||||
|
channels: threshold.channels,
|
||||||
|
targetUnit: threshold.unit,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Alert on absent data
|
||||||
|
const alertOnAbsentProps = getAlertOnAbsentProps(advancedOptions);
|
||||||
|
|
||||||
|
// Enforce minimum datapoints
|
||||||
|
const enforceMinimumDatapointsProps = getEnforceMinimumDatapointsProps(
|
||||||
|
advancedOptions,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Notification settings
|
||||||
|
const notificationSettingsProps = getNotificationSettingsProps(
|
||||||
|
notificationSettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Evaluation
|
||||||
|
const evaluationProps = getEvaluationProps(evaluationWindow, advancedOptions);
|
||||||
|
|
||||||
|
let ruleType: string = AlertDetectionTypes.THRESHOLD_ALERT;
|
||||||
|
if (query.queryType === EQueryType.PROM) {
|
||||||
|
ruleType = 'promql_rule';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
alert: basicAlertState.name,
|
||||||
|
ruleType,
|
||||||
|
alertType,
|
||||||
|
condition: {
|
||||||
|
thresholds: {
|
||||||
|
kind: 'basic',
|
||||||
|
spec: thresholds,
|
||||||
|
},
|
||||||
|
compositeQuery,
|
||||||
|
selectedQueryName: thresholdState.selectedQuery,
|
||||||
|
...alertOnAbsentProps,
|
||||||
|
...enforceMinimumDatapointsProps,
|
||||||
|
},
|
||||||
|
evaluation: evaluationProps,
|
||||||
|
labels: basicAlertState.labels,
|
||||||
|
annotations: {
|
||||||
|
description: notificationSettings.description,
|
||||||
|
summary: notificationSettings.description,
|
||||||
|
},
|
||||||
|
notificationSettings: notificationSettingsProps,
|
||||||
|
version: 'v5',
|
||||||
|
schemaVersion: 'v2alpha1',
|
||||||
|
source: window?.location.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build Create Anomaly Alert Rule Payload
|
||||||
|
// TODO: Update this function before enabling anomaly alert rule creation
|
||||||
|
export function buildCreateAnomalyAlertRulePayload(
|
||||||
|
args: BuildCreateAlertRulePayloadArgs,
|
||||||
|
): PostableAlertRuleV2 {
|
||||||
|
const {
|
||||||
|
alertType,
|
||||||
|
basicAlertState,
|
||||||
|
query,
|
||||||
|
notificationSettings,
|
||||||
|
evaluationWindow,
|
||||||
|
advancedOptions,
|
||||||
|
} = args;
|
||||||
|
|
||||||
|
const compositeQuery = compositeQueryToQueryEnvelope({
|
||||||
|
builderQueries: {
|
||||||
|
...mapQueryDataToApi(query.builder.queryData, 'queryName').data,
|
||||||
|
...mapQueryDataToApi(query.builder.queryFormulas, 'queryName').data,
|
||||||
|
},
|
||||||
|
promQueries: mapQueryDataToApi(query.promql, 'name').data,
|
||||||
|
chQueries: mapQueryDataToApi(query.clickhouse_sql, 'name').data,
|
||||||
|
queryType: query.queryType,
|
||||||
|
panelType: PANEL_TYPES.TIME_SERIES,
|
||||||
|
unit: basicAlertState.yAxisUnit,
|
||||||
|
});
|
||||||
|
|
||||||
|
const alertOnAbsentProps = getAlertOnAbsentProps(advancedOptions);
|
||||||
|
const enforceMinimumDatapointsProps = getEnforceMinimumDatapointsProps(
|
||||||
|
advancedOptions,
|
||||||
|
);
|
||||||
|
const evaluationProps = getEvaluationProps(evaluationWindow, advancedOptions);
|
||||||
|
const notificationSettingsProps = getNotificationSettingsProps(
|
||||||
|
notificationSettings,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
alert: basicAlertState.name,
|
||||||
|
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||||
|
alertType,
|
||||||
|
condition: {
|
||||||
|
compositeQuery,
|
||||||
|
...alertOnAbsentProps,
|
||||||
|
...enforceMinimumDatapointsProps,
|
||||||
|
},
|
||||||
|
labels: basicAlertState.labels,
|
||||||
|
annotations: {
|
||||||
|
description: notificationSettings.description,
|
||||||
|
summary: notificationSettings.description,
|
||||||
|
},
|
||||||
|
notificationSettings: notificationSettingsProps,
|
||||||
|
evaluation: evaluationProps,
|
||||||
|
version: '',
|
||||||
|
schemaVersion: '',
|
||||||
|
source: window?.location.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { Button, Popover, Tooltip, Typography } from 'antd';
|
import { Tooltip, Typography } from 'antd';
|
||||||
import TextArea from 'antd/lib/input/TextArea';
|
import TextArea from 'antd/lib/input/TextArea';
|
||||||
import { Info } from 'lucide-react';
|
import { Info } from 'lucide-react';
|
||||||
|
|
||||||
@ -10,46 +10,46 @@ function NotificationMessage(): JSX.Element {
|
|||||||
setNotificationSettings,
|
setNotificationSettings,
|
||||||
} = useCreateAlertState();
|
} = useCreateAlertState();
|
||||||
|
|
||||||
const templateVariables = [
|
// const templateVariables = [
|
||||||
{ variable: '{{alertname}}', description: 'Name of the alert rule' },
|
// { variable: '{{alertname}}', description: 'Name of the alert rule' },
|
||||||
{
|
// {
|
||||||
variable: '{{value}}',
|
// variable: '{{value}}',
|
||||||
description: 'Current value that triggered the alert',
|
// description: 'Current value that triggered the alert',
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
variable: '{{threshold}}',
|
// variable: '{{threshold}}',
|
||||||
description: 'Threshold value from alert condition',
|
// description: 'Threshold value from alert condition',
|
||||||
},
|
// },
|
||||||
{ variable: '{{unit}}', description: 'Unit of measurement for the metric' },
|
// { variable: '{{unit}}', description: 'Unit of measurement for the metric' },
|
||||||
{
|
// {
|
||||||
variable: '{{severity}}',
|
// variable: '{{severity}}',
|
||||||
description: 'Alert severity level (Critical, Warning, Info)',
|
// description: 'Alert severity level (Critical, Warning, Info)',
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
variable: '{{queryname}}',
|
// variable: '{{queryname}}',
|
||||||
description: 'Name of the query that triggered the alert',
|
// description: 'Name of the query that triggered the alert',
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
variable: '{{labels}}',
|
// variable: '{{labels}}',
|
||||||
description: 'All labels associated with the alert',
|
// description: 'All labels associated with the alert',
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
variable: '{{timestamp}}',
|
// variable: '{{timestamp}}',
|
||||||
description: 'Timestamp when alert was triggered',
|
// description: 'Timestamp when alert was triggered',
|
||||||
},
|
// },
|
||||||
];
|
// ];
|
||||||
|
|
||||||
const templateVariableContent = (
|
// const templateVariableContent = (
|
||||||
<div className="template-variable-content">
|
// <div className="template-variable-content">
|
||||||
<Typography.Text strong>Available Template Variables:</Typography.Text>
|
// <Typography.Text strong>Available Template Variables:</Typography.Text>
|
||||||
{templateVariables.map((item) => (
|
// {templateVariables.map((item) => (
|
||||||
<div className="template-variable-content-item" key={item.variable}>
|
// <div className="template-variable-content-item" key={item.variable}>
|
||||||
<code>{item.variable}</code>
|
// <code>{item.variable}</code>
|
||||||
<Typography.Text>{item.description}</Typography.Text>
|
// <Typography.Text>{item.description}</Typography.Text>
|
||||||
</div>
|
// </div>
|
||||||
))}
|
// ))}
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="notification-message-container">
|
<div className="notification-message-container">
|
||||||
@ -67,12 +67,13 @@ function NotificationMessage(): JSX.Element {
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<div className="notification-message-header-actions">
|
<div className="notification-message-header-actions">
|
||||||
<Popover content={templateVariableContent}>
|
{/* TODO: Add back when the functionality is implemented */}
|
||||||
|
{/* <Popover content={templateVariableContent}>
|
||||||
<Button type="text">
|
<Button type="text">
|
||||||
<Info size={12} />
|
<Info size={12} />
|
||||||
Variables
|
Variables
|
||||||
</Button>
|
</Button>
|
||||||
</Popover>
|
</Popover> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TextArea
|
<TextArea
|
||||||
|
|||||||
@ -84,12 +84,28 @@
|
|||||||
.ant-select {
|
.ant-select {
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-select-multiple {
|
.ant-select-multiple {
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -202,6 +218,15 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,6 +352,15 @@
|
|||||||
.ant-select {
|
.ant-select {
|
||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
border: 1px solid var(--bg-vanilla-300);
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--bg-ink-300);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { useCreateAlertState } from 'container/CreateAlertV2/context';
|
import { useCreateAlertState } from 'container/CreateAlertV2/context';
|
||||||
import ChartPreviewComponent from 'container/FormAlertRules/ChartPreview';
|
import ChartPreviewComponent from 'container/FormAlertRules/ChartPreview';
|
||||||
@ -16,7 +17,7 @@ export interface ChartPreviewProps {
|
|||||||
|
|
||||||
function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
|
function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
|
||||||
const { currentQuery, panelType, stagedQuery } = useQueryBuilder();
|
const { currentQuery, panelType, stagedQuery } = useQueryBuilder();
|
||||||
const { thresholdState, alertState } = useCreateAlertState();
|
const { thresholdState, alertState, setAlertState } = useCreateAlertState();
|
||||||
const { selectedTime: globalSelectedInterval } = useSelector<
|
const { selectedTime: globalSelectedInterval } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
GlobalReducer
|
GlobalReducer
|
||||||
@ -25,14 +26,24 @@ function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
|
|||||||
|
|
||||||
const yAxisUnit = alertState.yAxisUnit || '';
|
const yAxisUnit = alertState.yAxisUnit || '';
|
||||||
|
|
||||||
const renderQBChartPreview = (): JSX.Element => (
|
const headline = (
|
||||||
<ChartPreviewComponent
|
<div className="chart-preview-headline">
|
||||||
headline={
|
|
||||||
<PlotTag
|
<PlotTag
|
||||||
queryType={currentQuery.queryType}
|
queryType={currentQuery.queryType}
|
||||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||||
/>
|
/>
|
||||||
}
|
<YAxisUnitSelector
|
||||||
|
value={alertState.yAxisUnit}
|
||||||
|
onChange={(value): void => {
|
||||||
|
setAlertState({ type: 'SET_Y_AXIS_UNIT', payload: value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderQBChartPreview = (): JSX.Element => (
|
||||||
|
<ChartPreviewComponent
|
||||||
|
headline={headline}
|
||||||
name=""
|
name=""
|
||||||
query={stagedQuery}
|
query={stagedQuery}
|
||||||
selectedInterval={globalSelectedInterval}
|
selectedInterval={globalSelectedInterval}
|
||||||
@ -47,12 +58,7 @@ function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
|
|||||||
|
|
||||||
const renderPromAndChQueryChartPreview = (): JSX.Element => (
|
const renderPromAndChQueryChartPreview = (): JSX.Element => (
|
||||||
<ChartPreviewComponent
|
<ChartPreviewComponent
|
||||||
headline={
|
headline={headline}
|
||||||
<PlotTag
|
|
||||||
queryType={currentQuery.queryType}
|
|
||||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
name="Chart Preview"
|
name="Chart Preview"
|
||||||
query={stagedQuery}
|
query={stagedQuery}
|
||||||
alertDef={alertDef}
|
alertDef={alertDef}
|
||||||
|
|||||||
@ -2,12 +2,13 @@ import './styles.scss';
|
|||||||
|
|
||||||
import { Button } from 'antd';
|
import { Button } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import QuerySectionComponent from 'container/FormAlertRules/QuerySection';
|
import QuerySectionComponent from 'container/FormAlertRules/QuerySection';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { BarChart2, DraftingCompass, FileText, ScrollText } from 'lucide-react';
|
import { BarChart2, DraftingCompass, FileText, ScrollText } from 'lucide-react';
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
|
|
||||||
import { useCreateAlertState } from '../context';
|
import { useCreateAlertState } from '../context';
|
||||||
import Stepper from '../Stepper';
|
import Stepper from '../Stepper';
|
||||||
@ -15,17 +16,20 @@ import ChartPreview from './ChartPreview';
|
|||||||
import { buildAlertDefForChartPreview } from './utils';
|
import { buildAlertDefForChartPreview } from './utils';
|
||||||
|
|
||||||
function QuerySection(): JSX.Element {
|
function QuerySection(): JSX.Element {
|
||||||
const { currentQuery, handleRunQuery } = useQueryBuilder();
|
|
||||||
const {
|
const {
|
||||||
alertState,
|
currentQuery,
|
||||||
setAlertState,
|
handleRunQuery,
|
||||||
alertType,
|
redirectWithQueryBuilderData,
|
||||||
setAlertType,
|
} = useQueryBuilder();
|
||||||
thresholdState,
|
const { alertType, setAlertType, thresholdState } = useCreateAlertState();
|
||||||
} = useCreateAlertState();
|
|
||||||
|
|
||||||
const alertDef = buildAlertDefForChartPreview({ alertType, thresholdState });
|
const alertDef = buildAlertDefForChartPreview({ alertType, thresholdState });
|
||||||
|
|
||||||
|
const onQueryCategoryChange = (val: EQueryType): void => {
|
||||||
|
const query: Query = { ...currentQuery, queryType: val };
|
||||||
|
redirectWithQueryBuilderData(query);
|
||||||
|
};
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: 'Metrics',
|
label: 'Metrics',
|
||||||
@ -51,17 +55,8 @@ function QuerySection(): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="query-section">
|
<div className="query-section">
|
||||||
<Stepper
|
<Stepper stepNumber={1} label="Define the query" />
|
||||||
stepNumber={1}
|
|
||||||
label="Define the query you want to set an alert on"
|
|
||||||
/>
|
|
||||||
<ChartPreview alertDef={alertDef} />
|
<ChartPreview alertDef={alertDef} />
|
||||||
<YAxisUnitSelector
|
|
||||||
value={alertState.yAxisUnit}
|
|
||||||
onChange={(value): void => {
|
|
||||||
setAlertState({ type: 'SET_Y_AXIS_UNIT', payload: value });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="query-section-tabs">
|
<div className="query-section-tabs">
|
||||||
<div className="query-section-query-actions">
|
<div className="query-section-query-actions">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
@ -82,7 +77,7 @@ function QuerySection(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
<QuerySectionComponent
|
<QuerySectionComponent
|
||||||
queryCategory={currentQuery.queryType}
|
queryCategory={currentQuery.queryType}
|
||||||
setQueryCategory={(): void => {}}
|
setQueryCategory={onQueryCategoryChange}
|
||||||
alertType={alertType}
|
alertType={alertType}
|
||||||
runQuery={handleRunQuery}
|
runQuery={handleRunQuery}
|
||||||
alertDef={alertDef}
|
alertDef={alertDef}
|
||||||
|
|||||||
@ -134,7 +134,7 @@ const renderChartPreview = (): ReturnType<typeof render> =>
|
|||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<CreateAlertProvider>
|
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||||
<ChartPreview alertDef={mockAlertDef} />
|
<ChartPreview alertDef={mockAlertDef} />
|
||||||
</CreateAlertProvider>
|
</CreateAlertProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
|
import { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
@ -104,7 +105,7 @@ const renderQuerySection = (): ReturnType<typeof render> =>
|
|||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<CreateAlertProvider>
|
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||||
<QuerySection />
|
<QuerySection />
|
||||||
</CreateAlertProvider>
|
</CreateAlertProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
@ -135,7 +136,7 @@ describe('QuerySection', () => {
|
|||||||
expect(screen.getByTestId('stepper')).toBeInTheDocument();
|
expect(screen.getByTestId('stepper')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('step-number')).toHaveTextContent('1');
|
expect(screen.getByTestId('step-number')).toHaveTextContent('1');
|
||||||
expect(screen.getByTestId('step-label')).toHaveTextContent(
|
expect(screen.getByTestId('step-label')).toHaveTextContent(
|
||||||
'Define the query you want to set an alert on',
|
'Define the query',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if ChartPreview is rendered
|
// Check if ChartPreview is rendered
|
||||||
@ -186,6 +187,7 @@ describe('QuerySection', () => {
|
|||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
{
|
{
|
||||||
[QueryParams.alertType]: AlertTypes.LOGS_BASED_ALERT,
|
[QueryParams.alertType]: AlertTypes.LOGS_BASED_ALERT,
|
||||||
|
[QueryParams.ruleType]: AlertDetectionTypes.THRESHOLD_ALERT,
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
true,
|
true,
|
||||||
@ -200,6 +202,7 @@ describe('QuerySection', () => {
|
|||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
{
|
{
|
||||||
[QueryParams.alertType]: AlertTypes.TRACES_BASED_ALERT,
|
[QueryParams.alertType]: AlertTypes.TRACES_BASED_ALERT,
|
||||||
|
[QueryParams.ruleType]: AlertDetectionTypes.THRESHOLD_ALERT,
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
true,
|
true,
|
||||||
|
|||||||
@ -77,6 +77,14 @@
|
|||||||
.ant-select-selector {
|
.ant-select-selector {
|
||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
background: var(--bg-ink-300);
|
background: var(--bg-ink-300);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -88,6 +96,18 @@
|
|||||||
border: 1px solid var(--bg-slate-500);
|
border: 1px solid var(--bg-slate-500);
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
background-color: var(--bg-ink-500);
|
background-color: var(--bg-ink-500);
|
||||||
|
|
||||||
|
.chart-preview-headline {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.y-axis-unit-selector-component {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,3 +119,78 @@
|
|||||||
border: 1px solid var(--bg-slate-400);
|
border: 1px solid var(--bg-slate-400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.query-section {
|
||||||
|
.query-section-tabs {
|
||||||
|
.query-section-query-actions {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.explorer-view-option {
|
||||||
|
border-left: 0.5px solid var(--bg-vanilla-300);
|
||||||
|
border-bottom: 0.5px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
&.active-tab {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-vanilla-100) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.y-axis-unit-selector-component {
|
||||||
|
.ant-select {
|
||||||
|
.ant-select-selector {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--bg-ink-300);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-preview-container {
|
||||||
|
.alert-chart-container {
|
||||||
|
.ant-card {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
.ant-card-body {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
.chart-preview-header {
|
||||||
|
.plot-tag {
|
||||||
|
background-color: var(--bg-vanilla-300);
|
||||||
|
color: var(--bg-slate-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-query-section-container {
|
||||||
|
background-color: var(--bg-vanilla-100);
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -42,3 +42,22 @@
|
|||||||
background-position: center;
|
background-position: center;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.step-number {
|
||||||
|
background-color: var(--bg-robin-400);
|
||||||
|
color: var(--text-slate-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-label {
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dotted-line {
|
||||||
|
background-image: radial-gradient(
|
||||||
|
circle,
|
||||||
|
var(--bg-ink-200) 1px,
|
||||||
|
transparent 1px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -21,7 +21,6 @@ import {
|
|||||||
|
|
||||||
export const INITIAL_ALERT_STATE: AlertState = {
|
export const INITIAL_ALERT_STATE: AlertState = {
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
|
||||||
labels: {},
|
labels: {},
|
||||||
yAxisUnit: undefined,
|
yAxisUnit: undefined,
|
||||||
};
|
};
|
||||||
@ -30,7 +29,7 @@ export const INITIAL_CRITICAL_THRESHOLD: Threshold = {
|
|||||||
id: v4(),
|
id: v4(),
|
||||||
label: 'CRITICAL',
|
label: 'CRITICAL',
|
||||||
thresholdValue: 0,
|
thresholdValue: 0,
|
||||||
recoveryThresholdValue: 0,
|
recoveryThresholdValue: null,
|
||||||
unit: '',
|
unit: '',
|
||||||
channels: [],
|
channels: [],
|
||||||
color: Color.BG_SAKURA_500,
|
color: Color.BG_SAKURA_500,
|
||||||
@ -40,7 +39,7 @@ export const INITIAL_WARNING_THRESHOLD: Threshold = {
|
|||||||
id: v4(),
|
id: v4(),
|
||||||
label: 'WARNING',
|
label: 'WARNING',
|
||||||
thresholdValue: 0,
|
thresholdValue: 0,
|
||||||
recoveryThresholdValue: 0,
|
recoveryThresholdValue: null,
|
||||||
unit: '',
|
unit: '',
|
||||||
channels: [],
|
channels: [],
|
||||||
color: Color.BG_AMBER_500,
|
color: Color.BG_AMBER_500,
|
||||||
@ -50,7 +49,7 @@ export const INITIAL_INFO_THRESHOLD: Threshold = {
|
|||||||
id: v4(),
|
id: v4(),
|
||||||
label: 'INFO',
|
label: 'INFO',
|
||||||
thresholdValue: 0,
|
thresholdValue: 0,
|
||||||
recoveryThresholdValue: 0,
|
recoveryThresholdValue: null,
|
||||||
unit: '',
|
unit: '',
|
||||||
channels: [],
|
channels: [],
|
||||||
color: Color.BG_ROBIN_500,
|
color: Color.BG_ROBIN_500,
|
||||||
@ -60,7 +59,7 @@ export const INITIAL_RANDOM_THRESHOLD: Threshold = {
|
|||||||
id: v4(),
|
id: v4(),
|
||||||
label: '',
|
label: '',
|
||||||
thresholdValue: 0,
|
thresholdValue: 0,
|
||||||
recoveryThresholdValue: 0,
|
recoveryThresholdValue: null,
|
||||||
unit: '',
|
unit: '',
|
||||||
channels: [],
|
channels: [],
|
||||||
color: getRandomColor(),
|
color: getRandomColor(),
|
||||||
@ -80,9 +79,11 @@ export const INITIAL_ADVANCED_OPTIONS_STATE: AdvancedOptionsState = {
|
|||||||
sendNotificationIfDataIsMissing: {
|
sendNotificationIfDataIsMissing: {
|
||||||
toleranceLimit: 15,
|
toleranceLimit: 15,
|
||||||
timeUnit: UniversalYAxisUnit.MINUTES,
|
timeUnit: UniversalYAxisUnit.MINUTES,
|
||||||
|
enabled: false,
|
||||||
},
|
},
|
||||||
enforceMinimumDatapoints: {
|
enforceMinimumDatapoints: {
|
||||||
minimumDatapoints: 0,
|
minimumDatapoints: 0,
|
||||||
|
enabled: false,
|
||||||
},
|
},
|
||||||
delayEvaluation: {
|
delayEvaluation: {
|
||||||
delay: 5,
|
delay: 5,
|
||||||
@ -120,10 +121,10 @@ export const INITIAL_EVALUATION_WINDOW_STATE: EvaluationWindowState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const THRESHOLD_OPERATOR_OPTIONS = [
|
export const THRESHOLD_OPERATOR_OPTIONS = [
|
||||||
{ value: AlertThresholdOperator.IS_ABOVE, label: 'IS ABOVE' },
|
{ value: AlertThresholdOperator.IS_ABOVE, label: 'ABOVE' },
|
||||||
{ value: AlertThresholdOperator.IS_BELOW, label: 'IS BELOW' },
|
{ value: AlertThresholdOperator.IS_BELOW, label: 'BELOW' },
|
||||||
{ value: AlertThresholdOperator.IS_EQUAL_TO, label: 'IS EQUAL TO' },
|
{ value: AlertThresholdOperator.IS_EQUAL_TO, label: 'EQUAL TO' },
|
||||||
{ value: AlertThresholdOperator.IS_NOT_EQUAL_TO, label: 'IS NOT EQUAL TO' },
|
{ value: AlertThresholdOperator.IS_NOT_EQUAL_TO, label: 'NOT EQUAL TO' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ANOMALY_THRESHOLD_OPERATOR_OPTIONS = [
|
export const ANOMALY_THRESHOLD_OPERATOR_OPTIONS = [
|
||||||
@ -169,7 +170,6 @@ export const ADVANCED_OPTIONS_TIME_UNIT_OPTIONS = [
|
|||||||
{ value: UniversalYAxisUnit.SECONDS, label: 'Seconds' },
|
{ value: UniversalYAxisUnit.SECONDS, label: 'Seconds' },
|
||||||
{ value: UniversalYAxisUnit.MINUTES, label: 'Minutes' },
|
{ value: UniversalYAxisUnit.MINUTES, label: 'Minutes' },
|
||||||
{ value: UniversalYAxisUnit.HOURS, label: 'Hours' },
|
{ value: UniversalYAxisUnit.HOURS, label: 'Hours' },
|
||||||
{ value: UniversalYAxisUnit.DAYS, label: 'Days' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const NOTIFICATION_MESSAGE_PLACEHOLDER =
|
export const NOTIFICATION_MESSAGE_PLACEHOLDER =
|
||||||
@ -189,4 +189,5 @@ export const INITIAL_NOTIFICATION_SETTINGS_STATE: NotificationSettingsState = {
|
|||||||
conditions: [],
|
conditions: [],
|
||||||
},
|
},
|
||||||
description: NOTIFICATION_MESSAGE_PLACEHOLDER,
|
description: NOTIFICATION_MESSAGE_PLACEHOLDER,
|
||||||
|
routingPolicies: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
|
import { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||||
|
import { useCreateAlertRule } from 'hooks/alerts/useCreateAlertRule';
|
||||||
|
import { useTestAlertRule } from 'hooks/alerts/useTestAlertRule';
|
||||||
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 {
|
||||||
@ -72,6 +75,10 @@ export function CreateAlertProvider(
|
|||||||
currentQueryToRedirect,
|
currentQueryToRedirect,
|
||||||
{
|
{
|
||||||
[QueryParams.alertType]: value,
|
[QueryParams.alertType]: value,
|
||||||
|
[QueryParams.ruleType]:
|
||||||
|
value === AlertTypes.ANOMALY_BASED_ALERT
|
||||||
|
? AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||||
|
: AlertDetectionTypes.THRESHOLD_ALERT,
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
true,
|
true,
|
||||||
@ -107,6 +114,35 @@ export function CreateAlertProvider(
|
|||||||
});
|
});
|
||||||
}, [alertType]);
|
}, [alertType]);
|
||||||
|
|
||||||
|
const discardAlertRule = useCallback(() => {
|
||||||
|
setAlertState({
|
||||||
|
type: 'RESET',
|
||||||
|
});
|
||||||
|
setThresholdState({
|
||||||
|
type: 'RESET',
|
||||||
|
});
|
||||||
|
setEvaluationWindow({
|
||||||
|
type: 'RESET',
|
||||||
|
});
|
||||||
|
setAdvancedOptions({
|
||||||
|
type: 'RESET',
|
||||||
|
});
|
||||||
|
setNotificationSettings({
|
||||||
|
type: 'RESET',
|
||||||
|
});
|
||||||
|
handleAlertTypeChange(AlertTypes.METRICS_BASED_ALERT);
|
||||||
|
}, [handleAlertTypeChange]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: createAlertRule,
|
||||||
|
isLoading: isCreatingAlertRule,
|
||||||
|
} = useCreateAlertRule();
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: testAlertRule,
|
||||||
|
isLoading: isTestingAlertRule,
|
||||||
|
} = useTestAlertRule();
|
||||||
|
|
||||||
const contextValue: ICreateAlertContextProps = useMemo(
|
const contextValue: ICreateAlertContextProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
alertState,
|
alertState,
|
||||||
@ -121,6 +157,11 @@ export function CreateAlertProvider(
|
|||||||
setAdvancedOptions,
|
setAdvancedOptions,
|
||||||
notificationSettings,
|
notificationSettings,
|
||||||
setNotificationSettings,
|
setNotificationSettings,
|
||||||
|
discardAlertRule,
|
||||||
|
createAlertRule,
|
||||||
|
isCreatingAlertRule,
|
||||||
|
testAlertRule,
|
||||||
|
isTestingAlertRule,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
alertState,
|
alertState,
|
||||||
@ -130,6 +171,11 @@ export function CreateAlertProvider(
|
|||||||
evaluationWindow,
|
evaluationWindow,
|
||||||
advancedOptions,
|
advancedOptions,
|
||||||
notificationSettings,
|
notificationSettings,
|
||||||
|
discardAlertRule,
|
||||||
|
createAlertRule,
|
||||||
|
isCreatingAlertRule,
|
||||||
|
testAlertRule,
|
||||||
|
isTestingAlertRule,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
|
import { CreateAlertRuleResponse } from 'api/alerts/createAlertRule';
|
||||||
|
import { TestAlertRuleResponse } from 'api/alerts/testAlertRule';
|
||||||
import { Dayjs } from 'dayjs';
|
import { Dayjs } from 'dayjs';
|
||||||
import { Dispatch } from 'react';
|
import { Dispatch } from 'react';
|
||||||
|
import { UseMutateFunction } from 'react-query';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
|
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||||
import { Labels } from 'types/api/alerts/def';
|
import { Labels } from 'types/api/alerts/def';
|
||||||
|
|
||||||
export interface ICreateAlertContextProps {
|
export interface ICreateAlertContextProps {
|
||||||
@ -16,10 +21,26 @@ export interface ICreateAlertContextProps {
|
|||||||
setEvaluationWindow: Dispatch<EvaluationWindowAction>;
|
setEvaluationWindow: Dispatch<EvaluationWindowAction>;
|
||||||
notificationSettings: NotificationSettingsState;
|
notificationSettings: NotificationSettingsState;
|
||||||
setNotificationSettings: Dispatch<NotificationSettingsAction>;
|
setNotificationSettings: Dispatch<NotificationSettingsAction>;
|
||||||
|
isCreatingAlertRule: boolean;
|
||||||
|
createAlertRule: UseMutateFunction<
|
||||||
|
SuccessResponse<CreateAlertRuleResponse, unknown> | ErrorResponse,
|
||||||
|
Error,
|
||||||
|
PostableAlertRuleV2,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
isTestingAlertRule: boolean;
|
||||||
|
testAlertRule: UseMutateFunction<
|
||||||
|
SuccessResponse<TestAlertRuleResponse, unknown> | ErrorResponse,
|
||||||
|
Error,
|
||||||
|
PostableAlertRuleV2,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
discardAlertRule: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICreateAlertProviderProps {
|
export interface ICreateAlertProviderProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
initialAlertType: AlertTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AlertCreationStep {
|
export enum AlertCreationStep {
|
||||||
@ -31,14 +52,12 @@ export enum AlertCreationStep {
|
|||||||
|
|
||||||
export interface AlertState {
|
export interface AlertState {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
|
||||||
labels: Labels;
|
labels: Labels;
|
||||||
yAxisUnit: string | undefined;
|
yAxisUnit: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateAlertAction =
|
export type CreateAlertAction =
|
||||||
| { type: 'SET_ALERT_NAME'; payload: string }
|
| { type: 'SET_ALERT_NAME'; payload: string }
|
||||||
| { type: 'SET_ALERT_DESCRIPTION'; 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: 'RESET' };
|
| { type: 'RESET' };
|
||||||
@ -47,7 +66,7 @@ export interface Threshold {
|
|||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
thresholdValue: number;
|
thresholdValue: number;
|
||||||
recoveryThresholdValue: number;
|
recoveryThresholdValue: number | null;
|
||||||
unit: string;
|
unit: string;
|
||||||
channels: string[];
|
channels: string[];
|
||||||
color: string;
|
color: string;
|
||||||
@ -114,9 +133,11 @@ export interface AdvancedOptionsState {
|
|||||||
sendNotificationIfDataIsMissing: {
|
sendNotificationIfDataIsMissing: {
|
||||||
toleranceLimit: number;
|
toleranceLimit: number;
|
||||||
timeUnit: string;
|
timeUnit: string;
|
||||||
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
enforceMinimumDatapoints: {
|
enforceMinimumDatapoints: {
|
||||||
minimumDatapoints: number;
|
minimumDatapoints: number;
|
||||||
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
delayEvaluation: {
|
delayEvaluation: {
|
||||||
delay: number;
|
delay: number;
|
||||||
@ -147,10 +168,18 @@ export type AdvancedOptionsAction =
|
|||||||
type: 'SET_SEND_NOTIFICATION_IF_DATA_IS_MISSING';
|
type: 'SET_SEND_NOTIFICATION_IF_DATA_IS_MISSING';
|
||||||
payload: { toleranceLimit: number; timeUnit: string };
|
payload: { toleranceLimit: number; timeUnit: string };
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'TOGGLE_SEND_NOTIFICATION_IF_DATA_IS_MISSING';
|
||||||
|
payload: boolean;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'SET_ENFORCE_MINIMUM_DATAPOINTS';
|
type: 'SET_ENFORCE_MINIMUM_DATAPOINTS';
|
||||||
payload: { minimumDatapoints: number };
|
payload: { minimumDatapoints: number };
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'TOGGLE_ENFORCE_MINIMUM_DATAPOINTS';
|
||||||
|
payload: boolean;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'SET_DELAY_EVALUATION';
|
type: 'SET_DELAY_EVALUATION';
|
||||||
payload: { delay: number; timeUnit: string };
|
payload: { delay: number; timeUnit: string };
|
||||||
@ -203,6 +232,7 @@ export interface NotificationSettingsState {
|
|||||||
conditions: ('firing' | 'no-data')[];
|
conditions: ('firing' | 'no-data')[];
|
||||||
};
|
};
|
||||||
description: string;
|
description: string;
|
||||||
|
routingPolicies: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NotificationSettingsAction =
|
export type NotificationSettingsAction =
|
||||||
@ -220,4 +250,5 @@ export type NotificationSettingsAction =
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
| { type: 'SET_DESCRIPTION'; payload: string }
|
| { type: 'SET_DESCRIPTION'; payload: string }
|
||||||
|
| { type: 'SET_ROUTING_POLICIES'; payload: boolean }
|
||||||
| { type: 'RESET' };
|
| { type: 'RESET' };
|
||||||
|
|||||||
@ -41,11 +41,6 @@ export const alertCreationReducer = (
|
|||||||
...state,
|
...state,
|
||||||
name: action.payload,
|
name: action.payload,
|
||||||
};
|
};
|
||||||
case 'SET_ALERT_DESCRIPTION':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
description: action.payload,
|
|
||||||
};
|
|
||||||
case 'SET_ALERT_LABELS':
|
case 'SET_ALERT_LABELS':
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@ -99,6 +94,10 @@ export function getInitialAlertTypeFromURL(
|
|||||||
urlSearchParams: URLSearchParams,
|
urlSearchParams: URLSearchParams,
|
||||||
currentQuery: Query,
|
currentQuery: Query,
|
||||||
): AlertTypes {
|
): AlertTypes {
|
||||||
|
const ruleType = urlSearchParams.get(QueryParams.ruleType);
|
||||||
|
if (ruleType === 'anomaly_rule') {
|
||||||
|
return AlertTypes.ANOMALY_BASED_ALERT;
|
||||||
|
}
|
||||||
const alertTypeFromURL = urlSearchParams.get(QueryParams.alertType);
|
const alertTypeFromURL = urlSearchParams.get(QueryParams.alertType);
|
||||||
return alertTypeFromURL
|
return alertTypeFromURL
|
||||||
? (alertTypeFromURL as AlertTypes)
|
? (alertTypeFromURL as AlertTypes)
|
||||||
@ -131,9 +130,38 @@ export const advancedOptionsReducer = (
|
|||||||
): AdvancedOptionsState => {
|
): AdvancedOptionsState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'SET_SEND_NOTIFICATION_IF_DATA_IS_MISSING':
|
case 'SET_SEND_NOTIFICATION_IF_DATA_IS_MISSING':
|
||||||
return { ...state, sendNotificationIfDataIsMissing: action.payload };
|
return {
|
||||||
|
...state,
|
||||||
|
sendNotificationIfDataIsMissing: {
|
||||||
|
...state.sendNotificationIfDataIsMissing,
|
||||||
|
toleranceLimit: action.payload.toleranceLimit,
|
||||||
|
timeUnit: action.payload.timeUnit,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case 'TOGGLE_SEND_NOTIFICATION_IF_DATA_IS_MISSING':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
sendNotificationIfDataIsMissing: {
|
||||||
|
...state.sendNotificationIfDataIsMissing,
|
||||||
|
enabled: action.payload,
|
||||||
|
},
|
||||||
|
};
|
||||||
case 'SET_ENFORCE_MINIMUM_DATAPOINTS':
|
case 'SET_ENFORCE_MINIMUM_DATAPOINTS':
|
||||||
return { ...state, enforceMinimumDatapoints: action.payload };
|
return {
|
||||||
|
...state,
|
||||||
|
enforceMinimumDatapoints: {
|
||||||
|
...state.enforceMinimumDatapoints,
|
||||||
|
minimumDatapoints: action.payload.minimumDatapoints,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case 'TOGGLE_ENFORCE_MINIMUM_DATAPOINTS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
enforceMinimumDatapoints: {
|
||||||
|
...state.enforceMinimumDatapoints,
|
||||||
|
enabled: action.payload,
|
||||||
|
},
|
||||||
|
};
|
||||||
case 'SET_DELAY_EVALUATION':
|
case 'SET_DELAY_EVALUATION':
|
||||||
return { ...state, delayEvaluation: action.payload };
|
return { ...state, delayEvaluation: action.payload };
|
||||||
case 'SET_EVALUATION_CADENCE':
|
case 'SET_EVALUATION_CADENCE':
|
||||||
@ -190,6 +218,8 @@ export const notificationSettingsReducer = (
|
|||||||
return { ...state, reNotification: action.payload };
|
return { ...state, reNotification: action.payload };
|
||||||
case 'SET_DESCRIPTION':
|
case 'SET_DESCRIPTION':
|
||||||
return { ...state, description: action.payload };
|
return { ...state, description: action.payload };
|
||||||
|
case 'SET_ROUTING_POLICIES':
|
||||||
|
return { ...state, routingPolicies: action.payload };
|
||||||
case 'RESET':
|
case 'RESET':
|
||||||
return INITIAL_NOTIFICATION_SETTINGS_STATE;
|
return INITIAL_NOTIFICATION_SETTINGS_STATE;
|
||||||
default:
|
default:
|
||||||
|
|||||||
5
frontend/src/container/CreateAlertV2/types.ts
Normal file
5
frontend/src/container/CreateAlertV2/types.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
|
|
||||||
|
export interface CreateAlertV2Props {
|
||||||
|
alertType: AlertTypes;
|
||||||
|
}
|
||||||
@ -1,3 +1,8 @@
|
|||||||
|
import { Spin } from 'antd';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
import { useCreateAlertState } from './context';
|
||||||
|
|
||||||
// UI side feature flag
|
// UI side feature flag
|
||||||
export const showNewCreateAlertsPage = (): boolean =>
|
export const showNewCreateAlertsPage = (): boolean =>
|
||||||
localStorage.getItem('showNewCreateAlertsPage') === 'true';
|
localStorage.getItem('showNewCreateAlertsPage') === 'true';
|
||||||
@ -7,3 +12,16 @@ export const showNewCreateAlertsPage = (): boolean =>
|
|||||||
// Layout 2 - Condensed layout
|
// Layout 2 - Condensed layout
|
||||||
export const showCondensedLayout = (): boolean =>
|
export const showCondensedLayout = (): boolean =>
|
||||||
localStorage.getItem('showCondensedLayout') === 'true';
|
localStorage.getItem('showCondensedLayout') === 'true';
|
||||||
|
|
||||||
|
export function Spinner(): JSX.Element | null {
|
||||||
|
const { isCreatingAlertRule } = useCreateAlertState();
|
||||||
|
|
||||||
|
if (!isCreatingAlertRule) return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div className="sticky-page-spinner">
|
||||||
|
<Spin size="large" spinning />
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
.prom-ql-icon {
|
.prom-ql-icon {
|
||||||
height: 14px;
|
height: 14px;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import './QuerySection.styles.scss';
|
import './QuerySection.styles.scss';
|
||||||
|
|
||||||
import { Color } from '@signozhq/design-tokens';
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Button, Tabs, Tooltip } from 'antd';
|
import { Button, Tabs, Tooltip, Typography } from 'antd';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import PromQLIcon from 'assets/Dashboard/PromQl';
|
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||||
@ -71,6 +71,7 @@ function QuerySection({
|
|||||||
<Tooltip title="Query Builder">
|
<Tooltip title="Query Builder">
|
||||||
<Button className="nav-btns">
|
<Button className="nav-btns">
|
||||||
<Atom size={14} />
|
<Atom size={14} />
|
||||||
|
<Typography.Text>Query Builder</Typography.Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
@ -81,6 +82,7 @@ function QuerySection({
|
|||||||
<Tooltip title="ClickHouse">
|
<Tooltip title="ClickHouse">
|
||||||
<Button className="nav-btns">
|
<Button className="nav-btns">
|
||||||
<Terminal size={14} />
|
<Terminal size={14} />
|
||||||
|
<Typography.Text>ClickHouse Query</Typography.Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
@ -95,6 +97,7 @@ function QuerySection({
|
|||||||
<Tooltip title="Query Builder">
|
<Tooltip title="Query Builder">
|
||||||
<Button className="nav-btns" data-testid="query-builder-tab">
|
<Button className="nav-btns" data-testid="query-builder-tab">
|
||||||
<Atom size={14} />
|
<Atom size={14} />
|
||||||
|
<Typography.Text>Query Builder</Typography.Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
@ -105,6 +108,7 @@ function QuerySection({
|
|||||||
<Tooltip title="ClickHouse">
|
<Tooltip title="ClickHouse">
|
||||||
<Button className="nav-btns">
|
<Button className="nav-btns">
|
||||||
<Terminal size={14} />
|
<Terminal size={14} />
|
||||||
|
<Typography.Text>ClickHouse Query</Typography.Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
@ -117,6 +121,7 @@ function QuerySection({
|
|||||||
<PromQLIcon
|
<PromQLIcon
|
||||||
fillColor={isDarkMode ? Color.BG_VANILLA_200 : Color.BG_INK_300}
|
fillColor={isDarkMode ? Color.BG_VANILLA_200 : Color.BG_INK_300}
|
||||||
/>
|
/>
|
||||||
|
<Typography.Text>PromQL</Typography.Text>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
|
|||||||
20
frontend/src/hooks/alerts/useCreateAlertRule.ts
Normal file
20
frontend/src/hooks/alerts/useCreateAlertRule.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import createAlertRule, {
|
||||||
|
CreateAlertRuleResponse,
|
||||||
|
} from 'api/alerts/createAlertRule';
|
||||||
|
import { useMutation, UseMutationResult } from 'react-query';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||||
|
|
||||||
|
export function useCreateAlertRule(): UseMutationResult<
|
||||||
|
SuccessResponse<CreateAlertRuleResponse> | ErrorResponse,
|
||||||
|
Error,
|
||||||
|
PostableAlertRuleV2
|
||||||
|
> {
|
||||||
|
return useMutation<
|
||||||
|
SuccessResponse<CreateAlertRuleResponse> | ErrorResponse,
|
||||||
|
Error,
|
||||||
|
PostableAlertRuleV2
|
||||||
|
>({
|
||||||
|
mutationFn: (alertData) => createAlertRule(alertData),
|
||||||
|
});
|
||||||
|
}
|
||||||
18
frontend/src/hooks/alerts/useTestAlertRule.ts
Normal file
18
frontend/src/hooks/alerts/useTestAlertRule.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import testAlertRule, { TestAlertRuleResponse } from 'api/alerts/testAlertRule';
|
||||||
|
import { useMutation, UseMutationResult } from 'react-query';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||||
|
|
||||||
|
export function useTestAlertRule(): UseMutationResult<
|
||||||
|
SuccessResponse<TestAlertRuleResponse> | ErrorResponse,
|
||||||
|
Error,
|
||||||
|
PostableAlertRuleV2
|
||||||
|
> {
|
||||||
|
return useMutation<
|
||||||
|
SuccessResponse<TestAlertRuleResponse> | ErrorResponse,
|
||||||
|
Error,
|
||||||
|
PostableAlertRuleV2
|
||||||
|
>({
|
||||||
|
mutationFn: (alertData) => testAlertRule(alertData),
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,16 +1,6 @@
|
|||||||
import CreateAlertRule from 'container/CreateAlertRule';
|
import CreateAlertRule from 'container/CreateAlertRule';
|
||||||
import { showNewCreateAlertsPage } from 'container/CreateAlertV2/utils';
|
|
||||||
import { lazy } from 'react';
|
|
||||||
|
|
||||||
const CreateAlertV2 = lazy(() => import('container/CreateAlertV2'));
|
|
||||||
|
|
||||||
function CreateAlertPage(): JSX.Element {
|
function CreateAlertPage(): JSX.Element {
|
||||||
const showNewCreateAlertsPageFlag = showNewCreateAlertsPage();
|
|
||||||
|
|
||||||
if (showNewCreateAlertsPageFlag) {
|
|
||||||
return <CreateAlertV2 />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <CreateAlertRule />;
|
return <CreateAlertRule />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
68
frontend/src/types/api/alerts/alertTypesV2.ts
Normal file
68
frontend/src/types/api/alerts/alertTypesV2.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { AlertTypes } from './alertTypes';
|
||||||
|
import { ICompositeMetricQuery } from './compositeQuery';
|
||||||
|
import { Labels } from './def';
|
||||||
|
|
||||||
|
export interface BasicThreshold {
|
||||||
|
name: string;
|
||||||
|
target: number;
|
||||||
|
matchType: string;
|
||||||
|
op: string;
|
||||||
|
channels: string[];
|
||||||
|
targetUnit: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostableAlertRuleV2 {
|
||||||
|
schemaVersion: string;
|
||||||
|
alert: string;
|
||||||
|
alertType: AlertTypes;
|
||||||
|
ruleType: string;
|
||||||
|
condition: {
|
||||||
|
thresholds?: {
|
||||||
|
kind: string;
|
||||||
|
spec: BasicThreshold[];
|
||||||
|
};
|
||||||
|
compositeQuery: ICompositeMetricQuery;
|
||||||
|
selectedQueryName?: string;
|
||||||
|
alertOnAbsent?: boolean;
|
||||||
|
absentFor?: number;
|
||||||
|
requireMinPoints?: boolean;
|
||||||
|
requiredNumPoints?: number;
|
||||||
|
};
|
||||||
|
evaluation: {
|
||||||
|
kind: 'rolling' | 'cumulative';
|
||||||
|
spec: {
|
||||||
|
evalWindow?: string;
|
||||||
|
frequency: string;
|
||||||
|
schedule?: {
|
||||||
|
type: 'hourly' | 'daily' | 'monthly';
|
||||||
|
minute?: number;
|
||||||
|
hour?: number;
|
||||||
|
day?: number;
|
||||||
|
};
|
||||||
|
timezone?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
labels: Labels;
|
||||||
|
annotations: {
|
||||||
|
description: string;
|
||||||
|
summary: string;
|
||||||
|
};
|
||||||
|
notificationSettings: {
|
||||||
|
notificationGroupBy: string[];
|
||||||
|
renotify?: string;
|
||||||
|
alertStates: string[];
|
||||||
|
notificationPolicy: boolean;
|
||||||
|
};
|
||||||
|
version: string;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AlertRuleV2 extends PostableAlertRuleV2 {
|
||||||
|
schemaVersion: string;
|
||||||
|
state: string;
|
||||||
|
disabled: boolean;
|
||||||
|
createAt: string;
|
||||||
|
createBy: string;
|
||||||
|
updateAt: string;
|
||||||
|
updateBy: string;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user