mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-13 03:59:04 +08:00
feat: anomaly detection UI (#5916)
* feat: anamoly detection - initial ui * feat: anamoly detection - oct 10 * feat: use antd checkbox * feat: handle multiple series * feat: handle chart height on switch btwn threshold / anomaly * feat: do not update url on detection type change * chore: pr clean up * feat: remove chart container background
This commit is contained in:
parent
ecae842fa1
commit
2180118094
@ -56,6 +56,7 @@
|
|||||||
"option_last": "last",
|
"option_last": "last",
|
||||||
"option_above": "above",
|
"option_above": "above",
|
||||||
"option_below": "below",
|
"option_below": "below",
|
||||||
|
"option_above_below": "above/below",
|
||||||
"option_equal": "is equal to",
|
"option_equal": "is equal to",
|
||||||
"option_notequal": "not equal to",
|
"option_notequal": "not equal to",
|
||||||
"button_query": "Query",
|
"button_query": "Query",
|
||||||
@ -110,6 +111,8 @@
|
|||||||
"choose_alert_type": "Choose a type for the alert",
|
"choose_alert_type": "Choose a type for the alert",
|
||||||
"metric_based_alert": "Metric based Alert",
|
"metric_based_alert": "Metric based Alert",
|
||||||
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
|
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
|
||||||
|
"anomaly_based_alert": "Anomaly based Alert",
|
||||||
|
"anomaly_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
|
||||||
"log_based_alert": "Log-based Alert",
|
"log_based_alert": "Log-based Alert",
|
||||||
"log_based_alert_desc": "Send a notification when a condition occurs in the logs data.",
|
"log_based_alert_desc": "Send a notification when a condition occurs in the logs data.",
|
||||||
"traces_based_alert": "Trace-based Alert",
|
"traces_based_alert": "Trace-based Alert",
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
"option_last": "last",
|
"option_last": "last",
|
||||||
"option_above": "above",
|
"option_above": "above",
|
||||||
"option_below": "below",
|
"option_below": "below",
|
||||||
|
"option_above_below": "above/below",
|
||||||
"option_equal": "is equal to",
|
"option_equal": "is equal to",
|
||||||
"option_notequal": "not equal to",
|
"option_notequal": "not equal to",
|
||||||
"button_query": "Query",
|
"button_query": "Query",
|
||||||
|
@ -13,9 +13,12 @@
|
|||||||
"button_no": "No",
|
"button_no": "No",
|
||||||
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
"remove_label_confirm": "This action will remove all the labels. Do you want to proceed?",
|
||||||
"remove_label_success": "Labels cleared",
|
"remove_label_success": "Labels cleared",
|
||||||
"alert_form_step1": "Step 1 - Define the metric",
|
"alert_form_step1": "Choose a detection method",
|
||||||
"alert_form_step2": "Step 2 - Define Alert Conditions",
|
"alert_form_step2": "Define the metric",
|
||||||
"alert_form_step3": "Step 3 - Alert Configuration",
|
"alert_form_step3": "Define Alert Conditions",
|
||||||
|
"alert_form_step4": "Alert Configuration",
|
||||||
|
"threshold_alert_desc": "An alert is triggered whenever a metric deviates from an expected threshold.",
|
||||||
|
"anomaly_detection_alert_desc": "An alert is triggered whenever a metric deviates from an expected pattern.",
|
||||||
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
"metric_query_max_limit": "Can not create query. You can create maximum of 5 queries",
|
||||||
"confirm_save_title": "Save Changes",
|
"confirm_save_title": "Save Changes",
|
||||||
"confirm_save_content_part1": "Your alert built with",
|
"confirm_save_content_part1": "Your alert built with",
|
||||||
@ -35,6 +38,7 @@
|
|||||||
"button_cancelchanges": "Cancel",
|
"button_cancelchanges": "Cancel",
|
||||||
"button_discard": "Discard",
|
"button_discard": "Discard",
|
||||||
"text_condition1": "Send a notification when",
|
"text_condition1": "Send a notification when",
|
||||||
|
"text_condition1_anomaly": "Send notification when the observed value for",
|
||||||
"text_condition2": "the threshold",
|
"text_condition2": "the threshold",
|
||||||
"text_condition3": "during the last",
|
"text_condition3": "during the last",
|
||||||
"option_1min": "1 min",
|
"option_1min": "1 min",
|
||||||
@ -56,6 +60,7 @@
|
|||||||
"option_last": "last",
|
"option_last": "last",
|
||||||
"option_above": "above",
|
"option_above": "above",
|
||||||
"option_below": "below",
|
"option_below": "below",
|
||||||
|
"option_above_below": "above/below",
|
||||||
"option_equal": "is equal to",
|
"option_equal": "is equal to",
|
||||||
"option_notequal": "not equal to",
|
"option_notequal": "not equal to",
|
||||||
"button_query": "Query",
|
"button_query": "Query",
|
||||||
@ -109,7 +114,9 @@
|
|||||||
"user_tooltip_more_help": "More details on how to create alerts",
|
"user_tooltip_more_help": "More details on how to create alerts",
|
||||||
"choose_alert_type": "Choose a type for the alert",
|
"choose_alert_type": "Choose a type for the alert",
|
||||||
"metric_based_alert": "Metric based Alert",
|
"metric_based_alert": "Metric based Alert",
|
||||||
|
"anomaly_based_alert": "Anomaly based Alert",
|
||||||
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
|
"metric_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
|
||||||
|
"anomaly_based_alert_desc": "Send a notification when a condition occurs in the metric data.",
|
||||||
"log_based_alert": "Log-based Alert",
|
"log_based_alert": "Log-based Alert",
|
||||||
"log_based_alert_desc": "Send a notification when a condition occurs in the logs data.",
|
"log_based_alert_desc": "Send a notification when a condition occurs in the logs data.",
|
||||||
"traces_based_alert": "Trace-based Alert",
|
"traces_based_alert": "Trace-based Alert",
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
"option_last": "last",
|
"option_last": "last",
|
||||||
"option_above": "above",
|
"option_above": "above",
|
||||||
"option_below": "below",
|
"option_below": "below",
|
||||||
|
"option_above_below": "above/below",
|
||||||
"option_equal": "is equal to",
|
"option_equal": "is equal to",
|
||||||
"option_notequal": "not equal to",
|
"option_notequal": "not equal to",
|
||||||
"button_query": "Query",
|
"button_query": "Query",
|
||||||
|
@ -2,6 +2,7 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
|
|||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
export const ALERTS_DATA_SOURCE_MAP: Record<AlertTypes, DataSource> = {
|
export const ALERTS_DATA_SOURCE_MAP: Record<AlertTypes, DataSource> = {
|
||||||
|
[AlertTypes.ANOMALY_BASED_ALERT]: DataSource.METRICS,
|
||||||
[AlertTypes.METRICS_BASED_ALERT]: DataSource.METRICS,
|
[AlertTypes.METRICS_BASED_ALERT]: DataSource.METRICS,
|
||||||
[AlertTypes.LOGS_BASED_ALERT]: DataSource.LOGS,
|
[AlertTypes.LOGS_BASED_ALERT]: DataSource.LOGS,
|
||||||
[AlertTypes.TRACES_BASED_ALERT]: DataSource.TRACES,
|
[AlertTypes.TRACES_BASED_ALERT]: DataSource.TRACES,
|
||||||
|
@ -36,4 +36,5 @@ export enum QueryParams {
|
|||||||
topic = 'topic',
|
topic = 'topic',
|
||||||
partition = 'partition',
|
partition = 'partition',
|
||||||
selectedTimelineQuery = 'selectedTimelineQuery',
|
selectedTimelineQuery = 'selectedTimelineQuery',
|
||||||
|
ruleType = 'ruleType',
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,10 @@ import { QueryFunctionsTypes } from 'types/common/queryBuilder';
|
|||||||
import { SelectOption } from 'types/common/select';
|
import { SelectOption } from 'types/common/select';
|
||||||
|
|
||||||
export const metricQueryFunctionOptions: SelectOption<string, string>[] = [
|
export const metricQueryFunctionOptions: SelectOption<string, string>[] = [
|
||||||
|
{
|
||||||
|
value: QueryFunctionsTypes.ANOMALY,
|
||||||
|
label: 'Anomaly',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
value: QueryFunctionsTypes.CUTOFF_MIN,
|
value: QueryFunctionsTypes.CUTOFF_MIN,
|
||||||
label: 'Cut Off Min',
|
label: 'Cut Off Min',
|
||||||
@ -67,6 +71,10 @@ export const metricQueryFunctionOptions: SelectOption<string, string>[] = [
|
|||||||
value: QueryFunctionsTypes.TIME_SHIFT,
|
value: QueryFunctionsTypes.TIME_SHIFT,
|
||||||
label: 'Time Shift',
|
label: 'Time Shift',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: QueryFunctionsTypes.TIME_SHIFT,
|
||||||
|
label: 'Time Shift',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const logsQueryFunctionOptions: SelectOption<string, string>[] = [
|
export const logsQueryFunctionOptions: SelectOption<string, string>[] = [
|
||||||
@ -80,10 +88,15 @@ interface QueryFunctionConfigType {
|
|||||||
showInput: boolean;
|
showInput: boolean;
|
||||||
inputType?: string;
|
inputType?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const queryFunctionsTypesConfig: QueryFunctionConfigType = {
|
export const queryFunctionsTypesConfig: QueryFunctionConfigType = {
|
||||||
|
anomaly: {
|
||||||
|
showInput: false,
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
cutOffMin: {
|
cutOffMin: {
|
||||||
showInput: true,
|
showInput: true,
|
||||||
inputType: 'text',
|
inputType: 'text',
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
.anomaly-alert-evaluation-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-chart-section {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&.has-multi-series-data {
|
||||||
|
width: calc(100% - 240px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-no-data-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-series-selection {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
width: 240px;
|
||||||
|
padding: 8px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-series-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-series-list-title {
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 13px !important;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-series-list-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-series-list-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.uplot {
|
||||||
|
.u-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 400;
|
||||||
|
display: flex;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 13px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.u-legend {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 16px;
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.u-series {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,280 @@
|
|||||||
|
import 'uplot/dist/uPlot.min.css';
|
||||||
|
import './AnomalyAlertEvaluationView.styles.scss';
|
||||||
|
|
||||||
|
import { Checkbox, Typography } from 'antd';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
|
import getAxes from 'lib/uPlotLib/utils/getAxes';
|
||||||
|
import { getUplotChartDataForAnomalyDetection } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||||
|
import { getYAxisScaleForAnomalyDetection } from 'lib/uPlotLib/utils/getYAxisScale';
|
||||||
|
import { LineChart } from 'lucide-react';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
|
function UplotChart({
|
||||||
|
data,
|
||||||
|
options,
|
||||||
|
chartRef,
|
||||||
|
}: {
|
||||||
|
data: any;
|
||||||
|
options: any;
|
||||||
|
chartRef: any;
|
||||||
|
}): JSX.Element {
|
||||||
|
const plotInstance = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (plotInstance.current) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
plotInstance.current.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
// eslint-disable-next-line new-cap
|
||||||
|
plotInstance.current = new uPlot(options, data, chartRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (): void => {
|
||||||
|
if (plotInstance.current) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
plotInstance.current.destroy();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [data, options, chartRef]);
|
||||||
|
|
||||||
|
return <div ref={chartRef} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AnomalyAlertEvaluationView({
|
||||||
|
data,
|
||||||
|
yAxisUnit,
|
||||||
|
}: {
|
||||||
|
data: any;
|
||||||
|
yAxisUnit: string;
|
||||||
|
}): JSX.Element {
|
||||||
|
const { spline } = uPlot.paths;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
const _spline = spline ? spline() : undefined;
|
||||||
|
const chartRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
const [seriesData, setSeriesData] = useState<any>({});
|
||||||
|
const [selectedSeries, setSelectedSeries] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const graphRef = useRef<HTMLDivElement>(null);
|
||||||
|
const dimensions = useResizeObserver(graphRef);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const chartData = getUplotChartDataForAnomalyDetection(data);
|
||||||
|
setSeriesData(chartData);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const seriesKeys = Object.keys(seriesData);
|
||||||
|
if (seriesKeys.length === 1) {
|
||||||
|
setSelectedSeries(seriesKeys[0]); // Automatically select if only one series
|
||||||
|
} else {
|
||||||
|
setSelectedSeries(null); // Default to "Show All" if multiple series
|
||||||
|
}
|
||||||
|
}, [seriesData]);
|
||||||
|
|
||||||
|
const handleSeriesChange = (series: string | null): void => {
|
||||||
|
setSelectedSeries(series);
|
||||||
|
};
|
||||||
|
|
||||||
|
const bandsPlugin = {
|
||||||
|
hooks: {
|
||||||
|
draw: [
|
||||||
|
(u: any): void => {
|
||||||
|
if (!selectedSeries) return;
|
||||||
|
|
||||||
|
const { ctx } = u;
|
||||||
|
const upperBandIdx = 3;
|
||||||
|
const lowerBandIdx = 4;
|
||||||
|
|
||||||
|
const xData = u.data[0];
|
||||||
|
const yUpperData = u.data[upperBandIdx];
|
||||||
|
const yLowerData = u.data[lowerBandIdx];
|
||||||
|
|
||||||
|
const strokeStyle =
|
||||||
|
u.series[1]?.stroke || seriesData[selectedSeries].color;
|
||||||
|
const fillStyle =
|
||||||
|
typeof strokeStyle === 'string'
|
||||||
|
? strokeStyle.replace(')', ', 0.1)')
|
||||||
|
: 'rgba(255, 255, 255, 0.1)';
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
const firstX = u.valToPos(xData[0], 'x', true);
|
||||||
|
const firstUpperY = u.valToPos(yUpperData[0], 'y', true);
|
||||||
|
ctx.moveTo(firstX, firstUpperY);
|
||||||
|
|
||||||
|
for (let i = 0; i < xData.length; i++) {
|
||||||
|
const x = u.valToPos(xData[i], 'x', true);
|
||||||
|
const y = u.valToPos(yUpperData[i], 'y', true);
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = xData.length - 1; i >= 0; i--) {
|
||||||
|
const x = u.valToPos(xData[i], 'x', true);
|
||||||
|
const y = u.valToPos(yLowerData[i], 'y', true);
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fillStyle = fillStyle;
|
||||||
|
ctx.fill();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const allSeries = Object.keys(seriesData);
|
||||||
|
|
||||||
|
const initialData = allSeries.length
|
||||||
|
? [
|
||||||
|
seriesData[allSeries[0]].data[0], // Shared X-axis
|
||||||
|
...allSeries.map((key) => seriesData[key].data[1]), // Map through Y-axis data for all series
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
width: dimensions.width,
|
||||||
|
height: dimensions.height - 36,
|
||||||
|
plugins: [bandsPlugin],
|
||||||
|
focus: {
|
||||||
|
alpha: 0.3,
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
label: 'Time',
|
||||||
|
},
|
||||||
|
...(selectedSeries
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: `Main Series`,
|
||||||
|
stroke: seriesData[selectedSeries].color,
|
||||||
|
width: 2,
|
||||||
|
show: true,
|
||||||
|
paths: _spline,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `Predicted Value`,
|
||||||
|
stroke: seriesData[selectedSeries].color,
|
||||||
|
width: 1,
|
||||||
|
dash: [2, 2],
|
||||||
|
show: true,
|
||||||
|
paths: _spline,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `Upper Band`,
|
||||||
|
stroke: 'transparent',
|
||||||
|
show: false,
|
||||||
|
paths: _spline,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `Lower Band`,
|
||||||
|
stroke: 'transparent',
|
||||||
|
show: false,
|
||||||
|
paths: _spline,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: allSeries.map((seriesKey) => ({
|
||||||
|
label: seriesKey,
|
||||||
|
stroke: seriesData[seriesKey].color,
|
||||||
|
width: 2,
|
||||||
|
show: true,
|
||||||
|
paths: _spline,
|
||||||
|
}))),
|
||||||
|
],
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
time: true,
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
...getYAxisScaleForAnomalyDetection({
|
||||||
|
seriesData,
|
||||||
|
selectedSeries,
|
||||||
|
initialData,
|
||||||
|
yAxisUnit,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
axes: getAxes(isDarkMode, yAxisUnit),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="anomaly-alert-evaluation-view">
|
||||||
|
<div
|
||||||
|
className={`anomaly-alert-evaluation-view-chart-section ${
|
||||||
|
allSeries.length > 1 ? 'has-multi-series-data' : ''
|
||||||
|
}`}
|
||||||
|
ref={graphRef}
|
||||||
|
>
|
||||||
|
{allSeries.length > 0 ? (
|
||||||
|
<UplotChart
|
||||||
|
data={selectedSeries ? seriesData[selectedSeries].data : initialData}
|
||||||
|
options={options}
|
||||||
|
chartRef={chartRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="anomaly-alert-evaluation-view-no-data-container">
|
||||||
|
<LineChart size={48} strokeWidth={0.5} />
|
||||||
|
|
||||||
|
<Typography>No Data</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{allSeries.length > 1 && (
|
||||||
|
<div className="anomaly-alert-evaluation-view-series-selection">
|
||||||
|
{allSeries.length > 1 && (
|
||||||
|
<div className="anomaly-alert-evaluation-view-series-list">
|
||||||
|
<Typography.Title
|
||||||
|
level={5}
|
||||||
|
className="anomaly-alert-evaluation-view-series-list-title"
|
||||||
|
>
|
||||||
|
Select a series
|
||||||
|
</Typography.Title>
|
||||||
|
<div className="anomaly-alert-evaluation-view-series-list-items">
|
||||||
|
<Checkbox
|
||||||
|
className="anomaly-alert-evaluation-view-series-list-item"
|
||||||
|
type="checkbox"
|
||||||
|
name="series"
|
||||||
|
value="all"
|
||||||
|
checked={selectedSeries === null}
|
||||||
|
onChange={(): void => handleSeriesChange(null)}
|
||||||
|
>
|
||||||
|
Show All
|
||||||
|
</Checkbox>
|
||||||
|
|
||||||
|
{allSeries.map((seriesKey) => (
|
||||||
|
<Checkbox
|
||||||
|
className="anomaly-alert-evaluation-view-series-list-item"
|
||||||
|
key={seriesKey}
|
||||||
|
type="checkbox"
|
||||||
|
name="series"
|
||||||
|
value={seriesKey}
|
||||||
|
checked={selectedSeries === seriesKey}
|
||||||
|
onChange={(): void => handleSeriesChange(seriesKey)}
|
||||||
|
>
|
||||||
|
{seriesKey}
|
||||||
|
</Checkbox>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnomalyAlertEvaluationView;
|
@ -0,0 +1,3 @@
|
|||||||
|
import AnomalyAlertEvaluationView from './AnomalyAlertEvaluationView';
|
||||||
|
|
||||||
|
export default AnomalyAlertEvaluationView;
|
@ -4,6 +4,11 @@ import { AlertTypes } from 'types/api/alerts/alertTypes';
|
|||||||
import { OptionType } from './types';
|
import { OptionType } from './types';
|
||||||
|
|
||||||
export const getOptionList = (t: TFunction): OptionType[] => [
|
export const getOptionList = (t: TFunction): OptionType[] => [
|
||||||
|
{
|
||||||
|
title: t('anomaly_based_alert'),
|
||||||
|
selection: AlertTypes.ANOMALY_BASED_ALERT,
|
||||||
|
description: t('anomaly_based_alert_desc'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t('metric_based_alert'),
|
title: t('metric_based_alert'),
|
||||||
selection: AlertTypes.METRICS_BASED_ALERT,
|
selection: AlertTypes.METRICS_BASED_ALERT,
|
||||||
|
@ -17,6 +17,10 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
|||||||
function handleRedirection(option: AlertTypes): void {
|
function handleRedirection(option: AlertTypes): void {
|
||||||
let url = '';
|
let url = '';
|
||||||
switch (option) {
|
switch (option) {
|
||||||
|
case AlertTypes.ANOMALY_BASED_ALERT:
|
||||||
|
url =
|
||||||
|
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples';
|
||||||
|
break;
|
||||||
case AlertTypes.METRICS_BASED_ALERT:
|
case AlertTypes.METRICS_BASED_ALERT:
|
||||||
url =
|
url =
|
||||||
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples';
|
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples';
|
||||||
|
@ -7,9 +7,11 @@ import {
|
|||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
import {
|
import {
|
||||||
AlertDef,
|
AlertDef,
|
||||||
|
defaultAlgorithm,
|
||||||
defaultCompareOp,
|
defaultCompareOp,
|
||||||
defaultEvalWindow,
|
defaultEvalWindow,
|
||||||
defaultMatchType,
|
defaultMatchType,
|
||||||
|
defaultSeasonality,
|
||||||
} from 'types/api/alerts/def';
|
} from 'types/api/alerts/def';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
|
|
||||||
@ -46,6 +48,8 @@ export const alertDefaults: AlertDef = {
|
|||||||
},
|
},
|
||||||
op: defaultCompareOp,
|
op: defaultCompareOp,
|
||||||
matchType: defaultMatchType,
|
matchType: defaultMatchType,
|
||||||
|
algorithm: defaultAlgorithm,
|
||||||
|
seasonality: defaultSeasonality,
|
||||||
},
|
},
|
||||||
labels: {
|
labels: {
|
||||||
severity: 'warning',
|
severity: 'warning',
|
||||||
@ -145,6 +149,7 @@ export const exceptionAlertDefaults: AlertDef = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ALERTS_VALUES_MAP: Record<AlertTypes, AlertDef> = {
|
export const ALERTS_VALUES_MAP: Record<AlertTypes, AlertDef> = {
|
||||||
|
[AlertTypes.ANOMALY_BASED_ALERT]: alertDefaults,
|
||||||
[AlertTypes.METRICS_BASED_ALERT]: alertDefaults,
|
[AlertTypes.METRICS_BASED_ALERT]: alertDefaults,
|
||||||
[AlertTypes.LOGS_BASED_ALERT]: logAlertDefaults,
|
[AlertTypes.LOGS_BASED_ALERT]: logAlertDefaults,
|
||||||
[AlertTypes.TRACES_BASED_ALERT]: traceAlertDefaults,
|
[AlertTypes.TRACES_BASED_ALERT]: traceAlertDefaults,
|
||||||
|
@ -2,7 +2,7 @@ import { Form, Row } from 'antd';
|
|||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import FormAlertRules from 'container/FormAlertRules';
|
import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@ -45,6 +45,7 @@ function CreateRules(): JSX.Element {
|
|||||||
|
|
||||||
const onSelectType = (typ: AlertTypes): void => {
|
const onSelectType = (typ: AlertTypes): void => {
|
||||||
setAlertType(typ);
|
setAlertType(typ);
|
||||||
|
|
||||||
switch (typ) {
|
switch (typ) {
|
||||||
case AlertTypes.LOGS_BASED_ALERT:
|
case AlertTypes.LOGS_BASED_ALERT:
|
||||||
setInitValues(logAlertDefaults);
|
setInitValues(logAlertDefaults);
|
||||||
@ -55,13 +56,37 @@ function CreateRules(): JSX.Element {
|
|||||||
case AlertTypes.EXCEPTIONS_BASED_ALERT:
|
case AlertTypes.EXCEPTIONS_BASED_ALERT:
|
||||||
setInitValues(exceptionAlertDefaults);
|
setInitValues(exceptionAlertDefaults);
|
||||||
break;
|
break;
|
||||||
|
case AlertTypes.ANOMALY_BASED_ALERT:
|
||||||
|
setInitValues({
|
||||||
|
...alertDefaults,
|
||||||
|
version: version || ENTITY_VERSION_V4,
|
||||||
|
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||||
|
});
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
setInitValues({
|
setInitValues({
|
||||||
...alertDefaults,
|
...alertDefaults,
|
||||||
version: version || ENTITY_VERSION_V4,
|
version: version || ENTITY_VERSION_V4,
|
||||||
|
ruleType: AlertDetectionTypes.THRESHOLD_ALERT,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
queryParams.set(QueryParams.alertType, typ);
|
|
||||||
|
queryParams.set(
|
||||||
|
QueryParams.alertType,
|
||||||
|
typ === AlertTypes.ANOMALY_BASED_ALERT
|
||||||
|
? AlertTypes.METRICS_BASED_ALERT
|
||||||
|
: typ,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typ === AlertTypes.ANOMALY_BASED_ALERT) {
|
||||||
|
queryParams.set(
|
||||||
|
QueryParams.ruleType,
|
||||||
|
AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
queryParams.set(QueryParams.ruleType, AlertDetectionTypes.THRESHOLD_ALERT);
|
||||||
|
}
|
||||||
|
|
||||||
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
|
const generatedUrl = `${location.pathname}?${queryParams.toString()}`;
|
||||||
history.replace(generatedUrl);
|
history.replace(generatedUrl);
|
||||||
};
|
};
|
||||||
|
@ -7,18 +7,16 @@ function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
|
|||||||
const [formInstance] = Form.useForm();
|
const [formInstance] = Form.useForm();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginTop: '1rem' }}>
|
<FormAlertRules
|
||||||
<FormAlertRules
|
alertType={
|
||||||
alertType={
|
initialValue.alertType
|
||||||
initialValue.alertType
|
? (initialValue.alertType as AlertTypes)
|
||||||
? (initialValue.alertType as AlertTypes)
|
: AlertTypes.METRICS_BASED_ALERT
|
||||||
: AlertTypes.METRICS_BASED_ALERT
|
}
|
||||||
}
|
formInstance={formInstance}
|
||||||
formInstance={formInstance}
|
initialValue={initialValue}
|
||||||
initialValue={initialValue}
|
ruleId={ruleId}
|
||||||
ruleId={ruleId}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ function BasicInfo({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StepHeading> {t('alert_form_step3')} </StepHeading>
|
<StepHeading> {t('alert_form_step4')} </StepHeading>
|
||||||
<FormContainer>
|
<FormContainer>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('field_severity')}
|
label={t('field_severity')}
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
.alert-chart-container {
|
||||||
|
height: 57vh;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.threshold-alert-uplot-chart-container {
|
||||||
|
height: calc(100% - 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-loading-container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anomaly-alert-evaluation-view-error-container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,11 @@
|
|||||||
|
import './ChartPreview.styles.scss';
|
||||||
|
|
||||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
|
||||||
import GridPanelSwitch from 'container/GridPanelSwitch';
|
import GridPanelSwitch from 'container/GridPanelSwitch';
|
||||||
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||||
@ -34,6 +37,7 @@ import { getGraphType } from 'utils/getGraphType';
|
|||||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||||
import { getTimeRange } from 'utils/getTimeRange';
|
import { getTimeRange } from 'utils/getTimeRange';
|
||||||
|
|
||||||
|
import { AlertDetectionTypes } from '..';
|
||||||
import { ChartContainer, FailedMessageContainer } from './styles';
|
import { ChartContainer, FailedMessageContainer } from './styles';
|
||||||
import { getThresholdLabel } from './utils';
|
import { getThresholdLabel } from './utils';
|
||||||
|
|
||||||
@ -141,6 +145,7 @@ function ChartPreview({
|
|||||||
selectedInterval,
|
selectedInterval,
|
||||||
minTime,
|
minTime,
|
||||||
maxTime,
|
maxTime,
|
||||||
|
alertDef?.ruleType,
|
||||||
],
|
],
|
||||||
retry: false,
|
retry: false,
|
||||||
enabled: canQuery,
|
enabled: canQuery,
|
||||||
@ -163,8 +168,6 @@ function ChartPreview({
|
|||||||
queryResponse.data.payload.data.result = sortedSeriesData;
|
queryResponse.data.payload.data.result = sortedSeriesData;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chartData = getUPlotChartData(queryResponse?.data?.payload);
|
|
||||||
|
|
||||||
const containerDimensions = useResizeObserver(graphRef);
|
const containerDimensions = useResizeObserver(graphRef);
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
@ -202,7 +205,10 @@ function ChartPreview({
|
|||||||
id: 'alert_legend_widget',
|
id: 'alert_legend_widget',
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
apiResponse: queryResponse?.data?.payload,
|
apiResponse: queryResponse?.data?.payload,
|
||||||
dimensions: containerDimensions,
|
dimensions: {
|
||||||
|
height: containerDimensions?.height ? containerDimensions.height - 48 : 0,
|
||||||
|
width: containerDimensions?.width,
|
||||||
|
},
|
||||||
minTimeScale,
|
minTimeScale,
|
||||||
maxTimeScale,
|
maxTimeScale,
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
@ -245,36 +251,55 @@ function ChartPreview({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const chartData = getUPlotChartData(queryResponse?.data?.payload);
|
||||||
|
|
||||||
|
const isAnomalyDetectionAlert =
|
||||||
|
alertDef?.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT;
|
||||||
|
|
||||||
|
const chartDataAvailable =
|
||||||
|
chartData && !queryResponse.isError && !queryResponse.isLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartContainer>
|
<div className="alert-chart-container" ref={graphRef}>
|
||||||
{headline}
|
<ChartContainer>
|
||||||
|
{headline}
|
||||||
|
|
||||||
<div ref={graphRef} style={{ height: '100%' }}>
|
<div className="threshold-alert-uplot-chart-container">
|
||||||
{queryResponse.isLoading && (
|
{queryResponse.isLoading && (
|
||||||
<Spinner size="large" tip="Loading..." height="100%" />
|
<Spinner size="large" tip="Loading..." height="100%" />
|
||||||
)}
|
)}
|
||||||
{(queryResponse?.isError || queryResponse?.error) && (
|
{(queryResponse?.isError || queryResponse?.error) && (
|
||||||
<FailedMessageContainer color="red" title="Failed to refresh the chart">
|
<FailedMessageContainer color="red" title="Failed to refresh the chart">
|
||||||
<InfoCircleOutlined />{' '}
|
<InfoCircleOutlined />
|
||||||
{queryResponse.error.message || t('preview_chart_unexpected_error')}
|
{queryResponse.error.message || t('preview_chart_unexpected_error')}
|
||||||
</FailedMessageContainer>
|
</FailedMessageContainer>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{chartData && !queryResponse.isError && !queryResponse.isLoading && (
|
{chartDataAvailable && !isAnomalyDetectionAlert && (
|
||||||
<GridPanelSwitch
|
<GridPanelSwitch
|
||||||
options={options}
|
options={options}
|
||||||
panelType={graphType}
|
panelType={graphType}
|
||||||
data={chartData}
|
data={chartData}
|
||||||
name={name || 'Chart Preview'}
|
name={name || 'Chart Preview'}
|
||||||
panelData={
|
panelData={
|
||||||
queryResponse.data?.payload?.data?.newResult?.data?.result || []
|
queryResponse.data?.payload?.data?.newResult?.data?.result || []
|
||||||
}
|
}
|
||||||
query={query || initialQueriesMap.metrics}
|
query={query || initialQueriesMap.metrics}
|
||||||
yAxisUnit={yAxisUnit}
|
yAxisUnit={yAxisUnit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</ChartContainer>
|
{chartDataAvailable &&
|
||||||
|
isAnomalyDetectionAlert &&
|
||||||
|
queryResponse?.data?.payload?.data?.resultType === 'anomaly' && (
|
||||||
|
<AnomalyAlertEvaluationView
|
||||||
|
data={queryResponse?.data?.payload}
|
||||||
|
yAxisUnit={yAxisUnit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ChartContainer>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,70 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.steps-container {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qb-chart-preview-container {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-header {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.alert-type-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.alert-type-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-typography {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-preview-container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.ant-card {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detection-method-container {
|
||||||
|
margin: 24px 0;
|
||||||
|
|
||||||
|
.ant-tabs-nav {
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
.ant-tabs-tab {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detection-method-description {
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.info-help-btns {
|
.info-help-btns {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto auto;
|
grid-template-columns: auto auto;
|
||||||
|
@ -222,7 +222,7 @@ function QuerySection({
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StepHeading> {t('alert_form_step1')}</StepHeading>
|
<StepHeading> {t('alert_form_step2')}</StepHeading>
|
||||||
<FormContainer>
|
<FormContainer>
|
||||||
<div>{renderTabs(alertType)}</div>
|
<div>{renderTabs(alertType)}</div>
|
||||||
{renderQuerySection(currentTab)}
|
{renderQuerySection(currentTab)}
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
.rule-definition {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
import './RuleOptions.styles.scss';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Collapse,
|
Collapse,
|
||||||
@ -18,14 +20,17 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
AlertDef,
|
AlertDef,
|
||||||
|
defaultAlgorithm,
|
||||||
defaultCompareOp,
|
defaultCompareOp,
|
||||||
defaultEvalWindow,
|
defaultEvalWindow,
|
||||||
defaultFrequency,
|
defaultFrequency,
|
||||||
defaultMatchType,
|
defaultMatchType,
|
||||||
|
defaultSeasonality,
|
||||||
} from 'types/api/alerts/def';
|
} from 'types/api/alerts/def';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
|
import { AlertDetectionTypes } from '.';
|
||||||
import {
|
import {
|
||||||
FormContainer,
|
FormContainer,
|
||||||
InlineSelect,
|
InlineSelect,
|
||||||
@ -43,6 +48,8 @@ function RuleOptions({
|
|||||||
const { t } = useTranslation('alerts');
|
const { t } = useTranslation('alerts');
|
||||||
const { currentQuery } = useQueryBuilder();
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
|
||||||
|
const { ruleType } = alertDef;
|
||||||
|
|
||||||
const handleMatchOptChange = (value: string | unknown): void => {
|
const handleMatchOptChange = (value: string | unknown): void => {
|
||||||
const m = (value as string) || alertDef.condition?.matchType;
|
const m = (value as string) || alertDef.condition?.matchType;
|
||||||
setAlertDef({
|
setAlertDef({
|
||||||
@ -86,8 +93,19 @@ function RuleOptions({
|
|||||||
>
|
>
|
||||||
<Select.Option value="1">{t('option_above')}</Select.Option>
|
<Select.Option value="1">{t('option_above')}</Select.Option>
|
||||||
<Select.Option value="2">{t('option_below')}</Select.Option>
|
<Select.Option value="2">{t('option_below')}</Select.Option>
|
||||||
<Select.Option value="3">{t('option_equal')}</Select.Option>
|
|
||||||
<Select.Option value="4">{t('option_notequal')}</Select.Option>
|
{/* hide equal and not eqaul in case of analmoy based alert */}
|
||||||
|
|
||||||
|
{ruleType !== 'anomaly_rule' && (
|
||||||
|
<>
|
||||||
|
<Select.Option value="3">{t('option_equal')}</Select.Option>
|
||||||
|
<Select.Option value="4">{t('option_notequal')}</Select.Option>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ruleType === 'anomaly_rule' && (
|
||||||
|
<Select.Option value="5">{t('option_above_below')}</Select.Option>
|
||||||
|
)}
|
||||||
</InlineSelect>
|
</InlineSelect>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -101,9 +119,14 @@ function RuleOptions({
|
|||||||
>
|
>
|
||||||
<Select.Option value="1">{t('option_atleastonce')}</Select.Option>
|
<Select.Option value="1">{t('option_atleastonce')}</Select.Option>
|
||||||
<Select.Option value="2">{t('option_allthetimes')}</Select.Option>
|
<Select.Option value="2">{t('option_allthetimes')}</Select.Option>
|
||||||
<Select.Option value="3">{t('option_onaverage')}</Select.Option>
|
|
||||||
<Select.Option value="4">{t('option_intotal')}</Select.Option>
|
{ruleType !== 'anomaly_rule' && (
|
||||||
<Select.Option value="5">{t('option_last')}</Select.Option>
|
<>
|
||||||
|
<Select.Option value="3">{t('option_onaverage')}</Select.Option>
|
||||||
|
<Select.Option value="4">{t('option_intotal')}</Select.Option>
|
||||||
|
<Select.Option value="5">{t('option_last')}</Select.Option>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</InlineSelect>
|
</InlineSelect>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -115,6 +138,28 @@ function RuleOptions({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onChangeAlgorithm = (value: string | unknown): void => {
|
||||||
|
const alg = (value as string) || alertDef.condition.algorithm;
|
||||||
|
setAlertDef({
|
||||||
|
...alertDef,
|
||||||
|
condition: {
|
||||||
|
...alertDef.condition,
|
||||||
|
algorithm: alg,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeSeasonality = (value: string | unknown): void => {
|
||||||
|
const seasonality = (value as string) || alertDef.condition.seasonality;
|
||||||
|
setAlertDef({
|
||||||
|
...alertDef,
|
||||||
|
condition: {
|
||||||
|
...alertDef.condition,
|
||||||
|
seasonality,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const renderEvalWindows = (): JSX.Element => (
|
const renderEvalWindows = (): JSX.Element => (
|
||||||
<InlineSelect
|
<InlineSelect
|
||||||
getPopupContainer={popupContainer}
|
getPopupContainer={popupContainer}
|
||||||
@ -146,6 +191,32 @@ function RuleOptions({
|
|||||||
</InlineSelect>
|
</InlineSelect>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderAlgorithms = (): JSX.Element => (
|
||||||
|
<InlineSelect
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
|
defaultValue={defaultAlgorithm}
|
||||||
|
style={{ minWidth: '120px' }}
|
||||||
|
value={alertDef.condition.algorithm}
|
||||||
|
onChange={onChangeAlgorithm}
|
||||||
|
>
|
||||||
|
<Select.Option value="standard">Standard</Select.Option>
|
||||||
|
</InlineSelect>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderSeasonality = (): JSX.Element => (
|
||||||
|
<InlineSelect
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
|
defaultValue={defaultSeasonality}
|
||||||
|
style={{ minWidth: '120px' }}
|
||||||
|
value={alertDef.condition.seasonality}
|
||||||
|
onChange={onChangeSeasonality}
|
||||||
|
>
|
||||||
|
<Select.Option value="hourly">Hourly</Select.Option>
|
||||||
|
<Select.Option value="daily">Daily</Select.Option>
|
||||||
|
<Select.Option value="weekly">Weekly</Select.Option>
|
||||||
|
</InlineSelect>
|
||||||
|
);
|
||||||
|
|
||||||
const renderThresholdRuleOpts = (): JSX.Element => (
|
const renderThresholdRuleOpts = (): JSX.Element => (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
@ -166,6 +237,39 @@ function RuleOptions({
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderAnomalyRuleOpts = (
|
||||||
|
onChange: InputNumberProps['onChange'],
|
||||||
|
): JSX.Element => (
|
||||||
|
<Form.Item>
|
||||||
|
<Typography.Text className="rule-definition">
|
||||||
|
{t('text_condition1_anomaly')}
|
||||||
|
<InlineSelect
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
|
allowClear
|
||||||
|
showSearch
|
||||||
|
options={queryOptions}
|
||||||
|
placeholder={t('selected_query_placeholder')}
|
||||||
|
value={alertDef.condition.selectedQueryName}
|
||||||
|
onChange={onChangeSelectedQueryName}
|
||||||
|
/>
|
||||||
|
{t('text_condition3')} {renderEvalWindows()}
|
||||||
|
<Typography.Text>is</Typography.Text>
|
||||||
|
<InputNumber
|
||||||
|
value={alertDef?.condition?.target}
|
||||||
|
onChange={onChange}
|
||||||
|
type="number"
|
||||||
|
onWheel={(e): void => e.currentTarget.blur()}
|
||||||
|
/>
|
||||||
|
<Typography.Text>deviations</Typography.Text>
|
||||||
|
{renderCompareOps()}
|
||||||
|
<Typography.Text>the predicted data</Typography.Text>
|
||||||
|
{renderMatchOpts()}
|
||||||
|
using the {renderAlgorithms()} algorithm with {renderSeasonality()}{' '}
|
||||||
|
seasonality
|
||||||
|
</Typography.Text>
|
||||||
|
</Form.Item>
|
||||||
|
);
|
||||||
|
|
||||||
const renderPromRuleOptions = (): JSX.Element => (
|
const renderPromRuleOptions = (): JSX.Element => (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
@ -245,36 +349,46 @@ function RuleOptions({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StepHeading>{t('alert_form_step2')}</StepHeading>
|
<StepHeading>{t('alert_form_step3')}</StepHeading>
|
||||||
<FormContainer>
|
<FormContainer>
|
||||||
{queryCategory === EQueryType.PROM
|
{queryCategory === EQueryType.PROM && renderPromRuleOptions()}
|
||||||
? renderPromRuleOptions()
|
{queryCategory !== EQueryType.PROM &&
|
||||||
: renderThresholdRuleOpts()}
|
ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT && (
|
||||||
|
<>{renderAnomalyRuleOpts(onChange)}</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{queryCategory !== EQueryType.PROM &&
|
||||||
|
ruleType === AlertDetectionTypes.THRESHOLD_ALERT &&
|
||||||
|
renderThresholdRuleOpts()}
|
||||||
|
|
||||||
<Space direction="vertical" size="large">
|
<Space direction="vertical" size="large">
|
||||||
<Space direction="horizontal" align="center">
|
{queryCategory !== EQueryType.PROM &&
|
||||||
<Form.Item noStyle name={['condition', 'target']}>
|
ruleType !== AlertDetectionTypes.ANOMALY_DETECTION_ALERT && (
|
||||||
<InputNumber
|
<Space direction="horizontal" align="center">
|
||||||
addonBefore={t('field_threshold')}
|
<Form.Item noStyle name={['condition', 'target']}>
|
||||||
value={alertDef?.condition?.target}
|
<InputNumber
|
||||||
onChange={onChange}
|
addonBefore={t('field_threshold')}
|
||||||
type="number"
|
value={alertDef?.condition?.target}
|
||||||
onWheel={(e): void => e.currentTarget.blur()}
|
onChange={onChange}
|
||||||
/>
|
type="number"
|
||||||
</Form.Item>
|
onWheel={(e): void => e.currentTarget.blur()}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item noStyle>
|
||||||
|
<Select
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
|
allowClear
|
||||||
|
showSearch
|
||||||
|
options={categorySelectOptions}
|
||||||
|
placeholder={t('field_unit')}
|
||||||
|
value={alertDef.condition.targetUnit}
|
||||||
|
onChange={onChangeAlertUnit}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
|
||||||
<Form.Item noStyle>
|
|
||||||
<Select
|
|
||||||
getPopupContainer={popupContainer}
|
|
||||||
allowClear
|
|
||||||
showSearch
|
|
||||||
options={categorySelectOptions}
|
|
||||||
placeholder={t('field_unit')}
|
|
||||||
value={alertDef.condition.targetUnit}
|
|
||||||
onChange={onChangeAlertUnit}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Space>
|
|
||||||
<Collapse>
|
<Collapse>
|
||||||
<Collapse.Panel header={t('More options')} key="1">
|
<Collapse.Panel header={t('More options')} key="1">
|
||||||
<Space direction="vertical" size="large">
|
<Space direction="vertical" size="large">
|
||||||
|
@ -3,7 +3,6 @@ import './FormAlertRules.styles.scss';
|
|||||||
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Col,
|
|
||||||
FormInstance,
|
FormInstance,
|
||||||
Modal,
|
Modal,
|
||||||
SelectProps,
|
SelectProps,
|
||||||
@ -13,8 +12,6 @@ import {
|
|||||||
import saveAlertApi from 'api/alerts/save';
|
import saveAlertApi from 'api/alerts/save';
|
||||||
import testAlertApi from 'api/alerts/testAlert';
|
import testAlertApi from 'api/alerts/testAlert';
|
||||||
import logEvent from 'api/common/logEvent';
|
import logEvent from 'api/common/logEvent';
|
||||||
import LaunchChatSupport from 'components/LaunchChatSupport/LaunchChatSupport';
|
|
||||||
import { alertHelpMessage } from 'components/LaunchChatSupport/util';
|
|
||||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||||
import { FeatureKeys } from 'constants/features';
|
import { FeatureKeys } from 'constants/features';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
@ -33,6 +30,8 @@ import history from 'lib/history';
|
|||||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { BellDot, ExternalLink } from 'lucide-react';
|
||||||
|
import Tabs2 from 'periscope/components/Tabs2';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQueryClient } from 'react-query';
|
import { useQueryClient } from 'react-query';
|
||||||
@ -44,7 +43,11 @@ import {
|
|||||||
defaultEvalWindow,
|
defaultEvalWindow,
|
||||||
defaultMatchType,
|
defaultMatchType,
|
||||||
} from 'types/api/alerts/def';
|
} from 'types/api/alerts/def';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import {
|
||||||
|
IBuilderQuery,
|
||||||
|
Query,
|
||||||
|
QueryFunctionProps,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
@ -56,13 +59,16 @@ import {
|
|||||||
ActionButton,
|
ActionButton,
|
||||||
ButtonContainer,
|
ButtonContainer,
|
||||||
MainFormContainer,
|
MainFormContainer,
|
||||||
PanelContainer,
|
|
||||||
StepContainer,
|
StepContainer,
|
||||||
StyledLeftContainer,
|
StepHeading,
|
||||||
} from './styles';
|
} from './styles';
|
||||||
import UserGuide from './UserGuide';
|
|
||||||
import { getSelectedQueryOptions } from './utils';
|
import { getSelectedQueryOptions } from './utils';
|
||||||
|
|
||||||
|
export enum AlertDetectionTypes {
|
||||||
|
THRESHOLD_ALERT = 'threshold_rule',
|
||||||
|
ANOMALY_DETECTION_ALERT = 'anomaly_rule',
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
function FormAlertRules({
|
function FormAlertRules({
|
||||||
alertType,
|
alertType,
|
||||||
@ -86,6 +92,7 @@ function FormAlertRules({
|
|||||||
const {
|
const {
|
||||||
currentQuery,
|
currentQuery,
|
||||||
stagedQuery,
|
stagedQuery,
|
||||||
|
handleSetQueryData,
|
||||||
handleRunQuery,
|
handleRunQuery,
|
||||||
handleSetConfig,
|
handleSetConfig,
|
||||||
initialDataSource,
|
initialDataSource,
|
||||||
@ -108,6 +115,12 @@ function FormAlertRules({
|
|||||||
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
|
const [alertDef, setAlertDef] = useState<AlertDef>(initialValue);
|
||||||
const [yAxisUnit, setYAxisUnit] = useState<string>(currentQuery.unit || '');
|
const [yAxisUnit, setYAxisUnit] = useState<string>(currentQuery.unit || '');
|
||||||
|
|
||||||
|
const alertTypeFromURL = urlQuery.get(QueryParams.ruleType);
|
||||||
|
|
||||||
|
const [detectionMethod, setDetectionMethod] = useState<string>(
|
||||||
|
AlertDetectionTypes.THRESHOLD_ALERT,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isEqual(currentQuery.unit, yAxisUnit)) {
|
if (!isEqual(currentQuery.unit, yAxisUnit)) {
|
||||||
setYAxisUnit(currentQuery.unit || '');
|
setYAxisUnit(currentQuery.unit || '');
|
||||||
@ -138,6 +151,45 @@ function FormAlertRules({
|
|||||||
|
|
||||||
useShareBuilderUrl(sq);
|
useShareBuilderUrl(sq);
|
||||||
|
|
||||||
|
const updateFunctions = (data: IBuilderQuery): QueryFunctionProps[] => {
|
||||||
|
const anomalyFunction = {
|
||||||
|
name: 'anomaly',
|
||||||
|
args: [],
|
||||||
|
namedArgs: { z_score_threshold: 9 },
|
||||||
|
};
|
||||||
|
const functions = data.functions || [];
|
||||||
|
|
||||||
|
if (alertDef.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
|
||||||
|
// Add anomaly if not already present
|
||||||
|
if (!functions.some((func) => func.name === 'anomaly')) {
|
||||||
|
functions.push(anomalyFunction);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove anomaly if present
|
||||||
|
const index = functions.findIndex((func) => func.name === 'anomaly');
|
||||||
|
if (index !== -1) {
|
||||||
|
functions.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return functions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFunctionsBasedOnAlertType = (): void => {
|
||||||
|
for (let index = 0; index < currentQuery.builder.queryData.length; index++) {
|
||||||
|
const queryData = currentQuery.builder.queryData[index];
|
||||||
|
|
||||||
|
const updatedFunctions = updateFunctions(queryData);
|
||||||
|
queryData.functions = updatedFunctions;
|
||||||
|
handleSetQueryData(index, queryData);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateFunctionsBasedOnAlertType();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [detectionMethod, alertDef, currentQuery.builder.queryData.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const broadcastToSpecificChannels =
|
const broadcastToSpecificChannels =
|
||||||
(initialValue &&
|
(initialValue &&
|
||||||
@ -145,11 +197,22 @@ function FormAlertRules({
|
|||||||
initialValue.preferredChannels.length > 0) ||
|
initialValue.preferredChannels.length > 0) ||
|
||||||
isNewRule;
|
isNewRule;
|
||||||
|
|
||||||
|
let ruleType = AlertDetectionTypes.THRESHOLD_ALERT;
|
||||||
|
|
||||||
|
if (initialValue.ruleType) {
|
||||||
|
ruleType = initialValue.ruleType as AlertDetectionTypes;
|
||||||
|
} else if (alertTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
|
||||||
|
ruleType = AlertDetectionTypes.ANOMALY_DETECTION_ALERT;
|
||||||
|
}
|
||||||
|
|
||||||
setAlertDef({
|
setAlertDef({
|
||||||
...initialValue,
|
...initialValue,
|
||||||
broadcastToAll: !broadcastToSpecificChannels,
|
broadcastToAll: !broadcastToSpecificChannels,
|
||||||
|
ruleType,
|
||||||
});
|
});
|
||||||
}, [initialValue, isNewRule]);
|
|
||||||
|
setDetectionMethod(ruleType);
|
||||||
|
}, [initialValue, isNewRule, alertTypeFromURL]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Set selectedQueryName based on the length of queryOptions
|
// Set selectedQueryName based on the length of queryOptions
|
||||||
@ -300,12 +363,15 @@ function FormAlertRules({
|
|||||||
const postableAlert: AlertDef = {
|
const postableAlert: AlertDef = {
|
||||||
...alertDef,
|
...alertDef,
|
||||||
preferredChannels: alertDef.broadcastToAll ? [] : alertDef.preferredChannels,
|
preferredChannels: alertDef.broadcastToAll ? [] : alertDef.preferredChannels,
|
||||||
alertType,
|
alertType:
|
||||||
|
alertType === AlertTypes.ANOMALY_BASED_ALERT
|
||||||
|
? AlertTypes.METRICS_BASED_ALERT
|
||||||
|
: alertType,
|
||||||
source: window?.location.toString(),
|
source: window?.location.toString(),
|
||||||
ruleType:
|
ruleType:
|
||||||
currentQuery.queryType === EQueryType.PROM
|
currentQuery.queryType === EQueryType.PROM
|
||||||
? 'promql_rule'
|
? 'promql_rule'
|
||||||
: 'threshold_rule',
|
: alertDef.ruleType,
|
||||||
condition: {
|
condition: {
|
||||||
...alertDef.condition,
|
...alertDef.condition,
|
||||||
compositeQuery: {
|
compositeQuery: {
|
||||||
@ -322,6 +388,12 @@ function FormAlertRules({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (alertDef.ruleType === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
|
||||||
|
postableAlert.condition.algorithm = alertDef.condition.algorithm;
|
||||||
|
postableAlert.condition.seasonality = alertDef.condition.seasonality;
|
||||||
|
}
|
||||||
|
|
||||||
return postableAlert;
|
return postableAlert;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -585,63 +657,97 @@ function FormAlertRules({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function handleRedirection(option: AlertTypes): void {
|
const tabs = [
|
||||||
let url = '';
|
{
|
||||||
switch (option) {
|
value: AlertDetectionTypes.THRESHOLD_ALERT,
|
||||||
case AlertTypes.METRICS_BASED_ALERT:
|
label: 'Threshold Alert',
|
||||||
url =
|
},
|
||||||
'https://signoz.io/docs/alerts-management/metrics-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
|
{
|
||||||
break;
|
value: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||||
case AlertTypes.LOGS_BASED_ALERT:
|
label: 'Anomaly Detection Alert',
|
||||||
url =
|
},
|
||||||
'https://signoz.io/docs/alerts-management/log-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
|
];
|
||||||
break;
|
|
||||||
case AlertTypes.TRACES_BASED_ALERT:
|
const handleDetectionMethodChange = (value: any): void => {
|
||||||
url =
|
setAlertDef((def) => ({
|
||||||
'https://signoz.io/docs/alerts-management/trace-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
|
...def,
|
||||||
break;
|
ruleType: value,
|
||||||
case AlertTypes.EXCEPTIONS_BASED_ALERT:
|
}));
|
||||||
url =
|
|
||||||
'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-creation-page#examples';
|
setDetectionMethod(value);
|
||||||
break;
|
};
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
logEvent('Alert: Check example alert clicked', {
|
|
||||||
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
|
|
||||||
isNewRule: !ruleId || ruleId === 0,
|
|
||||||
ruleId,
|
|
||||||
queryType: currentQuery.queryType,
|
|
||||||
link: url,
|
|
||||||
});
|
|
||||||
window.open(url, '_blank');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{Element}
|
{Element}
|
||||||
|
|
||||||
<PanelContainer id="top">
|
<div id="top">
|
||||||
<StyledLeftContainer flex="5 1 600px" md={18}>
|
<div className="overview-header">
|
||||||
<MainFormContainer
|
<div className="alert-type-container">
|
||||||
initialValues={initialValue}
|
{isNewRule && (
|
||||||
layout="vertical"
|
<Typography.Title level={5} className="alert-type-title">
|
||||||
form={formInstance}
|
<BellDot size={14} />
|
||||||
className="main-container"
|
|
||||||
>
|
{alertDef.alertType === AlertTypes.ANOMALY_BASED_ALERT &&
|
||||||
|
'Anomaly Detection Alert'}
|
||||||
|
{alertDef.alertType === AlertTypes.METRICS_BASED_ALERT &&
|
||||||
|
'Metrics Based Alert'}
|
||||||
|
{alertDef.alertType === AlertTypes.LOGS_BASED_ALERT &&
|
||||||
|
'Logs Based Alert'}
|
||||||
|
{alertDef.alertType === AlertTypes.TRACES_BASED_ALERT &&
|
||||||
|
'Traces Based Alert'}
|
||||||
|
{alertDef.alertType === AlertTypes.EXCEPTIONS_BASED_ALERT &&
|
||||||
|
'Exceptions Based Alert'}
|
||||||
|
</Typography.Title>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button className="periscope-btn" icon={<ExternalLink size={14} />}>
|
||||||
|
Alert Setup Guide
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<MainFormContainer
|
||||||
|
initialValues={initialValue}
|
||||||
|
layout="vertical"
|
||||||
|
form={formInstance}
|
||||||
|
className="main-container"
|
||||||
|
>
|
||||||
|
<div className="chart-preview-container">
|
||||||
{currentQuery.queryType === EQueryType.QUERY_BUILDER &&
|
{currentQuery.queryType === EQueryType.QUERY_BUILDER &&
|
||||||
renderQBChartPreview()}
|
renderQBChartPreview()}
|
||||||
{currentQuery.queryType === EQueryType.PROM &&
|
{currentQuery.queryType === EQueryType.PROM &&
|
||||||
renderPromAndChQueryChartPreview()}
|
renderPromAndChQueryChartPreview()}
|
||||||
{currentQuery.queryType === EQueryType.CLICKHOUSE &&
|
{currentQuery.queryType === EQueryType.CLICKHOUSE &&
|
||||||
renderPromAndChQueryChartPreview()}
|
renderPromAndChQueryChartPreview()}
|
||||||
|
</div>
|
||||||
|
|
||||||
<StepContainer>
|
<StepContainer>
|
||||||
<BuilderUnitsFilter
|
<BuilderUnitsFilter
|
||||||
onChange={onUnitChangeHandler}
|
onChange={onUnitChangeHandler}
|
||||||
yAxisUnit={yAxisUnit}
|
yAxisUnit={yAxisUnit}
|
||||||
/>
|
/>
|
||||||
</StepContainer>
|
</StepContainer>
|
||||||
|
|
||||||
|
<div className="steps-container">
|
||||||
|
{alertDef.alertType === AlertTypes.METRICS_BASED_ALERT && (
|
||||||
|
<div className="detection-method-container">
|
||||||
|
<StepHeading> {t('alert_form_step1')}</StepHeading>
|
||||||
|
|
||||||
|
<Tabs2
|
||||||
|
key={detectionMethod}
|
||||||
|
tabs={tabs}
|
||||||
|
initialSelectedTab={detectionMethod}
|
||||||
|
onSelectTab={handleDetectionMethodChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="detection-method-description">
|
||||||
|
{detectionMethod === AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||||
|
? t('anomaly_detection_alert_desc')
|
||||||
|
: t('threshold_alert_desc')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<QuerySection
|
<QuerySection
|
||||||
queryCategory={currentQuery.queryType}
|
queryCategory={currentQuery.queryType}
|
||||||
@ -662,79 +768,49 @@ function FormAlertRules({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{renderBasicInfo()}
|
{renderBasicInfo()}
|
||||||
<ButtonContainer>
|
</div>
|
||||||
<Tooltip title={isAlertAvailableToSave ? MESSAGE.ALERT : ''}>
|
<ButtonContainer>
|
||||||
<ActionButton
|
<Tooltip title={isAlertAvailableToSave ? MESSAGE.ALERT : ''}>
|
||||||
loading={loading || false}
|
|
||||||
type="primary"
|
|
||||||
onClick={onSaveHandler}
|
|
||||||
icon={<SaveOutlined />}
|
|
||||||
disabled={
|
|
||||||
isAlertNameMissing ||
|
|
||||||
isAlertAvailableToSave ||
|
|
||||||
!isChannelConfigurationValid ||
|
|
||||||
queryStatus === 'error'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isNewRule ? t('button_createrule') : t('button_savechanges')}
|
|
||||||
</ActionButton>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<ActionButton
|
<ActionButton
|
||||||
loading={loading || false}
|
loading={loading || false}
|
||||||
|
type="primary"
|
||||||
|
onClick={onSaveHandler}
|
||||||
|
icon={<SaveOutlined />}
|
||||||
disabled={
|
disabled={
|
||||||
isAlertNameMissing ||
|
isAlertNameMissing ||
|
||||||
|
isAlertAvailableToSave ||
|
||||||
!isChannelConfigurationValid ||
|
!isChannelConfigurationValid ||
|
||||||
queryStatus === 'error'
|
queryStatus === 'error'
|
||||||
}
|
}
|
||||||
type="default"
|
|
||||||
onClick={onTestRuleHandler}
|
|
||||||
>
|
>
|
||||||
{' '}
|
{isNewRule ? t('button_createrule') : t('button_savechanges')}
|
||||||
{t('button_testrule')}
|
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton
|
</Tooltip>
|
||||||
disabled={loading || false}
|
|
||||||
type="default"
|
<ActionButton
|
||||||
onClick={onCancelHandler}
|
loading={loading || false}
|
||||||
>
|
disabled={
|
||||||
{ruleId === 0 && t('button_cancelchanges')}
|
isAlertNameMissing ||
|
||||||
{ruleId > 0 && t('button_discard')}
|
!isChannelConfigurationValid ||
|
||||||
</ActionButton>
|
queryStatus === 'error'
|
||||||
</ButtonContainer>
|
|
||||||
</MainFormContainer>
|
|
||||||
</StyledLeftContainer>
|
|
||||||
<Col flex="1 1 300px">
|
|
||||||
<UserGuide queryType={currentQuery.queryType} />
|
|
||||||
<div className="info-help-btns">
|
|
||||||
<Button
|
|
||||||
style={{ height: 32 }}
|
|
||||||
onClick={(): void =>
|
|
||||||
handleRedirection(alertDef?.alertType as AlertTypes)
|
|
||||||
}
|
}
|
||||||
className="doc-redirection-btn"
|
type="default"
|
||||||
|
onClick={onTestRuleHandler}
|
||||||
>
|
>
|
||||||
Check an example alert
|
{' '}
|
||||||
</Button>
|
{t('button_testrule')}
|
||||||
<LaunchChatSupport
|
</ActionButton>
|
||||||
attributes={{
|
<ActionButton
|
||||||
alert: alertDef?.alert,
|
disabled={loading || false}
|
||||||
alertType: alertDef?.alertType,
|
type="default"
|
||||||
id: ruleId,
|
onClick={onCancelHandler}
|
||||||
ruleType: alertDef?.ruleType,
|
>
|
||||||
state: (alertDef as any)?.state,
|
{ruleId === 0 && t('button_cancelchanges')}
|
||||||
panelType,
|
{ruleId > 0 && t('button_discard')}
|
||||||
screen: isRuleCreated ? 'Edit Alert' : 'New Alert',
|
</ActionButton>
|
||||||
}}
|
</ButtonContainer>
|
||||||
className="facing-issue-btn"
|
</MainFormContainer>
|
||||||
eventName="Alert: Facing Issues in alert"
|
</div>
|
||||||
buttonText="Need help with this alert?"
|
|
||||||
message={alertHelpMessage(alertDef, ruleId)}
|
|
||||||
onHoverText="Click here to get help with this alert"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</PanelContainer>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
import { Button, Card, Col, Form, Input, Row, Select, Typography } from 'antd';
|
import { Button, Card, Col, Form, Input, Select, Typography } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
const { Item } = Form;
|
const { Item } = Form;
|
||||||
|
|
||||||
export const PanelContainer = styled(Row)`
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const StyledLeftContainer = styled(Col)`
|
export const StyledLeftContainer = styled(Col)`
|
||||||
&&& {
|
&&& {
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
|
@ -33,7 +33,7 @@ export default function Function({
|
|||||||
handleDeleteFunction,
|
handleDeleteFunction,
|
||||||
}: FunctionProps): JSX.Element {
|
}: FunctionProps): JSX.Element {
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const { showInput } = queryFunctionsTypesConfig[funcData.name];
|
const { showInput, disabled } = queryFunctionsTypesConfig[funcData.name];
|
||||||
|
|
||||||
let functionValue;
|
let functionValue;
|
||||||
|
|
||||||
@ -62,6 +62,7 @@ export default function Function({
|
|||||||
<Select
|
<Select
|
||||||
className={cx('query-function-name-selector', showInput ? 'showInput' : '')}
|
className={cx('query-function-name-selector', showInput ? 'showInput' : '')}
|
||||||
value={funcData.name}
|
value={funcData.name}
|
||||||
|
disabled={disabled}
|
||||||
style={{ minWidth: '100px' }}
|
style={{ minWidth: '100px' }}
|
||||||
onChange={(value): void => {
|
onChange={(value): void => {
|
||||||
handleUpdateFunctionName(funcData, index, value);
|
handleUpdateFunctionName(funcData, index, value);
|
||||||
|
@ -32,7 +32,6 @@ export async function GetMetricQueryRange(
|
|||||||
signal,
|
signal,
|
||||||
headers,
|
headers,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
if (response.statusCode >= 400) {
|
if (response.statusCode >= 400) {
|
||||||
let error = `API responded with ${response.statusCode} - ${response.error} status: ${response.message}`;
|
let error = `API responded with ${response.statusCode} - ${response.error} status: ${response.message}`;
|
||||||
@ -71,6 +70,19 @@ export async function GetMetricQueryRange(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.payload?.data?.newResult?.data?.resultType === 'anomaly') {
|
||||||
|
response.payload.data.newResult.data.result = response.payload.data.newResult.data.result.map(
|
||||||
|
(queryData) => {
|
||||||
|
if (legendMap[queryData.queryName]) {
|
||||||
|
queryData.legend = legendMap[queryData.queryName];
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryData;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable sonarjs/no-identical-functions */
|
||||||
import {
|
import {
|
||||||
MetricRangePayloadProps,
|
MetricRangePayloadProps,
|
||||||
MetricRangePayloadV3,
|
MetricRangePayloadV3,
|
||||||
@ -12,8 +13,8 @@ export const convertNewDataToOld = (
|
|||||||
|
|
||||||
result.forEach((item) => {
|
result.forEach((item) => {
|
||||||
if (item.series) {
|
if (item.series) {
|
||||||
item.series.forEach((serie) => {
|
item.series.forEach((series) => {
|
||||||
const values: QueryData['values'] = serie.values.reduce<
|
const values: QueryData['values'] = series.values.reduce<
|
||||||
QueryData['values']
|
QueryData['values']
|
||||||
>((acc, currentInfo) => {
|
>((acc, currentInfo) => {
|
||||||
const renderValues: [number, string] = [
|
const renderValues: [number, string] = [
|
||||||
@ -23,16 +24,87 @@ export const convertNewDataToOld = (
|
|||||||
|
|
||||||
return [...acc, renderValues];
|
return [...acc, renderValues];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const result: QueryData = {
|
const result: QueryData = {
|
||||||
metric: serie.labels,
|
metric: series.labels,
|
||||||
values,
|
values,
|
||||||
queryName: item.queryName,
|
queryName: `${item.queryName}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
oldResult.push(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.predictedSeries) {
|
||||||
|
item.predictedSeries.forEach((series) => {
|
||||||
|
const values: QueryData['values'] = series.values.reduce<
|
||||||
|
QueryData['values']
|
||||||
|
>((acc, currentInfo) => {
|
||||||
|
const renderValues: [number, string] = [
|
||||||
|
currentInfo.timestamp / 1000,
|
||||||
|
currentInfo.value,
|
||||||
|
];
|
||||||
|
|
||||||
|
return [...acc, renderValues];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const result: QueryData = {
|
||||||
|
metric: series.labels,
|
||||||
|
values,
|
||||||
|
queryName: `${item.queryName}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
oldResult.push(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.upperBoundSeries) {
|
||||||
|
item.upperBoundSeries.forEach((series) => {
|
||||||
|
const values: QueryData['values'] = series.values.reduce<
|
||||||
|
QueryData['values']
|
||||||
|
>((acc, currentInfo) => {
|
||||||
|
const renderValues: [number, string] = [
|
||||||
|
currentInfo.timestamp / 1000,
|
||||||
|
currentInfo.value,
|
||||||
|
];
|
||||||
|
|
||||||
|
return [...acc, renderValues];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const result: QueryData = {
|
||||||
|
metric: series.labels,
|
||||||
|
values,
|
||||||
|
queryName: `${item.queryName}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
oldResult.push(result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.lowerBoundSeries) {
|
||||||
|
item.lowerBoundSeries.forEach((series) => {
|
||||||
|
const values: QueryData['values'] = series.values.reduce<
|
||||||
|
QueryData['values']
|
||||||
|
>((acc, currentInfo) => {
|
||||||
|
const renderValues: [number, string] = [
|
||||||
|
currentInfo.timestamp / 1000,
|
||||||
|
currentInfo.value,
|
||||||
|
];
|
||||||
|
|
||||||
|
return [...acc, renderValues];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const result: QueryData = {
|
||||||
|
metric: series.labels,
|
||||||
|
values,
|
||||||
|
queryName: `${item.queryName}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
oldResult.push(result);
|
oldResult.push(result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const oldResultType = resultType;
|
const oldResultType = resultType;
|
||||||
|
|
||||||
// TODO: fix it later for using only v3 version of api
|
// TODO: fix it later for using only v3 version of api
|
||||||
|
@ -163,6 +163,8 @@ export const getUPlotChartOptions = ({
|
|||||||
|
|
||||||
const stackBarChart = stackChart && isUndefined(hiddenGraph);
|
const stackBarChart = stackChart && isUndefined(hiddenGraph);
|
||||||
|
|
||||||
|
const isAnomalyRule = apiResponse?.data?.newResult?.data?.result[0].isAnomaly;
|
||||||
|
|
||||||
const series = getStackedSeries(apiResponse?.data?.result || []);
|
const series = getStackedSeries(apiResponse?.data?.result || []);
|
||||||
|
|
||||||
const bands = stackBarChart ? getBands(series) : null;
|
const bands = stackBarChart ? getBands(series) : null;
|
||||||
@ -251,11 +253,14 @@ export const getUPlotChartOptions = ({
|
|||||||
hooks: {
|
hooks: {
|
||||||
draw: [
|
draw: [
|
||||||
(u): void => {
|
(u): void => {
|
||||||
|
if (isAnomalyRule) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
thresholds?.forEach((threshold) => {
|
thresholds?.forEach((threshold) => {
|
||||||
if (threshold.thresholdValue !== undefined) {
|
if (threshold.thresholdValue !== undefined) {
|
||||||
const { ctx } = u;
|
const { ctx } = u;
|
||||||
ctx.save();
|
ctx.save();
|
||||||
|
|
||||||
const yPos = u.valToPos(
|
const yPos = u.valToPos(
|
||||||
convertValue(
|
convertValue(
|
||||||
threshold.thresholdValue,
|
threshold.thresholdValue,
|
||||||
@ -265,30 +270,22 @@ export const getUPlotChartOptions = ({
|
|||||||
'y',
|
'y',
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
ctx.strokeStyle = threshold.thresholdColor || 'red';
|
ctx.strokeStyle = threshold.thresholdColor || 'red';
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.setLineDash([10, 5]);
|
ctx.setLineDash([10, 5]);
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
|
||||||
const plotLeft = u.bbox.left; // left edge of the plot area
|
const plotLeft = u.bbox.left; // left edge of the plot area
|
||||||
const plotRight = plotLeft + u.bbox.width; // right edge of the plot area
|
const plotRight = plotLeft + u.bbox.width; // right edge of the plot area
|
||||||
|
|
||||||
ctx.moveTo(plotLeft, yPos);
|
ctx.moveTo(plotLeft, yPos);
|
||||||
ctx.lineTo(plotRight, yPos);
|
ctx.lineTo(plotRight, yPos);
|
||||||
|
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// Text configuration
|
// Text configuration
|
||||||
if (threshold.thresholdLabel) {
|
if (threshold.thresholdLabel) {
|
||||||
const text = threshold.thresholdLabel;
|
const text = threshold.thresholdLabel;
|
||||||
const textX = plotRight - ctx.measureText(text).width - 20;
|
const textX = plotRight - ctx.measureText(text).width - 20;
|
||||||
|
|
||||||
const canvasHeight = ctx.canvas.height;
|
const canvasHeight = ctx.canvas.height;
|
||||||
const yposHeight = canvasHeight - yPos;
|
const yposHeight = canvasHeight - yPos;
|
||||||
const isHeightGreaterThan90Percent = canvasHeight * 0.9 < yposHeight;
|
const isHeightGreaterThan90Percent = canvasHeight * 0.9 < yposHeight;
|
||||||
|
|
||||||
// Adjust textY based on the condition
|
// Adjust textY based on the condition
|
||||||
let textY;
|
let textY;
|
||||||
if (isHeightGreaterThan90Percent) {
|
if (isHeightGreaterThan90Percent) {
|
||||||
@ -299,7 +296,6 @@ export const getUPlotChartOptions = ({
|
|||||||
ctx.fillStyle = threshold.thresholdColor || 'red';
|
ctx.fillStyle = threshold.thresholdColor || 'red';
|
||||||
ctx.fillText(text, textX, textY);
|
ctx.fillText(text, textX, textY);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import getLabelName from 'lib/getLabelName';
|
||||||
|
import { colors } from 'lib/getRandomColor';
|
||||||
import { cloneDeep, isUndefined } from 'lodash-es';
|
import { cloneDeep, isUndefined } from 'lodash-es';
|
||||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
import { QueryData } from 'types/api/widgets/getQuery';
|
import { QueryData } from 'types/api/widgets/getQuery';
|
||||||
@ -20,7 +22,7 @@ function getXAxisTimestamps(seriesList: QueryData[]): number[] {
|
|||||||
function fillMissingXAxisTimestamps(timestampArr: number[], data: any[]): any {
|
function fillMissingXAxisTimestamps(timestampArr: number[], data: any[]): any {
|
||||||
// Generate a set of all timestamps in the range
|
// Generate a set of all timestamps in the range
|
||||||
const allTimestampsSet = new Set(timestampArr);
|
const allTimestampsSet = new Set(timestampArr);
|
||||||
const processedData = JSON.parse(JSON.stringify(data));
|
const processedData = cloneDeep(data);
|
||||||
|
|
||||||
// Fill missing timestamps with null values
|
// Fill missing timestamps with null values
|
||||||
processedData.forEach((entry: { values: (number | null)[][] }) => {
|
processedData.forEach((entry: { values: (number | null)[][] }) => {
|
||||||
@ -90,3 +92,70 @@ export const getUPlotChartData = (
|
|||||||
: yAxisValuesArr),
|
: yAxisValuesArr),
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const processAnomalyDetectionData = (
|
||||||
|
anomalyDetectionData: any,
|
||||||
|
): Record<string, { data: number[][]; color: string }> => {
|
||||||
|
if (!anomalyDetectionData) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedData: Record<
|
||||||
|
string,
|
||||||
|
{ data: number[][]; color: string; legendLabel: string }
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
for (
|
||||||
|
let queryIndex = 0;
|
||||||
|
queryIndex < anomalyDetectionData.length;
|
||||||
|
queryIndex++
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
series,
|
||||||
|
predictedSeries,
|
||||||
|
upperBoundSeries,
|
||||||
|
lowerBoundSeries,
|
||||||
|
queryName,
|
||||||
|
legend,
|
||||||
|
} = anomalyDetectionData[queryIndex];
|
||||||
|
|
||||||
|
for (let index = 0; index < series?.length; index++) {
|
||||||
|
const label = getLabelName(
|
||||||
|
series[index].labels,
|
||||||
|
queryName || '', // query
|
||||||
|
legend || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
const objKey = `${queryName}-${label}`;
|
||||||
|
|
||||||
|
processedData[objKey] = {
|
||||||
|
data: [
|
||||||
|
series[index].values.map((v: { timestamp: number }) => v.timestamp / 1000),
|
||||||
|
series[index].values.map((v: { value: number }) => v.value),
|
||||||
|
predictedSeries[index].values.map((v: { value: number }) => v.value),
|
||||||
|
upperBoundSeries[index].values.map((v: { value: number }) => v.value),
|
||||||
|
lowerBoundSeries[index].values.map((v: { value: number }) => v.value),
|
||||||
|
],
|
||||||
|
color: colors[index],
|
||||||
|
legendLabel: label,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUplotChartDataForAnomalyDetection = (
|
||||||
|
apiResponse?: MetricRangePayloadProps,
|
||||||
|
): Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
[x: string]: any;
|
||||||
|
data: number[][];
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
> => {
|
||||||
|
const anomalyDetectionData = apiResponse?.data?.newResult?.data?.result;
|
||||||
|
|
||||||
|
return processAnomalyDetectionData(anomalyDetectionData);
|
||||||
|
};
|
||||||
|
@ -233,6 +233,43 @@ GetYAxisScale): { auto?: boolean; range?: uPlot.Scale.Range } => {
|
|||||||
return { auto: false, range: [min, max] };
|
return { auto: false, range: [min, max] };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getMinMax(data: any): { minValue: number; maxValue: number } {
|
||||||
|
// Exclude the first array
|
||||||
|
const arrays = data.slice(1);
|
||||||
|
|
||||||
|
// Flatten the array and convert all elements to float
|
||||||
|
const flattened = arrays.flat().map(Number);
|
||||||
|
|
||||||
|
// Get min and max, with fallback of 0 for min
|
||||||
|
const minValue = flattened.length ? Math.min(...flattened) : 0;
|
||||||
|
const maxValue = Math.max(...flattened);
|
||||||
|
|
||||||
|
return { minValue, maxValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getYAxisScaleForAnomalyDetection = ({
|
||||||
|
seriesData,
|
||||||
|
selectedSeries,
|
||||||
|
initialData,
|
||||||
|
}: {
|
||||||
|
seriesData: any;
|
||||||
|
selectedSeries: string | null;
|
||||||
|
initialData: any;
|
||||||
|
yAxisUnit?: string;
|
||||||
|
}): { auto?: boolean; range?: uPlot.Scale.Range } => {
|
||||||
|
if (!selectedSeries && !initialData) {
|
||||||
|
return { auto: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedSeriesData = selectedSeries
|
||||||
|
? seriesData[selectedSeries]?.data
|
||||||
|
: initialData;
|
||||||
|
|
||||||
|
const { minValue, maxValue } = getMinMax(selectedSeriesData);
|
||||||
|
|
||||||
|
return { auto: false, range: [minValue, maxValue] };
|
||||||
|
};
|
||||||
|
|
||||||
export type GetYAxisScale = {
|
export type GetYAxisScale = {
|
||||||
thresholds?: ThresholdProps[];
|
thresholds?: ThresholdProps[];
|
||||||
series?: QueryDataV3[];
|
series?: QueryDataV3[];
|
||||||
|
@ -11,8 +11,8 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
color: var(--text-vanilla-400);
|
color: var(--text-vanilla-400);
|
||||||
background: var(--bg-ink-400);
|
background: var(--bg-ink-400);
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
line-height: 20px;
|
line-height: 18px;
|
||||||
letter-spacing: -0.07px;
|
letter-spacing: -0.07px;
|
||||||
padding: 6px 24px;
|
padding: 6px 24px;
|
||||||
border-color: var(--bg-slate-400);
|
border-color: var(--bg-slate-400);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// this list must exactly match with the backend
|
// this list must exactly match with the backend
|
||||||
export enum AlertTypes {
|
export enum AlertTypes {
|
||||||
|
ANOMALY_BASED_ALERT = 'ANOMALY_BASED_ALERT',
|
||||||
METRICS_BASED_ALERT = 'METRIC_BASED_ALERT',
|
METRICS_BASED_ALERT = 'METRIC_BASED_ALERT',
|
||||||
LOGS_BASED_ALERT = 'LOGS_BASED_ALERT',
|
LOGS_BASED_ALERT = 'LOGS_BASED_ALERT',
|
||||||
TRACES_BASED_ALERT = 'TRACES_BASED_ALERT',
|
TRACES_BASED_ALERT = 'TRACES_BASED_ALERT',
|
||||||
|
@ -12,6 +12,10 @@ export const defaultFrequency = '1m0s';
|
|||||||
// default compare op: above
|
// default compare op: above
|
||||||
export const defaultCompareOp = '1';
|
export const defaultCompareOp = '1';
|
||||||
|
|
||||||
|
export const defaultAlgorithm = 'standard';
|
||||||
|
|
||||||
|
export const defaultSeasonality = 'hourly';
|
||||||
|
|
||||||
export interface AlertDef {
|
export interface AlertDef {
|
||||||
id?: number;
|
id?: number;
|
||||||
alertType?: string;
|
alertType?: string;
|
||||||
@ -40,6 +44,8 @@ export interface RuleCondition {
|
|||||||
absentFor?: number | undefined;
|
absentFor?: number | undefined;
|
||||||
requireMinPoints?: boolean | undefined;
|
requireMinPoints?: boolean | undefined;
|
||||||
requiredNumPoints?: number | undefined;
|
requiredNumPoints?: number | undefined;
|
||||||
|
algorithm?: string;
|
||||||
|
seasonality?: string;
|
||||||
}
|
}
|
||||||
export interface Labels {
|
export interface Labels {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
|
@ -8,6 +8,10 @@ export interface PayloadProps {
|
|||||||
export type ListItem = { timestamp: string; data: Omit<ILog, 'timestamp'> };
|
export type ListItem = { timestamp: string; data: Omit<ILog, 'timestamp'> };
|
||||||
|
|
||||||
export interface QueryData {
|
export interface QueryData {
|
||||||
|
lowerBoundSeries?: [number, string][];
|
||||||
|
upperBoundSeries?: [number, string][];
|
||||||
|
predictedSeries?: [number, string][];
|
||||||
|
anomalyScores?: [number, string][];
|
||||||
metric: {
|
metric: {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
@ -34,6 +38,11 @@ export interface QueryDataV3 {
|
|||||||
quantity?: number;
|
quantity?: number;
|
||||||
unitPrice?: number;
|
unitPrice?: number;
|
||||||
unit?: string;
|
unit?: string;
|
||||||
|
lowerBoundSeries?: SeriesItem[] | null;
|
||||||
|
upperBoundSeries?: SeriesItem[] | null;
|
||||||
|
predictedSeries?: SeriesItem[] | null;
|
||||||
|
anomalyScores?: SeriesItem[] | null;
|
||||||
|
isAnomaly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -153,6 +153,7 @@ export enum LogsAggregatorOperator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum QueryFunctionsTypes {
|
export enum QueryFunctionsTypes {
|
||||||
|
ANOMALY = 'anomaly',
|
||||||
CUTOFF_MIN = 'cutOffMin',
|
CUTOFF_MIN = 'cutOffMin',
|
||||||
CUTOFF_MAX = 'cutOffMax',
|
CUTOFF_MAX = 'cutOffMax',
|
||||||
CLAMP_MIN = 'clampMin',
|
CLAMP_MIN = 'clampMin',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user