mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-06-20 17:51:21 +08:00
Alert UI with metrics builder (#1359)
* added more changes to query builder * added types for composite queries * (feat): added edit rules and create rules forms * (feat): added chart preview for alert metric ui * (feat): added threshold in chart, translations in alert form and a few fixes * feat: added a link for alert name in list alerts page and source for each rule update Co-authored-by: Pranshu Chittora <pranshu@signoz.io>
This commit is contained in:
parent
3a287b2b16
commit
a8c7237bbb
@ -44,6 +44,7 @@
|
||||
"babel-preset-react-app": "^10.0.0",
|
||||
"chart.js": "^3.4.0",
|
||||
"chartjs-adapter-date-fns": "^2.0.0",
|
||||
"chartjs-plugin-annotation": "^1.4.0",
|
||||
"color": "^4.2.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "4.3.0",
|
||||
|
62
frontend/public/locales/en-GB/rules.json
Normal file
62
frontend/public/locales/en-GB/rules.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
|
||||
"preview_chart_threshold_label": "Threshold",
|
||||
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
|
||||
"button_yes": "Yes",
|
||||
"button_no": "No",
|
||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||
"remove_label_success": "Labels cleared",
|
||||
"alert_form_step1": "Step 1 - Define the metric",
|
||||
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||
"confirm_save_title": "Save Changes",
|
||||
"confirm_save_content_part1": "Your alert built with",
|
||||
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
|
||||
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
|
||||
"rule_created": "Rule created successfully",
|
||||
"rule_edited": "Rule edited successfully",
|
||||
"expression_missing": "expression is missing in {{where}}",
|
||||
"metricname_missing": "metric name is missing in {{where}}",
|
||||
"condition_required": "at least one metric condition is required",
|
||||
"alertname_required": "alert name is required",
|
||||
"promql_required": "promql expression is required when query format is set to PromQL",
|
||||
"button_savechanges": "Save Rule",
|
||||
"button_createrule": "Create Rule",
|
||||
"button_returntorules": "Return to rules",
|
||||
"button_cancelchanges": "Cancel",
|
||||
"button_discard": "Discard",
|
||||
"text_condition1": "Send a notification when the metric is",
|
||||
"text_condition2": "the threshold",
|
||||
"text_condition3": "during the last",
|
||||
"option_5min": "5 mins",
|
||||
"option_10min": "10 mins",
|
||||
"option_15min": "15 mins",
|
||||
"option_60min": "60 mins",
|
||||
"option_24hours": "24 hours",
|
||||
"field_threshold": "Alert Threshold",
|
||||
"option_allthetimes": "all the times",
|
||||
"option_atleastonce": "at least once",
|
||||
"option_onaverage": "on average",
|
||||
"option_intotal": "in total",
|
||||
"option_above": "above",
|
||||
"option_below": "below",
|
||||
"option_equal": "is equal to",
|
||||
"option_notequal": "not equal to",
|
||||
"button_query": "Query",
|
||||
"button_formula": "Formula",
|
||||
"tab_qb": "Query Builder",
|
||||
"tab_promql": "PromQL",
|
||||
"title_confirm": "Confirm",
|
||||
"button_ok": "Yes",
|
||||
"button_cancel": "No",
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
"option_critical": "Critical",
|
||||
"option_error": "Error",
|
||||
"option_warning": "Warning",
|
||||
"option_info": "Info"
|
||||
}
|
62
frontend/public/locales/en/rules.json
Normal file
62
frontend/public/locales/en/rules.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"preview_chart_unexpected_error": "An unexpeced error occurred updating the chart, please check your query.",
|
||||
"preview_chart_threshold_label": "Threshold",
|
||||
"placeholder_label_key_pair": "Click here to enter a label (key value pairs)",
|
||||
"button_yes": "Yes",
|
||||
"button_no": "No",
|
||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||
"remove_label_success": "Labels cleared",
|
||||
"alert_form_step1": "Step 1 - Define the metric",
|
||||
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||
"confirm_save_title": "Save Changes",
|
||||
"confirm_save_content_part1": "Your alert built with",
|
||||
"confirm_save_content_part2": "query will be saved. Press OK to confirm.",
|
||||
"unexpected_error": "Sorry, an unexpected error occurred. Please contact your admin",
|
||||
"rule_created": "Rule created successfully",
|
||||
"rule_edited": "Rule edited successfully",
|
||||
"expression_missing": "expression is missing in {{where}}",
|
||||
"metricname_missing": "metric name is missing in {{where}}",
|
||||
"condition_required": "at least one metric condition is required",
|
||||
"alertname_required": "alert name is required",
|
||||
"promql_required": "promql expression is required when query format is set to PromQL",
|
||||
"button_savechanges": "Save Rule",
|
||||
"button_createrule": "Create Rule",
|
||||
"button_returntorules": "Return to rules",
|
||||
"button_cancelchanges": "Cancel",
|
||||
"button_discard": "Discard",
|
||||
"text_condition1": "Send a notification when the metric is",
|
||||
"text_condition2": "the threshold",
|
||||
"text_condition3": "during the last",
|
||||
"option_5min": "5 mins",
|
||||
"option_10min": "10 mins",
|
||||
"option_15min": "15 mins",
|
||||
"option_60min": "60 mins",
|
||||
"option_24hours": "24 hours",
|
||||
"field_threshold": "Alert Threshold",
|
||||
"option_allthetimes": "all the times",
|
||||
"option_atleastonce": "at least once",
|
||||
"option_onaverage": "on average",
|
||||
"option_intotal": "in total",
|
||||
"option_above": "above",
|
||||
"option_below": "below",
|
||||
"option_equal": "is equal to",
|
||||
"option_notequal": "not equal to",
|
||||
"button_query": "Query",
|
||||
"button_formula": "Formula",
|
||||
"tab_qb": "Query Builder",
|
||||
"tab_promql": "PromQL",
|
||||
"title_confirm": "Confirm",
|
||||
"button_ok": "Yes",
|
||||
"button_cancel": "No",
|
||||
"field_promql_expr": "PromQL Expression",
|
||||
"field_alert_name": "Alert Name",
|
||||
"field_alert_desc": "Alert Description",
|
||||
"field_labels": "Labels",
|
||||
"field_severity": "Severity",
|
||||
"option_critical": "Critical",
|
||||
"option_error": "Error",
|
||||
"option_warning": "Warning",
|
||||
"option_info": "Info"
|
||||
}
|
@ -9,7 +9,7 @@ const create = async (
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/rules', {
|
||||
data: props.query,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -14,7 +14,7 @@ const get = async (
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
|
@ -2,14 +2,14 @@ import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/put';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/save';
|
||||
|
||||
const put = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`/rules/${props.id}`, {
|
||||
data: props.data,
|
||||
...props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
|
17
frontend/src/api/alerts/save.ts
Normal file
17
frontend/src/api/alerts/save.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/save';
|
||||
|
||||
import create from './create';
|
||||
import put from './put';
|
||||
|
||||
const save = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
if (props.id && props.id > 0) {
|
||||
return put({ ...props });
|
||||
}
|
||||
|
||||
return create({ ...props });
|
||||
};
|
||||
|
||||
export default save;
|
@ -22,6 +22,7 @@ import {
|
||||
Tooltip,
|
||||
} from 'chart.js';
|
||||
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@ -50,6 +51,7 @@ Chart.register(
|
||||
SubTitle,
|
||||
BarController,
|
||||
BarElement,
|
||||
annotationPlugin,
|
||||
);
|
||||
|
||||
function Graph({
|
||||
@ -62,6 +64,7 @@ function Graph({
|
||||
name,
|
||||
yAxisUnit = 'short',
|
||||
forceReRender,
|
||||
staticLine,
|
||||
}: GraphProps): JSX.Element {
|
||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||
@ -99,6 +102,30 @@ function Graph({
|
||||
intersect: false,
|
||||
},
|
||||
plugins: {
|
||||
annotation: staticLine
|
||||
? {
|
||||
annotations: [
|
||||
{
|
||||
type: 'line',
|
||||
yMin: staticLine.yMin,
|
||||
yMax: staticLine.yMax,
|
||||
borderColor: staticLine.borderColor,
|
||||
borderWidth: staticLine.borderWidth,
|
||||
label: {
|
||||
content: staticLine.lineText,
|
||||
enabled: true,
|
||||
font: {
|
||||
size: 10,
|
||||
},
|
||||
borderWidth: 0,
|
||||
position: 'start',
|
||||
backgroundColor: 'transparent',
|
||||
color: staticLine.textColor,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
title: {
|
||||
display: title !== undefined,
|
||||
text: title,
|
||||
@ -180,6 +207,7 @@ function Graph({
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const chartHasData = hasData(data);
|
||||
const chartPlugins = [];
|
||||
|
||||
@ -205,6 +233,7 @@ function Graph({
|
||||
name,
|
||||
yAxisUnit,
|
||||
onClickHandler,
|
||||
staticLine,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -229,6 +258,16 @@ interface GraphProps {
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
forceReRender?: boolean | null | number;
|
||||
staticLine?: StaticLineProps | undefined;
|
||||
}
|
||||
|
||||
export interface StaticLineProps {
|
||||
yMin: number | undefined;
|
||||
yMax: number | undefined;
|
||||
borderColor: string;
|
||||
borderWidth: number;
|
||||
lineText: string;
|
||||
textColor: string;
|
||||
}
|
||||
|
||||
export type GraphOnClickHandler = (
|
||||
@ -245,5 +284,6 @@ Graph.defaultProps = {
|
||||
onClickHandler: undefined,
|
||||
yAxisUnit: undefined,
|
||||
forceReRender: undefined,
|
||||
staticLine: undefined,
|
||||
};
|
||||
export default Graph;
|
||||
|
22
frontend/src/container/CreateAlertRule/index.tsx
Normal file
22
frontend/src/container/CreateAlertRule/index.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Form } from 'antd';
|
||||
import FormAlertRules from 'container/FormAlertRules';
|
||||
import React from 'react';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
function CreateRules({ initialValue }: CreateRulesProps): JSX.Element {
|
||||
const [formInstance] = Form.useForm();
|
||||
|
||||
return (
|
||||
<FormAlertRules
|
||||
formInstance={formInstance}
|
||||
initialValue={initialValue}
|
||||
ruleId={0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface CreateRulesProps {
|
||||
initialValue: AlertDef;
|
||||
}
|
||||
|
||||
export default CreateRules;
|
@ -1,102 +1,23 @@
|
||||
import { SaveFilled } from '@ant-design/icons';
|
||||
import { Button, notification } from 'antd';
|
||||
import put from 'api/alerts/put';
|
||||
import Editor from 'components/Editor';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { State } from 'hooks/useFetch';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { PayloadProps } from 'types/api/alerts/get';
|
||||
import { PayloadProps as PutPayloadProps } from 'types/api/alerts/put';
|
||||
import { Form } from 'antd';
|
||||
import FormAlertRules from 'container/FormAlertRules';
|
||||
import React from 'react';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
import { ButtonContainer } from './styles';
|
||||
|
||||
function EditRules({ initialData, ruleId }: EditRulesProps): JSX.Element {
|
||||
const [value, setEditorValue] = useState<string>(initialData);
|
||||
const [notifications, Element] = notification.useNotification();
|
||||
const [editButtonState, setEditButtonState] = useState<State<PutPayloadProps>>(
|
||||
{
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: false,
|
||||
success: false,
|
||||
payload: undefined,
|
||||
},
|
||||
);
|
||||
|
||||
const onClickHandler = useCallback(async () => {
|
||||
try {
|
||||
setEditButtonState((state) => ({
|
||||
...state,
|
||||
loading: true,
|
||||
}));
|
||||
const response = await put({
|
||||
data: value,
|
||||
id: parseInt(ruleId, 10),
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
setEditButtonState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
payload: response.payload,
|
||||
}));
|
||||
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: 'Congrats. The alert was Edited correctly.',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
history.push(ROUTES.LIST_ALL_ALERT);
|
||||
}, 2000);
|
||||
} else {
|
||||
setEditButtonState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
errorMessage: response.error || 'Something went wrong',
|
||||
error: true,
|
||||
}));
|
||||
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description:
|
||||
response.error ||
|
||||
'Oops! Some issue occured in editing the alert please try again or contact support@signoz.io',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description:
|
||||
'Oops! Some issue occured in editing the alert please try again or contact support@signoz.io',
|
||||
});
|
||||
}
|
||||
}, [value, ruleId, notifications]);
|
||||
function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
|
||||
const [formInstance] = Form.useForm();
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
|
||||
<Editor onChange={(value): void => setEditorValue(value)} value={value} />
|
||||
|
||||
<ButtonContainer>
|
||||
<Button
|
||||
loading={editButtonState.loading || false}
|
||||
disabled={editButtonState.loading || false}
|
||||
icon={<SaveFilled />}
|
||||
onClick={onClickHandler}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
</>
|
||||
<FormAlertRules
|
||||
formInstance={formInstance}
|
||||
initialValue={initialValue}
|
||||
ruleId={ruleId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface EditRulesProps {
|
||||
initialData: PayloadProps['data'];
|
||||
ruleId: string;
|
||||
initialValue: AlertDef;
|
||||
ruleId: number;
|
||||
}
|
||||
|
||||
export default EditRules;
|
||||
|
101
frontend/src/container/FormAlertRules/BasicInfo.tsx
Normal file
101
frontend/src/container/FormAlertRules/BasicInfo.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import { Select } from 'antd';
|
||||
import FormItem from 'antd/lib/form/FormItem';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AlertDef, Labels } from 'types/api/alerts/def';
|
||||
|
||||
import LabelSelect from './labels';
|
||||
import {
|
||||
FormContainer,
|
||||
InputSmall,
|
||||
SeveritySelect,
|
||||
StepHeading,
|
||||
TextareaMedium,
|
||||
} from './styles';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
interface BasicInfoProps {
|
||||
alertDef: AlertDef;
|
||||
setAlertDef: (a: AlertDef) => void;
|
||||
}
|
||||
|
||||
function BasicInfo({ alertDef, setAlertDef }: BasicInfoProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('rules');
|
||||
|
||||
return (
|
||||
<>
|
||||
<StepHeading> {t('alert_form_step3')} </StepHeading>
|
||||
<FormContainer>
|
||||
<FormItem
|
||||
label={t('field_severity')}
|
||||
labelAlign="left"
|
||||
name={['labels', 'severity']}
|
||||
>
|
||||
<SeveritySelect
|
||||
defaultValue="critical"
|
||||
onChange={(value: unknown | string): void => {
|
||||
const s = (value as string) || 'critical';
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
labels: {
|
||||
...alertDef.labels,
|
||||
severity: s,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Option value="critical">{t('option_critical')}</Option>
|
||||
<Option value="error">{t('option_error')}</Option>
|
||||
<Option value="warning">{t('option_warning')}</Option>
|
||||
<Option value="info">{t('option_info')}</Option>
|
||||
</SeveritySelect>
|
||||
</FormItem>
|
||||
|
||||
<FormItem label={t('field_alert_name')} labelAlign="left" name="alert">
|
||||
<InputSmall
|
||||
onChange={(e): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
alert: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
label={t('field_alert_desc')}
|
||||
labelAlign="left"
|
||||
name={['annotations', 'description']}
|
||||
>
|
||||
<TextareaMedium
|
||||
onChange={(e): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
annotations: {
|
||||
...alertDef.annotations,
|
||||
description: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label={t('field_labels')}>
|
||||
<LabelSelect
|
||||
onSetLabels={(l: Labels): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
labels: {
|
||||
...l,
|
||||
},
|
||||
});
|
||||
}}
|
||||
initialValues={alertDef.labels}
|
||||
/>
|
||||
</FormItem>
|
||||
</FormContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default BasicInfo;
|
119
frontend/src/container/FormAlertRules/ChartPreview/index.tsx
Normal file
119
frontend/src/container/FormAlertRules/ChartPreview/index.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { StaticLineProps } from 'components/Graph';
|
||||
import GridGraphComponent from 'container/GridGraphComponent';
|
||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { GetMetricQueryRange } from 'store/actions/dashboard/getQueryResults';
|
||||
import { Query } from 'types/api/dashboard/getAll';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { ChartContainer, FailedMessageContainer } from './styles';
|
||||
|
||||
export interface ChartPreviewProps {
|
||||
name: string;
|
||||
query: Query | undefined;
|
||||
graphType?: GRAPH_TYPES;
|
||||
selectedTime?: timePreferenceType;
|
||||
selectedInterval?: Time;
|
||||
headline?: JSX.Element;
|
||||
threshold?: number;
|
||||
}
|
||||
|
||||
function ChartPreview({
|
||||
name,
|
||||
query,
|
||||
graphType = 'TIME_SERIES',
|
||||
selectedTime = 'GLOBAL_TIME',
|
||||
selectedInterval = '5min',
|
||||
headline,
|
||||
threshold,
|
||||
}: ChartPreviewProps): JSX.Element | null {
|
||||
const { t } = useTranslation('rules');
|
||||
const staticLine: StaticLineProps | undefined =
|
||||
threshold && threshold > 0
|
||||
? {
|
||||
yMin: threshold,
|
||||
yMax: threshold,
|
||||
borderColor: '#f14',
|
||||
borderWidth: 1,
|
||||
lineText: `${t('preview_chart_threshold_label')} (y=${threshold})`,
|
||||
textColor: '#f14',
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const queryKey = JSON.stringify(query);
|
||||
const queryResponse = useQuery({
|
||||
queryKey: ['chartPreview', queryKey, selectedInterval],
|
||||
queryFn: () =>
|
||||
GetMetricQueryRange({
|
||||
query: query || {
|
||||
queryType: 1,
|
||||
promQL: [],
|
||||
metricsBuilder: {
|
||||
formulas: [],
|
||||
queryBuilder: [],
|
||||
},
|
||||
clickHouse: [],
|
||||
},
|
||||
globalSelectedInterval: selectedInterval,
|
||||
graphType,
|
||||
selectedTime,
|
||||
}),
|
||||
enabled:
|
||||
query != null &&
|
||||
(query.queryType !== EQueryType.PROM ||
|
||||
(query.promQL?.length > 0 && query.promQL[0].query !== '')),
|
||||
});
|
||||
|
||||
const chartDataSet = queryResponse.isError
|
||||
? null
|
||||
: getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: queryResponse?.data?.payload?.data?.result
|
||||
? queryResponse?.data?.payload?.data?.result
|
||||
: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return (
|
||||
<ChartContainer>
|
||||
{headline}
|
||||
{(queryResponse?.data?.error || queryResponse?.isError) && (
|
||||
<FailedMessageContainer color="red" title="Failed to refresh the chart">
|
||||
<InfoCircleOutlined />{' '}
|
||||
{queryResponse?.data?.error ||
|
||||
queryResponse?.error ||
|
||||
t('preview_chart_unexpected_error')}
|
||||
</FailedMessageContainer>
|
||||
)}
|
||||
|
||||
{chartDataSet && !queryResponse.isError && (
|
||||
<GridGraphComponent
|
||||
title={name}
|
||||
data={chartDataSet}
|
||||
isStacked
|
||||
GRAPH_TYPES={graphType || 'TIME_SERIES'}
|
||||
name={name || 'Chart Preview'}
|
||||
staticLine={staticLine}
|
||||
/>
|
||||
)}
|
||||
</ChartContainer>
|
||||
);
|
||||
}
|
||||
|
||||
ChartPreview.defaultProps = {
|
||||
graphType: 'TIME_SERIES',
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
selectedInterval: '5min',
|
||||
headline: undefined,
|
||||
threshold: 0,
|
||||
};
|
||||
|
||||
export default ChartPreview;
|
28
frontend/src/container/FormAlertRules/ChartPreview/styles.ts
Normal file
28
frontend/src/container/FormAlertRules/ChartPreview/styles.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Card, Tooltip } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const NotFoundContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 55vh;
|
||||
`;
|
||||
|
||||
export const FailedMessageContainer = styled(Tooltip)`
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
`;
|
||||
|
||||
export const ChartContainer = styled(Card)`
|
||||
border-radius: 4px;
|
||||
&&& {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 1.5rem 0;
|
||||
height: 57vh;
|
||||
/* padding-bottom: 2rem; */
|
||||
}
|
||||
`;
|
49
frontend/src/container/FormAlertRules/PromqlSection.tsx
Normal file
49
frontend/src/container/FormAlertRules/PromqlSection.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import PromQLQueryBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/query';
|
||||
import { IPromQLQueryHandleChange } from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/types';
|
||||
import React from 'react';
|
||||
import { IPromQueries } from 'types/api/alerts/compositeQuery';
|
||||
|
||||
function PromqlSection({
|
||||
promQueries,
|
||||
setPromQueries,
|
||||
}: PromqlSectionProps): JSX.Element {
|
||||
const handlePromQLQueryChange = ({
|
||||
query,
|
||||
legend,
|
||||
toggleDelete,
|
||||
}: IPromQLQueryHandleChange): void => {
|
||||
let promQuery = promQueries.A;
|
||||
|
||||
// todo(amol): how to remove query, make it null?
|
||||
if (query) promQuery.query = query;
|
||||
if (legend) promQuery.legend = legend;
|
||||
if (toggleDelete) {
|
||||
promQuery = {
|
||||
query: '',
|
||||
legend: '',
|
||||
name: 'A',
|
||||
disabled: false,
|
||||
};
|
||||
}
|
||||
setPromQueries({
|
||||
A: {
|
||||
...promQuery,
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<PromQLQueryBuilder
|
||||
key="A"
|
||||
queryIndex="A"
|
||||
queryData={{ ...promQueries?.A, name: 'A' }}
|
||||
handleQueryChange={handlePromQLQueryChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface PromqlSectionProps {
|
||||
promQueries: IPromQueries;
|
||||
setPromQueries: (p: IPromQueries) => void;
|
||||
}
|
||||
|
||||
export default PromqlSection;
|
288
frontend/src/container/FormAlertRules/QuerySection.tsx
Normal file
288
frontend/src/container/FormAlertRules/QuerySection.tsx
Normal file
@ -0,0 +1,288 @@
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { notification, Tabs } from 'antd';
|
||||
import MetricsBuilderFormula from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/formula';
|
||||
import MetricsBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/query';
|
||||
import {
|
||||
IQueryBuilderFormulaHandleChange,
|
||||
IQueryBuilderQueryHandleChange,
|
||||
} from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/queryBuilder/types';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
IFormulaQueries,
|
||||
IMetricQueries,
|
||||
IPromQueries,
|
||||
} from 'types/api/alerts/compositeQuery';
|
||||
import { EAggregateOperator, EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import PromqlSection from './PromqlSection';
|
||||
import { FormContainer, QueryButton, StepHeading } from './styles';
|
||||
import { toIMetricsBuilderQuery } from './utils';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
function QuerySection({
|
||||
queryCategory,
|
||||
setQueryCategory,
|
||||
metricQueries,
|
||||
setMetricQueries,
|
||||
formulaQueries,
|
||||
setFormulaQueries,
|
||||
promQueries,
|
||||
setPromQueries,
|
||||
}: QuerySectionProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('rules');
|
||||
|
||||
const handleQueryCategoryChange = (s: string): void => {
|
||||
if (
|
||||
parseInt(s, 10) === EQueryType.PROM &&
|
||||
(!promQueries || Object.keys(promQueries).length === 0)
|
||||
) {
|
||||
setPromQueries({
|
||||
A: {
|
||||
query: '',
|
||||
stats: '',
|
||||
name: 'A',
|
||||
legend: '',
|
||||
disabled: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setQueryCategory(parseInt(s, 10));
|
||||
};
|
||||
|
||||
const getNextQueryLabel = useCallback((): string => {
|
||||
let maxAscii = 0;
|
||||
|
||||
Object.keys(metricQueries).forEach((key) => {
|
||||
const n = key.charCodeAt(0);
|
||||
if (n > maxAscii) {
|
||||
maxAscii = n - 64;
|
||||
}
|
||||
});
|
||||
|
||||
return String.fromCharCode(64 + maxAscii + 1);
|
||||
}, [metricQueries]);
|
||||
|
||||
const handleFormulaChange = ({
|
||||
formulaIndex,
|
||||
expression,
|
||||
toggleDisable,
|
||||
toggleDelete,
|
||||
}: IQueryBuilderFormulaHandleChange): void => {
|
||||
const allFormulas = formulaQueries;
|
||||
const current = allFormulas[formulaIndex];
|
||||
if (expression) {
|
||||
current.expression = expression;
|
||||
}
|
||||
|
||||
if (toggleDisable) {
|
||||
current.disabled = !current.disabled;
|
||||
}
|
||||
|
||||
if (toggleDelete) {
|
||||
delete allFormulas[formulaIndex];
|
||||
} else {
|
||||
allFormulas[formulaIndex] = current;
|
||||
}
|
||||
|
||||
setFormulaQueries({
|
||||
...allFormulas,
|
||||
});
|
||||
};
|
||||
|
||||
const handleMetricQueryChange = ({
|
||||
queryIndex,
|
||||
aggregateFunction,
|
||||
metricName,
|
||||
tagFilters,
|
||||
groupBy,
|
||||
legend,
|
||||
toggleDisable,
|
||||
toggleDelete,
|
||||
}: IQueryBuilderQueryHandleChange): void => {
|
||||
const allQueries = metricQueries;
|
||||
const current = metricQueries[queryIndex];
|
||||
if (aggregateFunction) {
|
||||
current.aggregateOperator = aggregateFunction;
|
||||
}
|
||||
if (metricName) {
|
||||
current.metricName = metricName;
|
||||
}
|
||||
|
||||
if (tagFilters && current.tagFilters) {
|
||||
current.tagFilters.items = tagFilters;
|
||||
}
|
||||
|
||||
if (legend) {
|
||||
current.legend = legend;
|
||||
}
|
||||
|
||||
if (groupBy) {
|
||||
current.groupBy = groupBy;
|
||||
}
|
||||
|
||||
if (toggleDisable) {
|
||||
current.disabled = !current.disabled;
|
||||
}
|
||||
|
||||
if (toggleDelete) {
|
||||
delete allQueries[queryIndex];
|
||||
} else {
|
||||
allQueries[queryIndex] = current;
|
||||
}
|
||||
|
||||
setMetricQueries({
|
||||
...allQueries,
|
||||
});
|
||||
};
|
||||
|
||||
const addMetricQuery = useCallback(() => {
|
||||
if (Object.keys(metricQueries).length > 5) {
|
||||
notification.error({
|
||||
message: t('metric_query_max_limit'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const queryLabel = getNextQueryLabel();
|
||||
|
||||
const queries = metricQueries;
|
||||
queries[queryLabel] = {
|
||||
name: queryLabel,
|
||||
queryName: queryLabel,
|
||||
metricName: '',
|
||||
formulaOnly: false,
|
||||
aggregateOperator: EAggregateOperator.NOOP,
|
||||
legend: '',
|
||||
tagFilters: {
|
||||
op: 'AND',
|
||||
items: [],
|
||||
},
|
||||
groupBy: [],
|
||||
disabled: false,
|
||||
expression: queryLabel,
|
||||
};
|
||||
setMetricQueries({ ...queries });
|
||||
}, [t, getNextQueryLabel, metricQueries, setMetricQueries]);
|
||||
|
||||
const addFormula = useCallback(() => {
|
||||
// defaulting to F1 as only one formula is supported
|
||||
// in alert definition
|
||||
const queryLabel = 'F1';
|
||||
|
||||
const formulas = formulaQueries;
|
||||
formulas[queryLabel] = {
|
||||
queryName: queryLabel,
|
||||
name: queryLabel,
|
||||
formulaOnly: true,
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
setFormulaQueries({ ...formulas });
|
||||
}, [formulaQueries, setFormulaQueries]);
|
||||
|
||||
const renderPromqlUI = (): JSX.Element => {
|
||||
return (
|
||||
<PromqlSection promQueries={promQueries} setPromQueries={setPromQueries} />
|
||||
);
|
||||
};
|
||||
|
||||
const renderFormulaButton = (): JSX.Element => {
|
||||
return (
|
||||
<QueryButton onClick={addFormula} icon={<PlusOutlined />}>
|
||||
{t('button_formula')}
|
||||
</QueryButton>
|
||||
);
|
||||
};
|
||||
|
||||
const renderQueryButton = (): JSX.Element => {
|
||||
return (
|
||||
<QueryButton onClick={addMetricQuery} icon={<PlusOutlined />}>
|
||||
{t('button_query')}
|
||||
</QueryButton>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMetricUI = (): JSX.Element => {
|
||||
return (
|
||||
<div>
|
||||
{metricQueries &&
|
||||
Object.keys(metricQueries).map((key: string) => {
|
||||
// todo(amol): need to handle this in fetch
|
||||
const current = metricQueries[key];
|
||||
current.name = key;
|
||||
|
||||
return (
|
||||
<MetricsBuilder
|
||||
key={key}
|
||||
queryIndex={key}
|
||||
queryData={toIMetricsBuilderQuery(current)}
|
||||
selectedGraph="TIME_SERIES"
|
||||
handleQueryChange={handleMetricQueryChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{queryCategory !== EQueryType.PROM && renderQueryButton()}
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
{formulaQueries &&
|
||||
Object.keys(formulaQueries).map((key: string) => {
|
||||
// todo(amol): need to handle this in fetch
|
||||
const current = formulaQueries[key];
|
||||
current.name = key;
|
||||
|
||||
return (
|
||||
<MetricsBuilderFormula
|
||||
key={key}
|
||||
formulaIndex={key}
|
||||
formulaData={current}
|
||||
handleFormulaChange={handleFormulaChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{queryCategory === EQueryType.QUERY_BUILDER &&
|
||||
(!formulaQueries || Object.keys(formulaQueries).length === 0) &&
|
||||
metricQueries &&
|
||||
Object.keys(metricQueries).length > 0 &&
|
||||
renderFormulaButton()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<StepHeading> {t('alert_form_step1')}</StepHeading>
|
||||
<FormContainer>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Tabs
|
||||
type="card"
|
||||
style={{ width: '100%' }}
|
||||
defaultActiveKey={EQueryType.QUERY_BUILDER.toString()}
|
||||
activeKey={queryCategory.toString()}
|
||||
onChange={handleQueryCategoryChange}
|
||||
>
|
||||
<TabPane tab={t('tab_qb')} key={EQueryType.QUERY_BUILDER.toString()} />
|
||||
<TabPane tab={t('tab_promql')} key={EQueryType.PROM.toString()} />
|
||||
</Tabs>
|
||||
</div>
|
||||
{queryCategory === EQueryType.PROM ? renderPromqlUI() : renderMetricUI()}
|
||||
</FormContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface QuerySectionProps {
|
||||
queryCategory: EQueryType;
|
||||
setQueryCategory: (n: EQueryType) => void;
|
||||
metricQueries: IMetricQueries;
|
||||
setMetricQueries: (b: IMetricQueries) => void;
|
||||
formulaQueries: IFormulaQueries;
|
||||
setFormulaQueries: (b: IFormulaQueries) => void;
|
||||
promQueries: IPromQueries;
|
||||
setPromQueries: (p: IPromQueries) => void;
|
||||
}
|
||||
|
||||
export default QuerySection;
|
174
frontend/src/container/FormAlertRules/RuleOptions.tsx
Normal file
174
frontend/src/container/FormAlertRules/RuleOptions.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
import { Select, Typography } from 'antd';
|
||||
import FormItem from 'antd/lib/form/FormItem';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
AlertDef,
|
||||
defaultCompareOp,
|
||||
defaultEvalWindow,
|
||||
defaultMatchType,
|
||||
} from 'types/api/alerts/def';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import {
|
||||
FormContainer,
|
||||
InlineSelect,
|
||||
StepHeading,
|
||||
ThresholdInput,
|
||||
} from './styles';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
function RuleOptions({
|
||||
alertDef,
|
||||
setAlertDef,
|
||||
queryCategory,
|
||||
}: RuleOptionsProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('rules');
|
||||
|
||||
const handleMatchOptChange = (value: string | unknown): void => {
|
||||
const m = (value as string) || alertDef.condition?.matchType;
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
condition: {
|
||||
...alertDef.condition,
|
||||
matchType: m,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const renderCompareOps = (): JSX.Element => {
|
||||
return (
|
||||
<InlineSelect
|
||||
defaultValue={defaultCompareOp}
|
||||
value={alertDef.condition?.op}
|
||||
style={{ minWidth: '120px' }}
|
||||
onChange={(value: string | unknown): void => {
|
||||
const newOp = (value as string) || '';
|
||||
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
condition: {
|
||||
...alertDef.condition,
|
||||
op: newOp,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Option value="1">{t('option_above')}</Option>
|
||||
<Option value="2">{t('option_below')}</Option>
|
||||
<Option value="3">{t('option_equal')}</Option>
|
||||
<Option value="4">{t('option_notequal')}</Option>
|
||||
</InlineSelect>
|
||||
);
|
||||
};
|
||||
|
||||
const renderThresholdMatchOpts = (): JSX.Element => {
|
||||
return (
|
||||
<InlineSelect
|
||||
defaultValue={defaultMatchType}
|
||||
style={{ minWidth: '130px' }}
|
||||
value={alertDef.condition?.matchType}
|
||||
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
|
||||
>
|
||||
<Option value="1">{t('option_atleastonce')}</Option>
|
||||
<Option value="2">{t('option_allthetimes')}</Option>
|
||||
<Option value="3">{t('option_onaverage')}</Option>
|
||||
<Option value="4">{t('option_intotal')}</Option>
|
||||
</InlineSelect>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPromMatchOpts = (): JSX.Element => {
|
||||
return (
|
||||
<InlineSelect
|
||||
defaultValue={defaultMatchType}
|
||||
style={{ minWidth: '130px' }}
|
||||
value={alertDef.condition?.matchType}
|
||||
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
|
||||
>
|
||||
<Option value="1">{t('option_atleastonce')}</Option>
|
||||
</InlineSelect>
|
||||
);
|
||||
};
|
||||
|
||||
const renderEvalWindows = (): JSX.Element => {
|
||||
return (
|
||||
<InlineSelect
|
||||
defaultValue={defaultEvalWindow}
|
||||
style={{ minWidth: '120px' }}
|
||||
value={alertDef.evalWindow}
|
||||
onChange={(value: string | unknown): void => {
|
||||
const ew = (value as string) || alertDef.evalWindow;
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
evalWindow: ew,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
<Option value="5m0s">{t('option_5min')}</Option>
|
||||
<Option value="10m0s">{t('option_10min')}</Option>
|
||||
<Option value="15m0s">{t('option_15min')}</Option>
|
||||
<Option value="60m0s">{t('option_60min')}</Option>
|
||||
<Option value="1440m0s">{t('option_24hours')}</Option>
|
||||
</InlineSelect>
|
||||
);
|
||||
};
|
||||
|
||||
const renderThresholdRuleOpts = (): JSX.Element => {
|
||||
return (
|
||||
<FormItem>
|
||||
<Typography.Text>
|
||||
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
|
||||
{renderThresholdMatchOpts()} {t('text_condition3')} {renderEvalWindows()}
|
||||
</Typography.Text>
|
||||
</FormItem>
|
||||
);
|
||||
};
|
||||
const renderPromRuleOptions = (): JSX.Element => {
|
||||
return (
|
||||
<FormItem>
|
||||
<Typography.Text>
|
||||
{t('text_condition1')} {renderCompareOps()} {t('text_condition2')}{' '}
|
||||
{renderPromMatchOpts()}
|
||||
</Typography.Text>
|
||||
</FormItem>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StepHeading>{t('alert_form_step2')}</StepHeading>
|
||||
<FormContainer>
|
||||
{queryCategory === EQueryType.PROM
|
||||
? renderPromRuleOptions()
|
||||
: renderThresholdRuleOpts()}
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<ThresholdInput
|
||||
controls={false}
|
||||
addonBefore={t('field_threshold')}
|
||||
value={alertDef?.condition?.target}
|
||||
onChange={(value: number | unknown): void => {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
condition: {
|
||||
...alertDef.condition,
|
||||
target: (value as number) || undefined,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface RuleOptionsProps {
|
||||
alertDef: AlertDef;
|
||||
setAlertDef: (a: AlertDef) => void;
|
||||
queryCategory: EQueryType;
|
||||
}
|
||||
export default RuleOptions;
|
366
frontend/src/container/FormAlertRules/index.tsx
Normal file
366
frontend/src/container/FormAlertRules/index.tsx
Normal file
@ -0,0 +1,366 @@
|
||||
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
import { FormInstance, Modal, notification, Typography } from 'antd';
|
||||
import saveAlertApi from 'api/alerts/save';
|
||||
import ROUTES from 'constants/routes';
|
||||
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
|
||||
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import {
|
||||
IFormulaQueries,
|
||||
IMetricQueries,
|
||||
IPromQueries,
|
||||
} from 'types/api/alerts/compositeQuery';
|
||||
import {
|
||||
AlertDef,
|
||||
defaultEvalWindow,
|
||||
defaultMatchType,
|
||||
} from 'types/api/alerts/def';
|
||||
import { Query as StagedQuery } from 'types/api/dashboard/getAll';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import BasicInfo from './BasicInfo';
|
||||
import ChartPreview from './ChartPreview';
|
||||
import QuerySection from './QuerySection';
|
||||
import RuleOptions from './RuleOptions';
|
||||
import { ActionButton, ButtonContainer, MainFormContainer } from './styles';
|
||||
import useDebounce from './useDebounce';
|
||||
import {
|
||||
prepareBuilderQueries,
|
||||
prepareStagedQuery,
|
||||
toChartInterval,
|
||||
toFormulaQueries,
|
||||
toMetricQueries,
|
||||
} from './utils';
|
||||
|
||||
function FormAlertRules({
|
||||
formInstance,
|
||||
initialValue,
|
||||
ruleId,
|
||||
}: FormAlertRuleProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('rules');
|
||||
|
||||
// use query client
|
||||
const ruleCache = useQueryClient();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// alertDef holds the form values to be posted
|
||||
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
|
||||
|
||||
// initQuery contains initial query when component was mounted
|
||||
const initQuery = initialValue?.condition?.compositeMetricQuery;
|
||||
|
||||
const [queryCategory, setQueryCategory] = useState<EQueryType>(
|
||||
initQuery?.queryType,
|
||||
);
|
||||
|
||||
// local state to handle metric queries
|
||||
const [metricQueries, setMetricQueries] = useState<IMetricQueries>(
|
||||
toMetricQueries(initQuery?.builderQueries),
|
||||
);
|
||||
|
||||
// local state to handle formula queries
|
||||
const [formulaQueries, setFormulaQueries] = useState<IFormulaQueries>(
|
||||
toFormulaQueries(initQuery?.builderQueries),
|
||||
);
|
||||
|
||||
// local state to handle promql queries
|
||||
const [promQueries, setPromQueries] = useState<IPromQueries>({
|
||||
...initQuery?.promQueries,
|
||||
});
|
||||
|
||||
// staged query is used to display chart preview
|
||||
const [stagedQuery, setStagedQuery] = useState<StagedQuery>();
|
||||
const debouncedStagedQuery = useDebounce(stagedQuery, 500);
|
||||
|
||||
// this use effect initiates staged query and
|
||||
// other queries based on server data.
|
||||
// useful when fetching of initial values (from api)
|
||||
// is delayed
|
||||
useEffect(() => {
|
||||
const initQuery = initialValue?.condition?.compositeMetricQuery;
|
||||
const typ = initQuery?.queryType;
|
||||
|
||||
// extract metric query from builderQueries
|
||||
const mq = toMetricQueries(initQuery?.builderQueries);
|
||||
|
||||
// extract formula query from builderQueries
|
||||
const fq = toFormulaQueries(initQuery?.builderQueries);
|
||||
|
||||
// prepare staged query
|
||||
const sq = prepareStagedQuery(typ, mq, fq, initQuery?.promQueries);
|
||||
const pq = initQuery?.promQueries;
|
||||
|
||||
setQueryCategory(typ);
|
||||
setMetricQueries(mq);
|
||||
setFormulaQueries(fq);
|
||||
setPromQueries(pq);
|
||||
setStagedQuery(sq);
|
||||
setAlertDef(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
// this useEffect updates staging query when
|
||||
// any of its sub-parameters changes
|
||||
useEffect(() => {
|
||||
// prepare staged query
|
||||
const sq: StagedQuery = prepareStagedQuery(
|
||||
queryCategory,
|
||||
metricQueries,
|
||||
formulaQueries,
|
||||
promQueries,
|
||||
);
|
||||
setStagedQuery(sq);
|
||||
}, [queryCategory, metricQueries, formulaQueries, promQueries]);
|
||||
|
||||
const onCancelHandler = useCallback(() => {
|
||||
history.replace(ROUTES.LIST_ALL_ALERT);
|
||||
}, []);
|
||||
|
||||
// onQueryCategoryChange handles changes to query category
|
||||
// in state as well as sets additional defaults
|
||||
const onQueryCategoryChange = (val: EQueryType): void => {
|
||||
setQueryCategory(val);
|
||||
if (val === EQueryType.PROM) {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
condition: {
|
||||
...alertDef.condition,
|
||||
matchType: defaultMatchType,
|
||||
},
|
||||
evalWindow: defaultEvalWindow,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isFormValid = useCallback((): boolean => {
|
||||
let retval = true;
|
||||
|
||||
if (!alertDef.alert || alertDef.alert === '') {
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: t('alertname_required'),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
queryCategory === EQueryType.PROM &&
|
||||
(!promQueries || Object.keys(promQueries).length === 0)
|
||||
) {
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: t('promql_required'),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
(queryCategory === EQueryType.QUERY_BUILDER && !metricQueries) ||
|
||||
Object.keys(metricQueries).length === 0
|
||||
) {
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: t('condition_required'),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
Object.keys(metricQueries).forEach((key) => {
|
||||
if (metricQueries[key].metricName === '') {
|
||||
retval = false;
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: t('metricname_missing', { where: metricQueries[key].name }),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(formulaQueries).forEach((key) => {
|
||||
if (formulaQueries[key].expression === '') {
|
||||
retval = false;
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: t('expression_missing', formulaQueries[key].name),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return retval;
|
||||
}, [t, alertDef, queryCategory, metricQueries, formulaQueries, promQueries]);
|
||||
|
||||
const saveRule = useCallback(async () => {
|
||||
if (!isFormValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const postableAlert: AlertDef = {
|
||||
...alertDef,
|
||||
source: window?.location.toString(),
|
||||
ruleType:
|
||||
queryCategory === EQueryType.PROM ? 'promql_rule' : 'threshold_rule',
|
||||
condition: {
|
||||
...alertDef.condition,
|
||||
compositeMetricQuery: {
|
||||
builderQueries: prepareBuilderQueries(metricQueries, formulaQueries),
|
||||
promQueries,
|
||||
queryType: queryCategory,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const apiReq =
|
||||
ruleId && ruleId > 0
|
||||
? { data: postableAlert, id: ruleId }
|
||||
: { data: postableAlert };
|
||||
|
||||
const response = await saveAlertApi(apiReq);
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
notification.success({
|
||||
message: 'Success',
|
||||
description:
|
||||
!ruleId || ruleId === 0 ? t('rule_created') : t('rule_edited'),
|
||||
});
|
||||
console.log('invalidting cache');
|
||||
// invalidate rule in cache
|
||||
ruleCache.invalidateQueries(['ruleId', ruleId]);
|
||||
|
||||
setTimeout(() => {
|
||||
history.replace(ROUTES.LIST_ALL_ALERT);
|
||||
}, 2000);
|
||||
} else {
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: response.error || t('unexpected_error'),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('save alert api failed:', e);
|
||||
notification.error({
|
||||
message: 'Error',
|
||||
description: t('unexpected_error'),
|
||||
});
|
||||
}
|
||||
setLoading(false);
|
||||
}, [
|
||||
t,
|
||||
isFormValid,
|
||||
queryCategory,
|
||||
ruleId,
|
||||
alertDef,
|
||||
metricQueries,
|
||||
formulaQueries,
|
||||
promQueries,
|
||||
ruleCache,
|
||||
]);
|
||||
|
||||
const onSaveHandler = useCallback(async () => {
|
||||
const content = (
|
||||
<Typography.Text>
|
||||
{' '}
|
||||
{t('confirm_save_content_part1')} <QueryTypeTag queryType={queryCategory} />{' '}
|
||||
{t('confirm_save_content_part2')}
|
||||
</Typography.Text>
|
||||
);
|
||||
Modal.confirm({
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
title: t('confirm_save_title'),
|
||||
centered: true,
|
||||
content,
|
||||
onOk() {
|
||||
saveRule();
|
||||
},
|
||||
});
|
||||
}, [t, saveRule, queryCategory]);
|
||||
|
||||
const renderBasicInfo = (): JSX.Element => (
|
||||
<BasicInfo alertDef={alertDef} setAlertDef={setAlertDef} />
|
||||
);
|
||||
|
||||
const renderQBChartPreview = (): JSX.Element => {
|
||||
return (
|
||||
<ChartPreview
|
||||
headline={<PlotTag queryType={queryCategory} />}
|
||||
name=""
|
||||
threshold={alertDef.condition?.target}
|
||||
query={debouncedStagedQuery}
|
||||
selectedInterval={toChartInterval(alertDef.evalWindow)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderPromChartPreview = (): JSX.Element => {
|
||||
return (
|
||||
<ChartPreview
|
||||
headline={<PlotTag queryType={queryCategory} />}
|
||||
name="Chart Preview"
|
||||
threshold={alertDef.condition?.target}
|
||||
query={debouncedStagedQuery}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
<MainFormContainer
|
||||
initialValues={initialValue}
|
||||
layout="vertical"
|
||||
form={formInstance}
|
||||
>
|
||||
{queryCategory === EQueryType.QUERY_BUILDER && renderQBChartPreview()}
|
||||
{queryCategory === EQueryType.PROM && renderPromChartPreview()}
|
||||
<QuerySection
|
||||
queryCategory={queryCategory}
|
||||
setQueryCategory={onQueryCategoryChange}
|
||||
metricQueries={metricQueries}
|
||||
setMetricQueries={setMetricQueries}
|
||||
formulaQueries={formulaQueries}
|
||||
setFormulaQueries={setFormulaQueries}
|
||||
promQueries={promQueries}
|
||||
setPromQueries={setPromQueries}
|
||||
/>
|
||||
|
||||
<RuleOptions
|
||||
queryCategory={queryCategory}
|
||||
alertDef={alertDef}
|
||||
setAlertDef={setAlertDef}
|
||||
/>
|
||||
|
||||
{renderBasicInfo()}
|
||||
<ButtonContainer>
|
||||
<ActionButton
|
||||
loading={loading || false}
|
||||
type="primary"
|
||||
onClick={onSaveHandler}
|
||||
icon={<SaveOutlined />}
|
||||
>
|
||||
{ruleId > 0 ? t('button_savechanges') : t('button_createrule')}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
disabled={loading || false}
|
||||
type="default"
|
||||
onClick={onCancelHandler}
|
||||
>
|
||||
{ruleId === 0 && t('button_cancelchanges')}
|
||||
{ruleId > 0 && t('button_discard')}
|
||||
</ActionButton>
|
||||
</ButtonContainer>
|
||||
</MainFormContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface FormAlertRuleProps {
|
||||
formInstance: FormInstance;
|
||||
initialValue: AlertDef;
|
||||
ruleId: number;
|
||||
}
|
||||
|
||||
export default FormAlertRules;
|
@ -0,0 +1,49 @@
|
||||
import { createMachine } from 'xstate';
|
||||
|
||||
export const ResourceAttributesFilterMachine =
|
||||
/** @xstate-layout N4IgpgJg5mDOIC5QBECGsAWAjA9qgThAAQDKYBAxhkQIIB2xAYgJYA2ALmPgHQAqqUANJgAngGIAcgFEAGr0SgADjljN2zHHQUgAHogAcAFgAM3AOz6ATAEYAzJdsA2Y4cOWAnABoQIxAFpDR2tuQ319AFYTcKdbFycAX3jvNExcAmIySmp6JjZOHn4hUTFNACFWAFd8bWVVdU1tPQQzY1MXY2tDdzNHM3dHd0NvXwR7biMTa313S0i+63DE5PRsPEJScnwqWgYiFg4uPgFhcQAlKRIpeSQQWrUNLRumx3Czbg8TR0sbS31jfUcw38fW47gBHmm4XCVms3SWIBSq3SGyyO1yBx4AHlFFxUOwcPhJLJrkoVPcGk9ENYFuF3i5YR0wtEHECEAEgiEmV8zH1DLYzHZ4Yi0utMltsrt9vluNjcfjCWVKtUbnd6o9QE1rMYBtxbGFvsZ3NrZj1WdYOfotUZLX0XEFHEKViKMpttjk9nlDrL8HiCWJzpcSbcyWrGoh3NCQj0zK53P1ph1WeFLLqnJZ2s5vmZLA6kginWsXaj3VLDoUAGqoSpgEp0cpVGohh5hhDWDy0sz8zruakzamWVm-Qyg362V5-AZOayO1KFlHitEejFHKCV6v+i5XRt1ZuU1s52zjNOOaZfdOWIY+RDZ0Hc6ZmKEXqyLPPCudit2Sz08ACSEFYNbSHI27kuquiIOEjiONwjJgrM3RWJYZisgEIJgnYPTmuEdi2OaiR5nQOAQHA2hvsiH4Sui0qFCcIGhnuLSmP0YJuJ2xjJsmKELG8XZTK0tjdHG06vgW5GupRS7St6vrKqSO4UhqVL8TBWp8o4eqdl0A5Xmy3G6gK56-B4uERDOSKiuJi6lgUAhrhUYB0buimtrEKZBDYrxaS0OZca8+ltheybOI4hivGZzrzp+VGHH+AGOQp4EIHy+ghNYnawtG4TsbYvk8QKfHGAJfQ9uF76WSW37xWBTSGJ0qXpd0vRZdEKGPqC2YeO2-zfO4+HxEAA */
|
||||
createMachine({
|
||||
tsTypes: {} as import('./Labels.machine.typegen').Typegen0,
|
||||
initial: 'Idle',
|
||||
states: {
|
||||
LabelKey: {
|
||||
on: {
|
||||
NEXT: {
|
||||
actions: 'onSelectLabelValue',
|
||||
target: 'LabelValue',
|
||||
},
|
||||
onBlur: {
|
||||
actions: 'onSelectLabelValue',
|
||||
target: 'LabelValue',
|
||||
},
|
||||
RESET: {
|
||||
target: 'Idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
LabelValue: {
|
||||
on: {
|
||||
NEXT: {
|
||||
actions: ['onValidateQuery'],
|
||||
},
|
||||
onBlur: {
|
||||
actions: ['onValidateQuery'],
|
||||
// target: 'Idle',
|
||||
},
|
||||
RESET: {
|
||||
target: 'Idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
Idle: {
|
||||
on: {
|
||||
NEXT: {
|
||||
actions: 'onSelectLabelKey',
|
||||
description: 'Enter a label key',
|
||||
target: 'LabelKey',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
id: 'Label Key Values',
|
||||
});
|
@ -0,0 +1,25 @@
|
||||
// This file was automatically generated. Edits will be overwritten
|
||||
|
||||
export interface Typegen0 {
|
||||
'@@xstate/typegen': true;
|
||||
eventsCausingActions: {
|
||||
onSelectLabelValue: 'NEXT' | 'onBlur';
|
||||
onValidateQuery: 'NEXT' | 'onBlur';
|
||||
onSelectLabelKey: 'NEXT';
|
||||
};
|
||||
internalEvents: {
|
||||
'xstate.init': { type: 'xstate.init' };
|
||||
};
|
||||
invokeSrcNameMap: {};
|
||||
missingImplementations: {
|
||||
actions: 'onSelectLabelValue' | 'onValidateQuery' | 'onSelectLabelKey';
|
||||
services: never;
|
||||
guards: never;
|
||||
delays: never;
|
||||
};
|
||||
eventsCausingServices: {};
|
||||
eventsCausingGuards: {};
|
||||
eventsCausingDelays: {};
|
||||
matchesStates: 'LabelKey' | 'LabelValue' | 'Idle';
|
||||
tags: never;
|
||||
}
|
26
frontend/src/container/FormAlertRules/labels/QueryChip.tsx
Normal file
26
frontend/src/container/FormAlertRules/labels/QueryChip.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
|
||||
import { QueryChipContainer, QueryChipItem } from './styles';
|
||||
import { ILabelRecord } from './types';
|
||||
|
||||
interface QueryChipProps {
|
||||
queryData: ILabelRecord;
|
||||
onRemove: (id: string) => void;
|
||||
}
|
||||
|
||||
export default function QueryChip({
|
||||
queryData,
|
||||
onRemove,
|
||||
}: QueryChipProps): JSX.Element {
|
||||
const { key, value } = queryData;
|
||||
return (
|
||||
<QueryChipContainer>
|
||||
<QueryChipItem
|
||||
closable={key !== 'severity' && key !== 'description'}
|
||||
onClose={(): void => onRemove(key)}
|
||||
>
|
||||
{key}: {value}
|
||||
</QueryChipItem>
|
||||
</QueryChipContainer>
|
||||
);
|
||||
}
|
164
frontend/src/container/FormAlertRules/labels/index.tsx
Normal file
164
frontend/src/container/FormAlertRules/labels/index.tsx
Normal file
@ -0,0 +1,164 @@
|
||||
import {
|
||||
CloseCircleFilled,
|
||||
ExclamationCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useMachine } from '@xstate/react';
|
||||
import { Button, Input, message, Modal } from 'antd';
|
||||
import { map } from 'lodash-es';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Labels } from 'types/api/alerts/def';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { ResourceAttributesFilterMachine } from './Labels.machine';
|
||||
import QueryChip from './QueryChip';
|
||||
import { QueryChipItem, SearchContainer } from './styles';
|
||||
import { ILabelRecord } from './types';
|
||||
import { createQuery, flattenLabels, prepareLabels } from './utils';
|
||||
|
||||
interface LabelSelectProps {
|
||||
onSetLabels: (q: Labels) => void;
|
||||
initialValues: Labels | undefined;
|
||||
}
|
||||
|
||||
function LabelSelect({
|
||||
onSetLabels,
|
||||
initialValues,
|
||||
}: LabelSelectProps): JSX.Element | null {
|
||||
const { t } = useTranslation('rules');
|
||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const [currentVal, setCurrentVal] = useState('');
|
||||
const [staging, setStaging] = useState<string[]>([]);
|
||||
const [queries, setQueries] = useState<ILabelRecord[]>(
|
||||
initialValues ? flattenLabels(initialValues) : [],
|
||||
);
|
||||
|
||||
const dispatchChanges = (updatedRecs: ILabelRecord[]): void => {
|
||||
onSetLabels(prepareLabels(updatedRecs, initialValues));
|
||||
setQueries(updatedRecs);
|
||||
};
|
||||
|
||||
const [state, send] = useMachine(ResourceAttributesFilterMachine, {
|
||||
actions: {
|
||||
onSelectLabelKey: () => {},
|
||||
onSelectLabelValue: () => {
|
||||
if (currentVal !== '') {
|
||||
setStaging((prevState) => [...prevState, currentVal]);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
setCurrentVal('');
|
||||
},
|
||||
onValidateQuery: (): void => {
|
||||
if (currentVal === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const generatedQuery = createQuery([...staging, currentVal]);
|
||||
|
||||
if (generatedQuery) {
|
||||
dispatchChanges([...queries, generatedQuery]);
|
||||
setStaging([]);
|
||||
setCurrentVal('');
|
||||
send('RESET');
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handleFocus = (): void => {
|
||||
if (state.value === 'Idle') {
|
||||
send('NEXT');
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = useCallback((): void => {
|
||||
if (staging.length === 1 && staging[0] !== undefined) {
|
||||
send('onBlur');
|
||||
}
|
||||
}, [send, staging]);
|
||||
|
||||
useEffect(() => {
|
||||
handleBlur();
|
||||
}, [handleBlur]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
setCurrentVal(e.target?.value);
|
||||
};
|
||||
|
||||
const handleClose = (key: string): void => {
|
||||
dispatchChanges(queries.filter((queryData) => queryData.key !== key));
|
||||
};
|
||||
|
||||
const handleClearAll = (): void => {
|
||||
Modal.confirm({
|
||||
title: 'Confirm',
|
||||
icon: <ExclamationCircleOutlined />,
|
||||
content: t('remove_label_confirm'),
|
||||
onOk() {
|
||||
send('RESET');
|
||||
dispatchChanges([]);
|
||||
setStaging([]);
|
||||
message.success(t('remove_label_success'));
|
||||
},
|
||||
okText: t('button_yes'),
|
||||
cancelText: t('button_no'),
|
||||
});
|
||||
};
|
||||
const renderPlaceholder = useCallback((): string => {
|
||||
if (state.value === 'LabelKey') return 'Enter a label key then press ENTER.';
|
||||
if (state.value === 'LabelValue')
|
||||
return `Enter a value for label key(${staging[0]}) then press ENTER.`;
|
||||
return t('placeholder_label_key_pair');
|
||||
}, [t, state, staging]);
|
||||
return (
|
||||
<SearchContainer isDarkMode={isDarkMode} disabled={false}>
|
||||
<div style={{ display: 'inline-flex', flexWrap: 'wrap' }}>
|
||||
{queries.length > 0 &&
|
||||
map(
|
||||
queries,
|
||||
(query): JSX.Element => {
|
||||
return (
|
||||
<QueryChip key={query.key} queryData={query} onRemove={handleClose} />
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{map(staging, (item) => {
|
||||
return <QueryChipItem key={uuid()}>{item}</QueryChipItem>;
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', width: '100%' }}>
|
||||
<Input
|
||||
placeholder={renderPlaceholder()}
|
||||
onChange={handleChange}
|
||||
onKeyUp={(e): void => {
|
||||
if (e.key === 'Enter' || e.code === 'Enter') {
|
||||
send('NEXT');
|
||||
}
|
||||
}}
|
||||
bordered={false}
|
||||
value={currentVal as never}
|
||||
style={{ flex: 1 }}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
|
||||
{queries.length || staging.length || currentVal ? (
|
||||
<Button
|
||||
onClick={handleClearAll}
|
||||
icon={<CloseCircleFilled />}
|
||||
type="text"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</SearchContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default LabelSelect;
|
35
frontend/src/container/FormAlertRules/labels/styles.ts
Normal file
35
frontend/src/container/FormAlertRules/labels/styles.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { grey } from '@ant-design/colors';
|
||||
import { Tag } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
interface SearchContainerProps {
|
||||
isDarkMode: boolean;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export const SearchContainer = styled.div<SearchContainerProps>`
|
||||
width: 70%;
|
||||
border-radisu: 4px;
|
||||
background: ${({ isDarkMode }): string => (isDarkMode ? '#000' : '#fff')};
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.2rem;
|
||||
border: 1px solid #ccc5;
|
||||
${({ disabled }): string => (disabled ? `cursor: not-allowed;` : '')}
|
||||
`;
|
||||
|
||||
export const QueryChipContainer = styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 0.5rem;
|
||||
&:hover {
|
||||
& > * {
|
||||
background: ${grey.primary}44;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QueryChipItem = styled(Tag)`
|
||||
margin-right: 0.1rem;
|
||||
`;
|
9
frontend/src/container/FormAlertRules/labels/types.ts
Normal file
9
frontend/src/container/FormAlertRules/labels/types.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface ILabelRecord {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface IOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
54
frontend/src/container/FormAlertRules/labels/utils.ts
Normal file
54
frontend/src/container/FormAlertRules/labels/utils.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { Labels } from 'types/api/alerts/def';
|
||||
|
||||
import { ILabelRecord } from './types';
|
||||
|
||||
const hiddenLabels = ['severity', 'description'];
|
||||
|
||||
export const createQuery = (
|
||||
selectedItems: Array<string | string[]> = [],
|
||||
): ILabelRecord | null => {
|
||||
if (selectedItems.length === 2) {
|
||||
return {
|
||||
key: selectedItems[0] as string,
|
||||
value: selectedItems[1] as string,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const flattenLabels = (labels: Labels): ILabelRecord[] => {
|
||||
const recs: ILabelRecord[] = [];
|
||||
|
||||
Object.keys(labels).forEach((key) => {
|
||||
if (!hiddenLabels.includes(key)) {
|
||||
recs.push({
|
||||
key,
|
||||
value: labels[key],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return recs;
|
||||
};
|
||||
|
||||
export const prepareLabels = (
|
||||
recs: ILabelRecord[],
|
||||
alertLabels: Labels | undefined,
|
||||
): Labels => {
|
||||
const labels: Labels = {};
|
||||
|
||||
recs.forEach((rec) => {
|
||||
if (!hiddenLabels.includes(rec.key)) {
|
||||
labels[rec.key] = rec.value;
|
||||
}
|
||||
});
|
||||
if (alertLabels) {
|
||||
Object.keys(alertLabels).forEach((key) => {
|
||||
if (hiddenLabels.includes(key)) {
|
||||
labels[key] = alertLabels[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return labels;
|
||||
};
|
90
frontend/src/container/FormAlertRules/styles.ts
Normal file
90
frontend/src/container/FormAlertRules/styles.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { Button, Card, Form, Input, InputNumber, Select } from 'antd';
|
||||
import TextArea from 'antd/lib/input/TextArea';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const MainFormContainer = styled(Form)`
|
||||
max-width: 900px;
|
||||
`;
|
||||
|
||||
export const ButtonContainer = styled.div`
|
||||
&&& {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ActionButton = styled(Button)`
|
||||
margin-right: 1rem;
|
||||
`;
|
||||
|
||||
export const QueryButton = styled(Button)`
|
||||
&&& {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const QueryContainer = styled(Card)`
|
||||
&&& {
|
||||
margin-top: 1rem;
|
||||
min-height: 23.5%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Container = styled.div`
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export const StepHeading = styled.p`
|
||||
margin-top: 1rem;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
export const InlineSelect = styled(Select)`
|
||||
display: inline-block;
|
||||
width: 10% !important;
|
||||
margin-left: 0.2em;
|
||||
margin-right: 0.2em;
|
||||
`;
|
||||
|
||||
export const SeveritySelect = styled(Select)`
|
||||
width: 15% !important;
|
||||
`;
|
||||
|
||||
export const InputSmall = styled(Input)`
|
||||
width: 40% !important;
|
||||
`;
|
||||
|
||||
export const FormContainer = styled.div`
|
||||
padding: 2em;
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #141414;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #303030;
|
||||
`;
|
||||
|
||||
export const ThresholdInput = styled(InputNumber)`
|
||||
& > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& > .ant-input-number-group-addon {
|
||||
width: 130px;
|
||||
}
|
||||
& > .ant-input-number {
|
||||
width: 50%;
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const TextareaMedium = styled(TextArea)`
|
||||
width: 70%;
|
||||
`;
|
31
frontend/src/container/FormAlertRules/useDebounce.js
Normal file
31
frontend/src/container/FormAlertRules/useDebounce.js
Normal file
@ -0,0 +1,31 @@
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
// @ts-nocheck
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
// see https://github.com/tannerlinsley/react-query/issues/293
|
||||
// see https://usehooks.com/useDebounce/
|
||||
export default function useDebounce(value, delay) {
|
||||
// State and setters for debounced value
|
||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
// Update debounced value after delay
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedValue(value);
|
||||
}, delay);
|
||||
|
||||
// Cancel the timeout if value changes (also on delay change or unmount)
|
||||
// This is how we prevent debounced value from updating if value is changed ...
|
||||
// .. within the delay period. Timeout gets cleared and restarted.
|
||||
return () => {
|
||||
clearTimeout(handler);
|
||||
};
|
||||
},
|
||||
[value, delay] // Only re-call effect if value or delay changes
|
||||
);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
134
frontend/src/container/FormAlertRules/utils.ts
Normal file
134
frontend/src/container/FormAlertRules/utils.ts
Normal file
@ -0,0 +1,134 @@
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import {
|
||||
IBuilderQueries,
|
||||
IFormulaQueries,
|
||||
IFormulaQuery,
|
||||
IMetricQueries,
|
||||
IMetricQuery,
|
||||
IPromQueries,
|
||||
IPromQuery,
|
||||
} from 'types/api/alerts/compositeQuery';
|
||||
import {
|
||||
IMetricsBuilderQuery,
|
||||
Query as IStagedQuery,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
export const toFormulaQueries = (b: IBuilderQueries): IFormulaQueries => {
|
||||
const f: IFormulaQueries = {};
|
||||
if (!b) return f;
|
||||
Object.keys(b).forEach((key) => {
|
||||
if (key === 'F1') {
|
||||
f[key] = b[key] as IFormulaQuery;
|
||||
}
|
||||
});
|
||||
|
||||
return f;
|
||||
};
|
||||
|
||||
export const toMetricQueries = (b: IBuilderQueries): IMetricQueries => {
|
||||
const m: IMetricQueries = {};
|
||||
if (!b) return m;
|
||||
Object.keys(b).forEach((key) => {
|
||||
if (key !== 'F1') {
|
||||
m[key] = b[key] as IMetricQuery;
|
||||
}
|
||||
});
|
||||
|
||||
return m;
|
||||
};
|
||||
|
||||
export const toIMetricsBuilderQuery = (
|
||||
q: IMetricQuery,
|
||||
): IMetricsBuilderQuery => {
|
||||
return {
|
||||
name: q.name,
|
||||
metricName: q.metricName,
|
||||
tagFilters: q.tagFilters,
|
||||
groupBy: q.groupBy,
|
||||
aggregateOperator: q.aggregateOperator,
|
||||
disabled: q.disabled,
|
||||
legend: q.legend,
|
||||
};
|
||||
};
|
||||
|
||||
export const prepareBuilderQueries = (
|
||||
m: IMetricQueries,
|
||||
f: IFormulaQueries,
|
||||
): IBuilderQueries => {
|
||||
if (!m) return {};
|
||||
const b: IBuilderQueries = {
|
||||
...m,
|
||||
};
|
||||
|
||||
Object.keys(f).forEach((key) => {
|
||||
b[key] = {
|
||||
...f[key],
|
||||
aggregateOperator: undefined,
|
||||
metricName: '',
|
||||
};
|
||||
});
|
||||
return b;
|
||||
};
|
||||
|
||||
export const prepareStagedQuery = (
|
||||
t: EQueryType,
|
||||
m: IMetricQueries,
|
||||
f: IFormulaQueries,
|
||||
p: IPromQueries,
|
||||
): IStagedQuery => {
|
||||
const qbList: IMetricQuery[] = [];
|
||||
const formulaList: IFormulaQuery[] = [];
|
||||
const promList: IPromQuery[] = [];
|
||||
|
||||
// convert map[string]IMetricQuery to IMetricQuery[]
|
||||
if (m) {
|
||||
Object.keys(m).forEach((key) => {
|
||||
qbList.push(m[key]);
|
||||
});
|
||||
}
|
||||
|
||||
// convert map[string]IFormulaQuery to IFormulaQuery[]
|
||||
if (f) {
|
||||
Object.keys(f).forEach((key) => {
|
||||
formulaList.push(f[key]);
|
||||
});
|
||||
}
|
||||
|
||||
// convert map[string]IPromQuery to IPromQuery[]
|
||||
if (p) {
|
||||
Object.keys(p).forEach((key) => {
|
||||
promList.push({ ...p[key], name: key });
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
queryType: t,
|
||||
promQL: promList,
|
||||
metricsBuilder: {
|
||||
formulas: formulaList,
|
||||
queryBuilder: qbList,
|
||||
},
|
||||
clickHouse: [],
|
||||
};
|
||||
};
|
||||
|
||||
// toChartInterval converts eval window to chart selection time interval
|
||||
export const toChartInterval = (evalWindow: string | undefined): Time => {
|
||||
switch (evalWindow) {
|
||||
case '5m0s':
|
||||
return '5min';
|
||||
case '10m0s':
|
||||
return '10min';
|
||||
case '15m0s':
|
||||
return '15min';
|
||||
case '30m0s':
|
||||
return '30min';
|
||||
case '60m0s':
|
||||
return '30min';
|
||||
case '1440m0s':
|
||||
return '1day';
|
||||
default:
|
||||
return '5min';
|
||||
}
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import { Typography } from 'antd';
|
||||
import { ChartData } from 'chart.js';
|
||||
import Graph, { GraphOnClickHandler } from 'components/Graph';
|
||||
import Graph, { GraphOnClickHandler, StaticLineProps } from 'components/Graph';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import ValueGraph from 'components/ValueGraph';
|
||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||
@ -18,6 +18,7 @@ function GridGraphComponent({
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
}: GridGraphComponentProps): JSX.Element | null {
|
||||
const location = history.location.pathname;
|
||||
|
||||
@ -36,6 +37,7 @@ function GridGraphComponent({
|
||||
onClickHandler,
|
||||
name,
|
||||
yAxisUnit,
|
||||
staticLine,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@ -82,6 +84,7 @@ export interface GridGraphComponentProps {
|
||||
onClickHandler?: GraphOnClickHandler;
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
staticLine?: StaticLineProps;
|
||||
}
|
||||
|
||||
GridGraphComponent.defaultProps = {
|
||||
@ -90,6 +93,7 @@ GridGraphComponent.defaultProps = {
|
||||
isStacked: undefined,
|
||||
onClickHandler: undefined,
|
||||
yAxisUnit: undefined,
|
||||
staticLine: undefined,
|
||||
};
|
||||
|
||||
export default GridGraphComponent;
|
||||
|
@ -64,9 +64,14 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
},
|
||||
{
|
||||
title: 'Alert Name',
|
||||
dataIndex: 'name',
|
||||
dataIndex: 'alert',
|
||||
key: 'name',
|
||||
sorter: (a, b): number => a.name.charCodeAt(0) - b.name.charCodeAt(0),
|
||||
render: (value, record): JSX.Element => (
|
||||
<Typography.Link onClick={(): void => onEditHandler(record.id.toString())}>
|
||||
{value}
|
||||
</Typography.Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Severity',
|
||||
@ -83,7 +88,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
title: 'Labels',
|
||||
dataIndex: 'labels',
|
||||
key: 'tags',
|
||||
align: 'center',
|
||||
@ -100,7 +105,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
{withOutSeverityKeys.map((e) => {
|
||||
return (
|
||||
<Tag key={e} color="magenta">
|
||||
{e}
|
||||
{e}: {value[e]}
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
|
@ -29,7 +29,7 @@ function PromQLQueryContainer({
|
||||
toggleDelete,
|
||||
}: IPromQLQueryHandleChange): void => {
|
||||
const allQueries = queryData[WIDGET_PROMQL_QUERY_KEY_NAME];
|
||||
const currentIndexQuery = allQueries[queryIndex];
|
||||
const currentIndexQuery = allQueries[queryIndex as number];
|
||||
if (query !== undefined) currentIndexQuery.query = query;
|
||||
if (legend !== undefined) currentIndexQuery.legend = legend;
|
||||
|
||||
@ -37,7 +37,7 @@ function PromQLQueryContainer({
|
||||
currentIndexQuery.disabled = !currentIndexQuery.disabled;
|
||||
}
|
||||
if (toggleDelete) {
|
||||
allQueries.splice(queryIndex, 1);
|
||||
allQueries.splice(queryIndex as number, 1);
|
||||
}
|
||||
updateQueryData({ updatedQuery: { ...queryData } });
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ import { IPromQLQueryHandleChange } from './types';
|
||||
|
||||
interface IPromQLQueryBuilderProps {
|
||||
queryData: IPromQLQuery;
|
||||
queryIndex: number;
|
||||
queryIndex: number | string;
|
||||
handleQueryChange: (args: IPromQLQueryHandleChange) => void;
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { IPromQLQuery } from 'types/api/dashboard/getAll';
|
||||
|
||||
export interface IPromQLQueryHandleChange {
|
||||
queryIndex: number;
|
||||
queryIndex: number | string;
|
||||
query?: IPromQLQuery['query'];
|
||||
legend?: IPromQLQuery['legend'];
|
||||
toggleDisable?: IPromQLQuery['disabled'];
|
||||
|
@ -9,7 +9,7 @@ const { TextArea } = Input;
|
||||
|
||||
interface IMetricsBuilderFormulaProps {
|
||||
formulaData: IMetricsBuilderFormula;
|
||||
formulaIndex: number;
|
||||
formulaIndex: number | string;
|
||||
handleFormulaChange: (args: IQueryBuilderFormulaHandleChange) => void;
|
||||
}
|
||||
function MetricsBuilderFormula({
|
||||
|
@ -50,7 +50,7 @@ function QueryBuilderQueryContainer({
|
||||
}: IQueryBuilderQueryHandleChange): void => {
|
||||
const allQueries =
|
||||
queryData[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME].queryBuilder;
|
||||
const currentIndexQuery = allQueries[queryIndex];
|
||||
const currentIndexQuery = allQueries[queryIndex as number];
|
||||
if (aggregateFunction) {
|
||||
currentIndexQuery.aggregateOperator = aggregateFunction;
|
||||
}
|
||||
@ -78,7 +78,7 @@ function QueryBuilderQueryContainer({
|
||||
currentIndexQuery.disabled = !currentIndexQuery.disabled;
|
||||
}
|
||||
if (toggleDelete) {
|
||||
allQueries.splice(queryIndex, 1);
|
||||
allQueries.splice(queryIndex as number, 1);
|
||||
}
|
||||
updateQueryData({ updatedQuery: { ...queryData } });
|
||||
};
|
||||
@ -92,7 +92,7 @@ function QueryBuilderQueryContainer({
|
||||
queryData[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME][
|
||||
WIDGET_QUERY_BUILDER_FORMULA_KEY_NAME
|
||||
];
|
||||
const currentIndexFormula = allFormulas[formulaIndex];
|
||||
const currentIndexFormula = allFormulas[formulaIndex as number];
|
||||
|
||||
if (expression) {
|
||||
currentIndexFormula.expression = expression;
|
||||
@ -103,7 +103,7 @@ function QueryBuilderQueryContainer({
|
||||
}
|
||||
|
||||
if (toggleDelete) {
|
||||
allFormulas.splice(formulaIndex, 1);
|
||||
allFormulas.splice(formulaIndex as number, 1);
|
||||
}
|
||||
updateQueryData({ updatedQuery: { ...queryData } });
|
||||
};
|
||||
|
@ -15,7 +15,7 @@ import { IQueryBuilderQueryHandleChange } from './types';
|
||||
const { Option } = Select;
|
||||
|
||||
interface IMetricsBuilderProps {
|
||||
queryIndex: number;
|
||||
queryIndex: number | string;
|
||||
selectedGraph: GRAPH_TYPES;
|
||||
queryData: IMetricsBuilderQuery;
|
||||
handleQueryChange: (args: IQueryBuilderQueryHandleChange) => void;
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
} from 'types/api/dashboard/getAll';
|
||||
|
||||
export interface IQueryBuilderQueryHandleChange {
|
||||
queryIndex: number;
|
||||
queryIndex: number | string;
|
||||
aggregateFunction?: IMetricsBuilderQuery['aggregateOperator'];
|
||||
metricName?: IMetricsBuilderQuery['metricName'];
|
||||
tagFilters?: IMetricsBuilderQuery['tagFilters']['items'];
|
||||
@ -16,7 +16,7 @@ export interface IQueryBuilderQueryHandleChange {
|
||||
}
|
||||
|
||||
export interface IQueryBuilderFormulaHandleChange {
|
||||
formulaIndex: number;
|
||||
formulaIndex: number | string;
|
||||
expression?: IMetricsBuilderFormula['expression'];
|
||||
toggleDisable?: IMetricsBuilderFormula['disabled'];
|
||||
toggleDelete?: boolean;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
|
||||
type FiveMin = '5min';
|
||||
type TenMin = '10min';
|
||||
type FifteenMin = '15min';
|
||||
type ThirtyMin = '30min';
|
||||
type OneMin = '1min';
|
||||
@ -12,6 +13,7 @@ type Custom = 'custom';
|
||||
|
||||
export type Time =
|
||||
| FiveMin
|
||||
| TenMin
|
||||
| FifteenMin
|
||||
| ThirtyMin
|
||||
| OneMin
|
||||
|
@ -13,6 +13,9 @@ const GetMinMax = (
|
||||
if (interval === '1min') {
|
||||
const minTimeAgo = getMinAgo({ minutes: 1 }).getTime();
|
||||
minTime = minTimeAgo;
|
||||
} else if (interval === '10min') {
|
||||
const minTimeAgo = getMinAgo({ minutes: 10 }).getTime();
|
||||
minTime = minTimeAgo;
|
||||
} else if (interval === '15min') {
|
||||
const minTimeAgo = getMinAgo({ minutes: 15 }).getTime();
|
||||
minTime = minTimeAgo;
|
||||
|
@ -1,109 +1,9 @@
|
||||
import { SaveOutlined } from '@ant-design/icons';
|
||||
import { Button, notification } from 'antd';
|
||||
import createAlertsApi from 'api/alerts/create';
|
||||
import Editor from 'components/Editor';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { State } from 'hooks/useFetch';
|
||||
import history from 'lib/history';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { PayloadProps as CreateAlertPayloadProps } from 'types/api/alerts/create';
|
||||
import CreateAlertRule from 'container/CreateAlertRule';
|
||||
import React from 'react';
|
||||
import { alertDefaults } from 'types/api/alerts/create';
|
||||
|
||||
import { ButtonContainer, Title } from './styles';
|
||||
|
||||
function CreateAlert(): JSX.Element {
|
||||
const [value, setEditorValue] = useState<string>(
|
||||
`\n alert: High RPS\n expr: sum(rate(signoz_latency_count{span_kind="SPAN_KIND_SERVER"}[2m])) by (service_name) > 100\n for: 0m\n labels:\n severity: warning\n annotations:\n summary: High RPS of Applications\n description: "RPS is > 100\n\t\t\t VALUE = {{ $value }}\n\t\t\t LABELS = {{ $labels }}"\n `,
|
||||
);
|
||||
|
||||
const [newAlertState, setNewAlertState] = useState<
|
||||
State<CreateAlertPayloadProps>
|
||||
>({
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: false,
|
||||
payload: undefined,
|
||||
success: false,
|
||||
});
|
||||
const [notifications, Element] = notification.useNotification();
|
||||
|
||||
const defaultError =
|
||||
'Oops! Some issue occured in saving the alert please try again or contact support@signoz.io';
|
||||
|
||||
const onSaveHandler = useCallback(async () => {
|
||||
try {
|
||||
setNewAlertState((state) => ({
|
||||
...state,
|
||||
loading: true,
|
||||
}));
|
||||
|
||||
if (value.length === 0) {
|
||||
setNewAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
}));
|
||||
notifications.error({
|
||||
description: `Oops! We didn't catch that. Please make sure the alert settings are not empty or try again`,
|
||||
message: 'Error',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await createAlertsApi({
|
||||
query: value,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
setNewAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
payload: response.payload,
|
||||
}));
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: 'Congrats. The alert was saved correctly.',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
history.push(ROUTES.LIST_ALL_ALERT);
|
||||
}, 3000);
|
||||
} else {
|
||||
notifications.error({
|
||||
description: response.error || defaultError,
|
||||
message: 'Error',
|
||||
});
|
||||
setNewAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
error: true,
|
||||
errorMessage: response.error || defaultError,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
message: defaultError,
|
||||
});
|
||||
}
|
||||
}, [notifications, value]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{Element}
|
||||
|
||||
<Title>Create New Alert</Title>
|
||||
<Editor onChange={(value): void => setEditorValue(value)} value={value} />
|
||||
|
||||
<ButtonContainer>
|
||||
<Button
|
||||
loading={newAlertState.loading || false}
|
||||
type="primary"
|
||||
onClick={onSaveHandler}
|
||||
icon={<SaveOutlined />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
</>
|
||||
);
|
||||
function CreateAlertPage(): JSX.Element {
|
||||
return <CreateAlertRule initialValue={alertDefaults} />;
|
||||
}
|
||||
|
||||
export default CreateAlert;
|
||||
export default CreateAlertPage;
|
||||
|
@ -47,7 +47,12 @@ function EditRules(): JSX.Element {
|
||||
return <Spinner tip="Loading Rules..." />;
|
||||
}
|
||||
|
||||
return <EditRulesContainer ruleId={ruleId} initialData={data.payload.data} />;
|
||||
return (
|
||||
<EditRulesContainer
|
||||
ruleId={parseInt(ruleId, 10)}
|
||||
initialValue={data.payload.data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditRules;
|
||||
|
64
frontend/src/types/api/alerts/compositeQuery.ts
Normal file
64
frontend/src/types/api/alerts/compositeQuery.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import {
|
||||
IMetricsBuilderFormula,
|
||||
IMetricsBuilderQuery,
|
||||
IPromQLQuery,
|
||||
IQueryBuilderTagFilters,
|
||||
} from 'types/api/dashboard/getAll';
|
||||
import { EAggregateOperator, EQueryType } from 'types/common/dashboard';
|
||||
|
||||
export interface ICompositeMetricQuery {
|
||||
builderQueries: IBuilderQueries;
|
||||
promQueries: IPromQueries;
|
||||
queryType: EQueryType;
|
||||
}
|
||||
|
||||
export interface IPromQueries {
|
||||
[key: string]: IPromQuery;
|
||||
}
|
||||
|
||||
export interface IPromQuery extends IPromQLQuery {
|
||||
stats?: '';
|
||||
}
|
||||
|
||||
export interface IBuilderQueries {
|
||||
[key: string]: IBuilderQuery;
|
||||
}
|
||||
|
||||
// IBuilderQuery combines IMetricQuery and IFormulaQuery
|
||||
// for api calls
|
||||
export interface IBuilderQuery
|
||||
extends Omit<
|
||||
IMetricQuery,
|
||||
'aggregateOperator' | 'legend' | 'metricName' | 'tagFilters'
|
||||
>,
|
||||
Omit<IFormulaQuery, 'expression'> {
|
||||
aggregateOperator: EAggregateOperator | undefined;
|
||||
disabled: boolean;
|
||||
name: string;
|
||||
legend?: string;
|
||||
metricName: string | null;
|
||||
groupBy?: string[];
|
||||
expression?: string;
|
||||
tagFilters?: IQueryBuilderTagFilters;
|
||||
toggleDisable?: boolean;
|
||||
toggleDelete?: boolean;
|
||||
}
|
||||
|
||||
export interface IFormulaQueries {
|
||||
[key: string]: IFormulaQuery;
|
||||
}
|
||||
|
||||
export interface IFormulaQuery extends IMetricsBuilderFormula {
|
||||
formulaOnly: boolean;
|
||||
queryName: string;
|
||||
}
|
||||
|
||||
export interface IMetricQueries {
|
||||
[key: string]: IMetricQuery;
|
||||
}
|
||||
|
||||
export interface IMetricQuery extends IMetricsBuilderQuery {
|
||||
formulaOnly: boolean;
|
||||
expression?: string;
|
||||
queryName: string;
|
||||
}
|
@ -1,8 +1,48 @@
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
import { defaultCompareOp, defaultEvalWindow, defaultMatchType } from './def';
|
||||
|
||||
export interface Props {
|
||||
query: string;
|
||||
data: AlertDef;
|
||||
}
|
||||
|
||||
export interface PayloadProps {
|
||||
status: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export const alertDefaults: AlertDef = {
|
||||
condition: {
|
||||
compositeMetricQuery: {
|
||||
builderQueries: {
|
||||
A: {
|
||||
queryName: 'A',
|
||||
name: 'A',
|
||||
formulaOnly: false,
|
||||
metricName: '',
|
||||
tagFilters: {
|
||||
op: 'AND',
|
||||
items: [],
|
||||
},
|
||||
groupBy: [],
|
||||
aggregateOperator: 1,
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
toggleDisable: false,
|
||||
toggleDelete: false,
|
||||
},
|
||||
},
|
||||
promQueries: {},
|
||||
queryType: 1,
|
||||
},
|
||||
op: defaultCompareOp,
|
||||
matchType: defaultMatchType,
|
||||
},
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
},
|
||||
annotations: {
|
||||
description: 'A new alert',
|
||||
},
|
||||
evalWindow: defaultEvalWindow,
|
||||
};
|
||||
|
32
frontend/src/types/api/alerts/def.ts
Normal file
32
frontend/src/types/api/alerts/def.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||
|
||||
// default match type for threshold
|
||||
export const defaultMatchType = '1';
|
||||
|
||||
// default eval window
|
||||
export const defaultEvalWindow = '5m0s';
|
||||
|
||||
// default compare op: above
|
||||
export const defaultCompareOp = '1';
|
||||
|
||||
export interface AlertDef {
|
||||
id?: number;
|
||||
alert?: string;
|
||||
ruleType?: string;
|
||||
condition: RuleCondition;
|
||||
labels?: Labels;
|
||||
annotations?: Labels;
|
||||
evalWindow?: string;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
export interface RuleCondition {
|
||||
compositeMetricQuery: ICompositeMetricQuery;
|
||||
op?: string | undefined;
|
||||
target?: number | undefined;
|
||||
matchType?: string | undefined;
|
||||
}
|
||||
|
||||
export interface Labels {
|
||||
[key: string]: string;
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { Alerts } from './getAll';
|
||||
import { AlertDef } from './def';
|
||||
|
||||
export interface Props {
|
||||
id: Alerts['id'];
|
||||
id: AlertDef['id'];
|
||||
}
|
||||
|
||||
export type PayloadProps = {
|
||||
data: string;
|
||||
data: AlertDef;
|
||||
};
|
||||
|
@ -1,9 +0,0 @@
|
||||
import { PayloadProps as DeletePayloadProps } from './delete';
|
||||
import { Alerts } from './getAll';
|
||||
|
||||
export type PayloadProps = DeletePayloadProps;
|
||||
|
||||
export interface Props {
|
||||
id: Alerts['id'];
|
||||
data: DeletePayloadProps['data'];
|
||||
}
|
17
frontend/src/types/api/alerts/queryType.ts
Normal file
17
frontend/src/types/api/alerts/queryType.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export type QueryType = 1 | 2 | 3;
|
||||
|
||||
export const QUERY_BUILDER: QueryType = 1;
|
||||
export const PROMQL: QueryType = 3;
|
||||
|
||||
export const resolveQueryCategoryName = (s: number): string => {
|
||||
switch (s) {
|
||||
case 1:
|
||||
return 'Query Builder';
|
||||
case 2:
|
||||
return 'Clickhouse Query';
|
||||
case 3:
|
||||
return 'PromQL';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
11
frontend/src/types/api/alerts/save.ts
Normal file
11
frontend/src/types/api/alerts/save.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { AlertDef } from './def';
|
||||
|
||||
export type PayloadProps = {
|
||||
status: string;
|
||||
data: string;
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
id?: number;
|
||||
data: AlertDef;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user