SagarRajput-7 45d6430ab3
fix: fixed panelType when creating alerts from histogram dashboard (#5251)
* fix: fixed panelType when creating alerts from histogram dashboard

* fix: added PANEL_TYPES.TIME_SERIES always for all type in case of alerts
2024-06-18 13:48:28 +05:30

669 lines
18 KiB
TypeScript

import './FormAlertRules.styles.scss';
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
import {
Button,
Col,
FormInstance,
Modal,
SelectProps,
Tooltip,
Typography,
} from 'antd';
import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { alertHelpMessage } from 'components/facingIssueBtn/util';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEqual } from 'lodash-es';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import {
AlertDef,
defaultEvalWindow,
defaultMatchType,
} from 'types/api/alerts/def';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import BasicInfo from './BasicInfo';
import ChartPreview from './ChartPreview';
import QuerySection from './QuerySection';
import RuleOptions from './RuleOptions';
import {
ActionButton,
ButtonContainer,
MainFormContainer,
PanelContainer,
StepContainer,
StyledLeftContainer,
} from './styles';
import UserGuide from './UserGuide';
import { getSelectedQueryOptions } from './utils';
// eslint-disable-next-line sonarjs/cognitive-complexity
function FormAlertRules({
alertType,
formInstance,
initialValue,
ruleId,
}: FormAlertRuleProps): JSX.Element {
// init namespace for translations
const { t } = useTranslation('alerts');
const { selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const urlQuery = useUrlQuery();
// In case of alert the panel types should always be "Graph" only
const panelType = PANEL_TYPES.TIME_SERIES;
const {
currentQuery,
stagedQuery,
handleRunQuery,
handleSetConfig,
initialDataSource,
redirectWithQueryBuilderData,
} = useQueryBuilder();
useEffect(() => {
handleSetConfig(panelType || PANEL_TYPES.TIME_SERIES, initialDataSource);
}, [handleSetConfig, initialDataSource, panelType]);
// use query client
const ruleCache = useQueryClient();
const isNewRule = ruleId === 0;
const [loading, setLoading] = useState(false);
// alertDef holds the form values to be posted
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
const [yAxisUnit, setYAxisUnit] = useState<string>(currentQuery.unit || '');
useEffect(() => {
if (!isEqual(currentQuery.unit, yAxisUnit)) {
setYAxisUnit(currentQuery.unit || '');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentQuery.unit]);
// initQuery contains initial query when component was mounted
const initQuery = useMemo(() => initialValue.condition.compositeQuery, [
initialValue,
]);
const queryOptions = useMemo(() => {
const queryConfig: Record<EQueryType, () => SelectProps['options']> = {
[EQueryType.QUERY_BUILDER]: () => [
...(getSelectedQueryOptions(currentQuery.builder.queryData) || []),
...(getSelectedQueryOptions(currentQuery.builder.queryFormulas) || []),
],
[EQueryType.PROM]: () => getSelectedQueryOptions(currentQuery.promql),
[EQueryType.CLICKHOUSE]: () =>
getSelectedQueryOptions(currentQuery.clickhouse_sql),
};
return queryConfig[currentQuery.queryType]?.() || [];
}, [currentQuery]);
const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]);
useShareBuilderUrl(sq);
useEffect(() => {
const broadcastToSpecificChannels =
(initialValue &&
initialValue.preferredChannels &&
initialValue.preferredChannels.length > 0) ||
isNewRule;
setAlertDef({
...initialValue,
broadcastToAll: !broadcastToSpecificChannels,
});
}, [initialValue, isNewRule]);
useEffect(() => {
// Set selectedQueryName based on the length of queryOptions
const selectedQueryName = alertDef?.condition?.selectedQueryName;
if (
!selectedQueryName ||
!queryOptions.some((option) => option.value === selectedQueryName)
) {
setAlertDef((def) => ({
...def,
condition: {
...def.condition,
selectedQueryName:
queryOptions.length > 0 ? String(queryOptions[0].value) : undefined,
},
}));
}
}, [alertDef, currentQuery?.queryType, queryOptions]);
const onCancelHandler = useCallback(() => {
urlQuery.delete(QueryParams.compositeQuery);
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
history.replace(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, [urlQuery]);
// onQueryCategoryChange handles changes to query category
// in state as well as sets additional defaults
const onQueryCategoryChange = (val: EQueryType): void => {
const element = document.getElementById('top');
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
if (val === EQueryType.PROM) {
setAlertDef({
...alertDef,
condition: {
...alertDef.condition,
matchType: defaultMatchType,
},
evalWindow: defaultEvalWindow,
});
}
const query: Query = { ...currentQuery, queryType: val };
// update step interval is removed from here as if the user enters
// any value we will use that rather than auto update
redirectWithQueryBuilderData(query);
};
const { notifications } = useNotifications();
const validatePromParams = useCallback((): boolean => {
let retval = true;
if (currentQuery.queryType !== EQueryType.PROM) return retval;
if (!currentQuery.promql || currentQuery.promql.length === 0) {
notifications.error({
message: 'Error',
description: t('promql_required'),
});
return false;
}
currentQuery.promql.forEach((item) => {
if (item.query === '') {
notifications.error({
message: 'Error',
description: t('promql_required'),
});
retval = false;
}
});
return retval;
}, [t, currentQuery, notifications]);
const validateChQueryParams = useCallback((): boolean => {
let retval = true;
if (currentQuery.queryType !== EQueryType.CLICKHOUSE) return retval;
if (
!currentQuery.clickhouse_sql ||
currentQuery.clickhouse_sql.length === 0
) {
notifications.error({
message: 'Error',
description: t('chquery_required'),
});
return false;
}
currentQuery.clickhouse_sql.forEach((item) => {
if (item.query === '') {
notifications.error({
message: 'Error',
description: t('chquery_required'),
});
retval = false;
}
});
return retval;
}, [t, currentQuery, notifications]);
const validateQBParams = useCallback((): boolean => {
if (currentQuery.queryType !== EQueryType.QUERY_BUILDER) return true;
if (
!currentQuery.builder.queryData ||
currentQuery.builder.queryData?.length === 0
) {
notifications.error({
message: 'Error',
description: t('condition_required'),
});
return false;
}
if (alertDef.condition?.target !== 0 && !alertDef.condition?.target) {
notifications.error({
message: 'Error',
description: t('target_missing'),
});
return false;
}
return true;
}, [t, alertDef, currentQuery, notifications]);
const isFormValid = useCallback((): boolean => {
if (!alertDef.alert || alertDef.alert === '') {
return false;
}
if (!validatePromParams()) {
return false;
}
if (!validateChQueryParams()) {
return false;
}
return validateQBParams();
}, [validateQBParams, validateChQueryParams, alertDef, validatePromParams]);
const preparePostData = (): AlertDef => {
const postableAlert: AlertDef = {
...alertDef,
preferredChannels: alertDef.broadcastToAll ? [] : alertDef.preferredChannels,
alertType,
source: window?.location.toString(),
ruleType:
currentQuery.queryType === EQueryType.PROM
? 'promql_rule'
: 'threshold_rule',
condition: {
...alertDef.condition,
compositeQuery: {
builderQueries: {
...mapQueryDataToApi(currentQuery.builder.queryData, 'queryName').data,
...mapQueryDataToApi(currentQuery.builder.queryFormulas, 'queryName')
.data,
},
promQueries: mapQueryDataToApi(currentQuery.promql, 'name').data,
chQueries: mapQueryDataToApi(currentQuery.clickhouse_sql, 'name').data,
queryType: currentQuery.queryType,
panelType: panelType || initQuery.panelType,
unit: currentQuery.unit,
},
},
};
return postableAlert;
};
const memoizedPreparePostData = useCallback(preparePostData, [
currentQuery,
alertDef,
alertType,
initQuery,
panelType,
]);
const isAlertAvailable = useIsFeatureDisabled(
FeatureKeys.QUERY_BUILDER_ALERTS,
);
const saveRule = useCallback(async () => {
if (!isFormValid()) {
return;
}
const postableAlert = memoizedPreparePostData();
setLoading(true);
try {
const apiReq =
ruleId && ruleId > 0
? { data: postableAlert, id: ruleId }
: { data: postableAlert };
const response = await saveAlertApi(apiReq);
if (response.statusCode === 200) {
notifications.success({
message: 'Success',
description:
!ruleId || ruleId === 0 ? t('rule_created') : t('rule_edited'),
});
// invalidate rule in cache
ruleCache.invalidateQueries(['ruleId', ruleId]);
// eslint-disable-next-line sonarjs/no-identical-functions
setTimeout(() => {
urlQuery.delete(QueryParams.compositeQuery);
urlQuery.delete(QueryParams.panelTypes);
urlQuery.delete(QueryParams.ruleId);
urlQuery.delete(QueryParams.relativeTime);
history.replace(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
}, 2000);
} else {
notifications.error({
message: 'Error',
description: response.error || t('unexpected_error'),
});
}
} catch (e) {
notifications.error({
message: 'Error',
description: t('unexpected_error'),
});
}
setLoading(false);
}, [
isFormValid,
memoizedPreparePostData,
ruleId,
notifications,
t,
ruleCache,
urlQuery,
]);
const onSaveHandler = useCallback(async () => {
const content = (
<Typography.Text>
{' '}
{t('confirm_save_content_part1')}{' '}
<QueryTypeTag queryType={currentQuery.queryType} />{' '}
{t('confirm_save_content_part2')}
</Typography.Text>
);
Modal.confirm({
icon: <ExclamationCircleOutlined />,
title: t('confirm_save_title'),
centered: true,
content,
onOk: saveRule,
className: 'create-alert-modal',
});
}, [t, saveRule, currentQuery]);
const onTestRuleHandler = useCallback(async () => {
if (!isFormValid()) {
return;
}
const postableAlert = memoizedPreparePostData();
setLoading(true);
try {
const response = await testAlertApi({ data: postableAlert });
if (response.statusCode === 200) {
const { payload } = response;
if (payload?.alertCount === 0) {
notifications.error({
message: 'Error',
description: t('no_alerts_found'),
});
} else {
notifications.success({
message: 'Success',
description: t('rule_test_fired'),
});
}
} else {
notifications.error({
message: 'Error',
description: response.error || t('unexpected_error'),
});
}
} catch (e) {
notifications.error({
message: 'Error',
description: t('unexpected_error'),
});
}
setLoading(false);
}, [t, isFormValid, memoizedPreparePostData, notifications]);
const renderBasicInfo = (): JSX.Element => (
<BasicInfo
alertDef={alertDef}
setAlertDef={setAlertDef}
isNewRule={isNewRule}
/>
);
const renderQBChartPreview = (): JSX.Element => (
<ChartPreview
headline={
<PlotTag
queryType={currentQuery.queryType}
panelType={panelType || PANEL_TYPES.TIME_SERIES}
/>
}
name=""
query={stagedQuery}
selectedInterval={globalSelectedInterval}
alertDef={alertDef}
yAxisUnit={yAxisUnit || ''}
graphType={panelType || PANEL_TYPES.TIME_SERIES}
/>
);
const renderPromAndChQueryChartPreview = (): JSX.Element => (
<ChartPreview
headline={
<PlotTag
queryType={currentQuery.queryType}
panelType={panelType || PANEL_TYPES.TIME_SERIES}
/>
}
name="Chart Preview"
query={stagedQuery}
alertDef={alertDef}
selectedInterval={globalSelectedInterval}
yAxisUnit={yAxisUnit || ''}
graphType={panelType || PANEL_TYPES.TIME_SERIES}
/>
);
const isAlertNameMissing = !formInstance.getFieldValue('alert');
const isAlertAvailableToSave =
isAlertAvailable &&
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
alertType !== AlertTypes.METRICS_BASED_ALERT;
const onUnitChangeHandler = (value: string): void => {
setYAxisUnit(value);
// reset target unit
setAlertDef((def) => ({
...def,
condition: {
...def.condition,
targetUnit: undefined,
},
}));
};
const isChannelConfigurationValid =
alertDef?.broadcastToAll ||
(alertDef.preferredChannels && alertDef.preferredChannels.length > 0);
const isRuleCreated = !ruleId || ruleId === 0;
function handleRedirection(option: AlertTypes): void {
let url = '';
switch (option) {
case AlertTypes.METRICS_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
break;
case AlertTypes.LOGS_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
break;
case AlertTypes.TRACES_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
break;
case AlertTypes.EXCEPTIONS_BASED_ALERT:
url =
'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
break;
default:
break;
}
window.open(url, '_blank');
}
return (
<>
{Element}
<PanelContainer id="top">
<StyledLeftContainer flex="5 1 600px" md={18}>
<MainFormContainer
initialValues={initialValue}
layout="vertical"
form={formInstance}
className="main-container"
>
{currentQuery.queryType === EQueryType.QUERY_BUILDER &&
renderQBChartPreview()}
{currentQuery.queryType === EQueryType.PROM &&
renderPromAndChQueryChartPreview()}
{currentQuery.queryType === EQueryType.CLICKHOUSE &&
renderPromAndChQueryChartPreview()}
<StepContainer>
<BuilderUnitsFilter
onChange={onUnitChangeHandler}
yAxisUnit={yAxisUnit}
/>
</StepContainer>
<QuerySection
queryCategory={currentQuery.queryType}
setQueryCategory={onQueryCategoryChange}
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
runQuery={(): void => handleRunQuery(true)}
alertDef={alertDef}
panelType={panelType || PANEL_TYPES.TIME_SERIES}
key={currentQuery.queryType}
/>
<RuleOptions
queryCategory={currentQuery.queryType}
alertDef={alertDef}
setAlertDef={setAlertDef}
queryOptions={queryOptions}
/>
{renderBasicInfo()}
<ButtonContainer>
<Tooltip title={isAlertAvailableToSave ? MESSAGE.ALERT : ''}>
<ActionButton
loading={loading || false}
type="primary"
onClick={onSaveHandler}
icon={<SaveOutlined />}
disabled={
isAlertNameMissing ||
isAlertAvailableToSave ||
!isChannelConfigurationValid
}
>
{isNewRule ? t('button_createrule') : t('button_savechanges')}
</ActionButton>
</Tooltip>
<ActionButton
loading={loading || false}
disabled={isAlertNameMissing || !isChannelConfigurationValid}
type="default"
onClick={onTestRuleHandler}
>
{' '}
{t('button_testrule')}
</ActionButton>
<ActionButton
disabled={loading || false}
type="default"
onClick={onCancelHandler}
>
{ruleId === 0 && t('button_cancelchanges')}
{ruleId > 0 && t('button_discard')}
</ActionButton>
</ButtonContainer>
</MainFormContainer>
</StyledLeftContainer>
<Col flex="1 1 300px">
<UserGuide queryType={currentQuery.queryType} />
<div className="info-help-btns">
<Button
style={{ height: 32 }}
onClick={(): void =>
handleRedirection(alertDef?.alertType as AlertTypes)
}
className="doc-redirection-btn"
>
Check an example alert
</Button>
<FacingIssueBtn
attributes={{
alert: alertDef?.alert,
alertType: alertDef?.alertType,
id: ruleId,
ruleType: alertDef?.ruleType,
state: (alertDef as any)?.state,
panelType,
screen: isRuleCreated ? 'Edit Alert' : 'New Alert',
}}
className="facing-issue-btn"
eventName="Alert: Facing Issues in alert"
buttonText="Need help with this alert?"
message={alertHelpMessage(alertDef, ruleId)}
onHoverText="Click here to get help with this alert"
/>
</div>
</Col>
</PanelContainer>
</>
);
}
FormAlertRules.defaultProps = {
alertType: AlertTypes.METRICS_BASED_ALERT,
};
interface FormAlertRuleProps {
alertType?: AlertTypes;
formInstance: FormInstance;
initialValue: AlertDef;
ruleId: number;
}
export default FormAlertRules;