mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-06-21 00:37:58 +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",
|
"babel-preset-react-app": "^10.0.0",
|
||||||
"chart.js": "^3.4.0",
|
"chart.js": "^3.4.0",
|
||||||
"chartjs-adapter-date-fns": "^2.0.0",
|
"chartjs-adapter-date-fns": "^2.0.0",
|
||||||
|
"chartjs-plugin-annotation": "^1.4.0",
|
||||||
"color": "^4.2.1",
|
"color": "^4.2.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"css-loader": "4.3.0",
|
"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> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post('/rules', {
|
const response = await axios.post('/rules', {
|
||||||
data: props.query,
|
...props.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -14,7 +14,7 @@ const get = async (
|
|||||||
statusCode: 200,
|
statusCode: 200,
|
||||||
error: null,
|
error: null,
|
||||||
message: response.data.status,
|
message: response.data.status,
|
||||||
payload: response.data.data,
|
payload: response.data,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
@ -2,14 +2,14 @@ import axios from 'api';
|
|||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
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 (
|
const put = async (
|
||||||
props: Props,
|
props: Props,
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.put(`/rules/${props.id}`, {
|
const response = await axios.put(`/rules/${props.id}`, {
|
||||||
data: props.data,
|
...props.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
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,
|
Tooltip,
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
|
||||||
|
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||||
import React, { useCallback, useEffect, useRef } from 'react';
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@ -50,6 +51,7 @@ Chart.register(
|
|||||||
SubTitle,
|
SubTitle,
|
||||||
BarController,
|
BarController,
|
||||||
BarElement,
|
BarElement,
|
||||||
|
annotationPlugin,
|
||||||
);
|
);
|
||||||
|
|
||||||
function Graph({
|
function Graph({
|
||||||
@ -62,6 +64,7 @@ function Graph({
|
|||||||
name,
|
name,
|
||||||
yAxisUnit = 'short',
|
yAxisUnit = 'short',
|
||||||
forceReRender,
|
forceReRender,
|
||||||
|
staticLine,
|
||||||
}: GraphProps): JSX.Element {
|
}: GraphProps): JSX.Element {
|
||||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||||
@ -99,6 +102,30 @@ function Graph({
|
|||||||
intersect: false,
|
intersect: false,
|
||||||
},
|
},
|
||||||
plugins: {
|
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: {
|
title: {
|
||||||
display: title !== undefined,
|
display: title !== undefined,
|
||||||
text: title,
|
text: title,
|
||||||
@ -180,6 +207,7 @@ function Graph({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const chartHasData = hasData(data);
|
const chartHasData = hasData(data);
|
||||||
const chartPlugins = [];
|
const chartPlugins = [];
|
||||||
|
|
||||||
@ -205,6 +233,7 @@ function Graph({
|
|||||||
name,
|
name,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
onClickHandler,
|
onClickHandler,
|
||||||
|
staticLine,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -229,6 +258,16 @@ interface GraphProps {
|
|||||||
name: string;
|
name: string;
|
||||||
yAxisUnit?: string;
|
yAxisUnit?: string;
|
||||||
forceReRender?: boolean | null | number;
|
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 = (
|
export type GraphOnClickHandler = (
|
||||||
@ -245,5 +284,6 @@ Graph.defaultProps = {
|
|||||||
onClickHandler: undefined,
|
onClickHandler: undefined,
|
||||||
yAxisUnit: undefined,
|
yAxisUnit: undefined,
|
||||||
forceReRender: undefined,
|
forceReRender: undefined,
|
||||||
|
staticLine: undefined,
|
||||||
};
|
};
|
||||||
export default Graph;
|
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 { Form } from 'antd';
|
||||||
import { Button, notification } from 'antd';
|
import FormAlertRules from 'container/FormAlertRules';
|
||||||
import put from 'api/alerts/put';
|
import React from 'react';
|
||||||
import Editor from 'components/Editor';
|
import { AlertDef } from 'types/api/alerts/def';
|
||||||
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 { ButtonContainer } from './styles';
|
function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
|
||||||
|
const [formInstance] = Form.useForm();
|
||||||
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]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<FormAlertRules
|
||||||
{Element}
|
formInstance={formInstance}
|
||||||
|
initialValue={initialValue}
|
||||||
<Editor onChange={(value): void => setEditorValue(value)} value={value} />
|
ruleId={ruleId}
|
||||||
|
/>
|
||||||
<ButtonContainer>
|
|
||||||
<Button
|
|
||||||
loading={editButtonState.loading || false}
|
|
||||||
disabled={editButtonState.loading || false}
|
|
||||||
icon={<SaveFilled />}
|
|
||||||
onClick={onClickHandler}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</ButtonContainer>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditRulesProps {
|
interface EditRulesProps {
|
||||||
initialData: PayloadProps['data'];
|
initialValue: AlertDef;
|
||||||
ruleId: string;
|
ruleId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditRules;
|
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 { Typography } from 'antd';
|
||||||
import { ChartData } from 'chart.js';
|
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 { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||||
import ValueGraph from 'components/ValueGraph';
|
import ValueGraph from 'components/ValueGraph';
|
||||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||||
@ -18,6 +18,7 @@ function GridGraphComponent({
|
|||||||
onClickHandler,
|
onClickHandler,
|
||||||
name,
|
name,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
|
staticLine,
|
||||||
}: GridGraphComponentProps): JSX.Element | null {
|
}: GridGraphComponentProps): JSX.Element | null {
|
||||||
const location = history.location.pathname;
|
const location = history.location.pathname;
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ function GridGraphComponent({
|
|||||||
onClickHandler,
|
onClickHandler,
|
||||||
name,
|
name,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
|
staticLine,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -82,6 +84,7 @@ export interface GridGraphComponentProps {
|
|||||||
onClickHandler?: GraphOnClickHandler;
|
onClickHandler?: GraphOnClickHandler;
|
||||||
name: string;
|
name: string;
|
||||||
yAxisUnit?: string;
|
yAxisUnit?: string;
|
||||||
|
staticLine?: StaticLineProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
GridGraphComponent.defaultProps = {
|
GridGraphComponent.defaultProps = {
|
||||||
@ -90,6 +93,7 @@ GridGraphComponent.defaultProps = {
|
|||||||
isStacked: undefined,
|
isStacked: undefined,
|
||||||
onClickHandler: undefined,
|
onClickHandler: undefined,
|
||||||
yAxisUnit: undefined,
|
yAxisUnit: undefined,
|
||||||
|
staticLine: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GridGraphComponent;
|
export default GridGraphComponent;
|
||||||
|
@ -64,9 +64,14 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Alert Name',
|
title: 'Alert Name',
|
||||||
dataIndex: 'name',
|
dataIndex: 'alert',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
sorter: (a, b): number => a.name.charCodeAt(0) - b.name.charCodeAt(0),
|
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',
|
title: 'Severity',
|
||||||
@ -83,7 +88,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Tags',
|
title: 'Labels',
|
||||||
dataIndex: 'labels',
|
dataIndex: 'labels',
|
||||||
key: 'tags',
|
key: 'tags',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
@ -100,7 +105,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
{withOutSeverityKeys.map((e) => {
|
{withOutSeverityKeys.map((e) => {
|
||||||
return (
|
return (
|
||||||
<Tag key={e} color="magenta">
|
<Tag key={e} color="magenta">
|
||||||
{e}
|
{e}: {value[e]}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -29,7 +29,7 @@ function PromQLQueryContainer({
|
|||||||
toggleDelete,
|
toggleDelete,
|
||||||
}: IPromQLQueryHandleChange): void => {
|
}: IPromQLQueryHandleChange): void => {
|
||||||
const allQueries = queryData[WIDGET_PROMQL_QUERY_KEY_NAME];
|
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 (query !== undefined) currentIndexQuery.query = query;
|
||||||
if (legend !== undefined) currentIndexQuery.legend = legend;
|
if (legend !== undefined) currentIndexQuery.legend = legend;
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ function PromQLQueryContainer({
|
|||||||
currentIndexQuery.disabled = !currentIndexQuery.disabled;
|
currentIndexQuery.disabled = !currentIndexQuery.disabled;
|
||||||
}
|
}
|
||||||
if (toggleDelete) {
|
if (toggleDelete) {
|
||||||
allQueries.splice(queryIndex, 1);
|
allQueries.splice(queryIndex as number, 1);
|
||||||
}
|
}
|
||||||
updateQueryData({ updatedQuery: { ...queryData } });
|
updateQueryData({ updatedQuery: { ...queryData } });
|
||||||
};
|
};
|
||||||
|
@ -7,7 +7,7 @@ import { IPromQLQueryHandleChange } from './types';
|
|||||||
|
|
||||||
interface IPromQLQueryBuilderProps {
|
interface IPromQLQueryBuilderProps {
|
||||||
queryData: IPromQLQuery;
|
queryData: IPromQLQuery;
|
||||||
queryIndex: number;
|
queryIndex: number | string;
|
||||||
handleQueryChange: (args: IPromQLQueryHandleChange) => void;
|
handleQueryChange: (args: IPromQLQueryHandleChange) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { IPromQLQuery } from 'types/api/dashboard/getAll';
|
import { IPromQLQuery } from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
export interface IPromQLQueryHandleChange {
|
export interface IPromQLQueryHandleChange {
|
||||||
queryIndex: number;
|
queryIndex: number | string;
|
||||||
query?: IPromQLQuery['query'];
|
query?: IPromQLQuery['query'];
|
||||||
legend?: IPromQLQuery['legend'];
|
legend?: IPromQLQuery['legend'];
|
||||||
toggleDisable?: IPromQLQuery['disabled'];
|
toggleDisable?: IPromQLQuery['disabled'];
|
||||||
|
@ -9,7 +9,7 @@ const { TextArea } = Input;
|
|||||||
|
|
||||||
interface IMetricsBuilderFormulaProps {
|
interface IMetricsBuilderFormulaProps {
|
||||||
formulaData: IMetricsBuilderFormula;
|
formulaData: IMetricsBuilderFormula;
|
||||||
formulaIndex: number;
|
formulaIndex: number | string;
|
||||||
handleFormulaChange: (args: IQueryBuilderFormulaHandleChange) => void;
|
handleFormulaChange: (args: IQueryBuilderFormulaHandleChange) => void;
|
||||||
}
|
}
|
||||||
function MetricsBuilderFormula({
|
function MetricsBuilderFormula({
|
||||||
|
@ -50,7 +50,7 @@ function QueryBuilderQueryContainer({
|
|||||||
}: IQueryBuilderQueryHandleChange): void => {
|
}: IQueryBuilderQueryHandleChange): void => {
|
||||||
const allQueries =
|
const allQueries =
|
||||||
queryData[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME].queryBuilder;
|
queryData[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME].queryBuilder;
|
||||||
const currentIndexQuery = allQueries[queryIndex];
|
const currentIndexQuery = allQueries[queryIndex as number];
|
||||||
if (aggregateFunction) {
|
if (aggregateFunction) {
|
||||||
currentIndexQuery.aggregateOperator = aggregateFunction;
|
currentIndexQuery.aggregateOperator = aggregateFunction;
|
||||||
}
|
}
|
||||||
@ -78,7 +78,7 @@ function QueryBuilderQueryContainer({
|
|||||||
currentIndexQuery.disabled = !currentIndexQuery.disabled;
|
currentIndexQuery.disabled = !currentIndexQuery.disabled;
|
||||||
}
|
}
|
||||||
if (toggleDelete) {
|
if (toggleDelete) {
|
||||||
allQueries.splice(queryIndex, 1);
|
allQueries.splice(queryIndex as number, 1);
|
||||||
}
|
}
|
||||||
updateQueryData({ updatedQuery: { ...queryData } });
|
updateQueryData({ updatedQuery: { ...queryData } });
|
||||||
};
|
};
|
||||||
@ -92,7 +92,7 @@ function QueryBuilderQueryContainer({
|
|||||||
queryData[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME][
|
queryData[WIDGET_QUERY_BUILDER_QUERY_KEY_NAME][
|
||||||
WIDGET_QUERY_BUILDER_FORMULA_KEY_NAME
|
WIDGET_QUERY_BUILDER_FORMULA_KEY_NAME
|
||||||
];
|
];
|
||||||
const currentIndexFormula = allFormulas[formulaIndex];
|
const currentIndexFormula = allFormulas[formulaIndex as number];
|
||||||
|
|
||||||
if (expression) {
|
if (expression) {
|
||||||
currentIndexFormula.expression = expression;
|
currentIndexFormula.expression = expression;
|
||||||
@ -103,7 +103,7 @@ function QueryBuilderQueryContainer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (toggleDelete) {
|
if (toggleDelete) {
|
||||||
allFormulas.splice(formulaIndex, 1);
|
allFormulas.splice(formulaIndex as number, 1);
|
||||||
}
|
}
|
||||||
updateQueryData({ updatedQuery: { ...queryData } });
|
updateQueryData({ updatedQuery: { ...queryData } });
|
||||||
};
|
};
|
||||||
|
@ -15,7 +15,7 @@ import { IQueryBuilderQueryHandleChange } from './types';
|
|||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
interface IMetricsBuilderProps {
|
interface IMetricsBuilderProps {
|
||||||
queryIndex: number;
|
queryIndex: number | string;
|
||||||
selectedGraph: GRAPH_TYPES;
|
selectedGraph: GRAPH_TYPES;
|
||||||
queryData: IMetricsBuilderQuery;
|
queryData: IMetricsBuilderQuery;
|
||||||
handleQueryChange: (args: IQueryBuilderQueryHandleChange) => void;
|
handleQueryChange: (args: IQueryBuilderQueryHandleChange) => void;
|
||||||
|
@ -4,7 +4,7 @@ import {
|
|||||||
} from 'types/api/dashboard/getAll';
|
} from 'types/api/dashboard/getAll';
|
||||||
|
|
||||||
export interface IQueryBuilderQueryHandleChange {
|
export interface IQueryBuilderQueryHandleChange {
|
||||||
queryIndex: number;
|
queryIndex: number | string;
|
||||||
aggregateFunction?: IMetricsBuilderQuery['aggregateOperator'];
|
aggregateFunction?: IMetricsBuilderQuery['aggregateOperator'];
|
||||||
metricName?: IMetricsBuilderQuery['metricName'];
|
metricName?: IMetricsBuilderQuery['metricName'];
|
||||||
tagFilters?: IMetricsBuilderQuery['tagFilters']['items'];
|
tagFilters?: IMetricsBuilderQuery['tagFilters']['items'];
|
||||||
@ -16,7 +16,7 @@ export interface IQueryBuilderQueryHandleChange {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IQueryBuilderFormulaHandleChange {
|
export interface IQueryBuilderFormulaHandleChange {
|
||||||
formulaIndex: number;
|
formulaIndex: number | string;
|
||||||
expression?: IMetricsBuilderFormula['expression'];
|
expression?: IMetricsBuilderFormula['expression'];
|
||||||
toggleDisable?: IMetricsBuilderFormula['disabled'];
|
toggleDisable?: IMetricsBuilderFormula['disabled'];
|
||||||
toggleDelete?: boolean;
|
toggleDelete?: boolean;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
|
|
||||||
type FiveMin = '5min';
|
type FiveMin = '5min';
|
||||||
|
type TenMin = '10min';
|
||||||
type FifteenMin = '15min';
|
type FifteenMin = '15min';
|
||||||
type ThirtyMin = '30min';
|
type ThirtyMin = '30min';
|
||||||
type OneMin = '1min';
|
type OneMin = '1min';
|
||||||
@ -12,6 +13,7 @@ type Custom = 'custom';
|
|||||||
|
|
||||||
export type Time =
|
export type Time =
|
||||||
| FiveMin
|
| FiveMin
|
||||||
|
| TenMin
|
||||||
| FifteenMin
|
| FifteenMin
|
||||||
| ThirtyMin
|
| ThirtyMin
|
||||||
| OneMin
|
| OneMin
|
||||||
|
@ -13,6 +13,9 @@ const GetMinMax = (
|
|||||||
if (interval === '1min') {
|
if (interval === '1min') {
|
||||||
const minTimeAgo = getMinAgo({ minutes: 1 }).getTime();
|
const minTimeAgo = getMinAgo({ minutes: 1 }).getTime();
|
||||||
minTime = minTimeAgo;
|
minTime = minTimeAgo;
|
||||||
|
} else if (interval === '10min') {
|
||||||
|
const minTimeAgo = getMinAgo({ minutes: 10 }).getTime();
|
||||||
|
minTime = minTimeAgo;
|
||||||
} else if (interval === '15min') {
|
} else if (interval === '15min') {
|
||||||
const minTimeAgo = getMinAgo({ minutes: 15 }).getTime();
|
const minTimeAgo = getMinAgo({ minutes: 15 }).getTime();
|
||||||
minTime = minTimeAgo;
|
minTime = minTimeAgo;
|
||||||
|
@ -1,109 +1,9 @@
|
|||||||
import { SaveOutlined } from '@ant-design/icons';
|
import CreateAlertRule from 'container/CreateAlertRule';
|
||||||
import { Button, notification } from 'antd';
|
import React from 'react';
|
||||||
import createAlertsApi from 'api/alerts/create';
|
import { alertDefaults } from 'types/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 { ButtonContainer, Title } from './styles';
|
function CreateAlertPage(): JSX.Element {
|
||||||
|
return <CreateAlertRule initialValue={alertDefaults} />;
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateAlert;
|
export default CreateAlertPage;
|
||||||
|
@ -47,7 +47,12 @@ function EditRules(): JSX.Element {
|
|||||||
return <Spinner tip="Loading Rules..." />;
|
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;
|
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 {
|
export interface Props {
|
||||||
query: string;
|
data: AlertDef;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PayloadProps {
|
export interface PayloadProps {
|
||||||
status: string;
|
status: string;
|
||||||
data: 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 {
|
export interface Props {
|
||||||
id: Alerts['id'];
|
id: AlertDef['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PayloadProps = {
|
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