mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-21 19:30:29 +01:00
Compare commits
4 Commits
platform-p
...
add-exampl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a72ab8421f | ||
|
|
72bc4d143f | ||
|
|
ed17003329 | ||
|
|
fd8b886617 |
3140
docs/api/openapi.yml
3140
docs/api/openapi.yml
File diff suppressed because it is too large
Load Diff
20
frontend/src/api/alerts/create.ts
Normal file
20
frontend/src/api/alerts/create.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/create';
|
||||
|
||||
const create = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
const response = await axios.post('/rules', {
|
||||
...props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default create;
|
||||
28
frontend/src/api/alerts/createAlertRule.ts
Normal file
28
frontend/src/api/alerts/createAlertRule.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import {
|
||||
AlertRuleV2,
|
||||
PostableAlertRuleV2,
|
||||
} from 'types/api/alerts/alertTypesV2';
|
||||
|
||||
export interface CreateAlertRuleResponse {
|
||||
data: AlertRuleV2;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const createAlertRule = async (
|
||||
props: PostableAlertRuleV2,
|
||||
): Promise<SuccessResponse<CreateAlertRuleResponse> | ErrorResponse> => {
|
||||
const response = await axios.post(`/rules`, {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default createAlertRule;
|
||||
18
frontend/src/api/alerts/delete.ts
Normal file
18
frontend/src/api/alerts/delete.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/delete';
|
||||
|
||||
const deleteAlerts = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
const response = await axios.delete(`/rules/${props.id}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data.rules,
|
||||
};
|
||||
};
|
||||
|
||||
export default deleteAlerts;
|
||||
16
frontend/src/api/alerts/get.ts
Normal file
16
frontend/src/api/alerts/get.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/get';
|
||||
|
||||
const get = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
const response = await axios.get(`/rules/${props.id}`);
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
};
|
||||
export default get;
|
||||
24
frontend/src/api/alerts/getAll.ts
Normal file
24
frontend/src/api/alerts/getAll.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/alerts/getAll';
|
||||
|
||||
const getAll = async (): Promise<
|
||||
SuccessResponse<PayloadProps> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.get('/rules');
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data.rules,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getAll;
|
||||
29
frontend/src/api/alerts/getGroup.ts
Normal file
29
frontend/src/api/alerts/getGroup.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { AxiosAlertManagerInstance } from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import convertObjectIntoParams from 'lib/query/convertObjectIntoParams';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/getGroups';
|
||||
|
||||
const getGroups = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const queryParams = convertObjectIntoParams(props);
|
||||
|
||||
const response = await AxiosAlertManagerInstance.get(
|
||||
`/alerts/groups?${queryParams}`,
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getGroups;
|
||||
20
frontend/src/api/alerts/patch.ts
Normal file
20
frontend/src/api/alerts/patch.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/patch';
|
||||
|
||||
const patch = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
const response = await axios.patch(`/rules/${props.id}`, {
|
||||
...props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default patch;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { patchRuleByID } from 'api/generated/services/rules';
|
||||
import type { RuletypesPostableRuleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
// why: patchRuleByID's generated body type is the full RuletypesPostableRuleDTO
|
||||
// because the backend OpenAPI spec currently advertises PostableRule. The
|
||||
// endpoint itself accepts any subset of fields. Until the backend introduces
|
||||
// PatchableRule, this wrapper localizes the cast so callers stay typed.
|
||||
export const patchRulePartial = (
|
||||
id: string,
|
||||
patch: Partial<RuletypesPostableRuleDTO>,
|
||||
): ReturnType<typeof patchRuleByID> =>
|
||||
patchRuleByID({ id }, patch as RuletypesPostableRuleDTO);
|
||||
20
frontend/src/api/alerts/put.ts
Normal file
20
frontend/src/api/alerts/put.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/save';
|
||||
|
||||
const put = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
const response = await axios.put(`/rules/${props.id}`, {
|
||||
...props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default put;
|
||||
18
frontend/src/api/alerts/save.ts
Normal file
18
frontend/src/api/alerts/save.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { isEmpty } from 'lodash-es';
|
||||
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 && !isEmpty(props.id)) {
|
||||
return put({ ...props });
|
||||
}
|
||||
|
||||
return create({ ...props });
|
||||
};
|
||||
|
||||
export default save;
|
||||
26
frontend/src/api/alerts/testAlert.ts
Normal file
26
frontend/src/api/alerts/testAlert.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/alerts/testAlert';
|
||||
|
||||
const testAlert = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/testRule', {
|
||||
...props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default testAlert;
|
||||
28
frontend/src/api/alerts/testAlertRule.ts
Normal file
28
frontend/src/api/alerts/testAlertRule.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
|
||||
export interface TestAlertRuleResponse {
|
||||
data: {
|
||||
alertCount: number;
|
||||
message: string;
|
||||
};
|
||||
status: string;
|
||||
}
|
||||
|
||||
const testAlertRule = async (
|
||||
props: PostableAlertRuleV2,
|
||||
): Promise<SuccessResponse<TestAlertRuleResponse> | ErrorResponse> => {
|
||||
const response = await axios.post(`/testRule`, {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default testAlertRule;
|
||||
26
frontend/src/api/alerts/updateAlertRule.ts
Normal file
26
frontend/src/api/alerts/updateAlertRule.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
|
||||
export interface UpdateAlertRuleResponse {
|
||||
data: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const updateAlertRule = async (
|
||||
id: string,
|
||||
postableAlertRule: PostableAlertRuleV2,
|
||||
): Promise<SuccessResponse<UpdateAlertRuleResponse> | ErrorResponse> => {
|
||||
const response = await axios.put(`/rules/${id}`, {
|
||||
...postableAlertRule,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
};
|
||||
|
||||
export default updateAlertRule;
|
||||
@@ -4774,7 +4774,7 @@ export interface RuletypesPostableRuleDTO {
|
||||
* @type string
|
||||
*/
|
||||
alert: string;
|
||||
alertType?: RuletypesAlertTypeDTO;
|
||||
alertType: RuletypesAlertTypeDTO;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
@@ -4899,7 +4899,7 @@ export interface RuletypesRuleDTO {
|
||||
* @type string
|
||||
*/
|
||||
alert: string;
|
||||
alertType?: RuletypesAlertTypeDTO;
|
||||
alertType: RuletypesAlertTypeDTO;
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
@@ -4984,8 +4984,8 @@ export interface RuletypesRuleConditionDTO {
|
||||
*/
|
||||
algorithm?: string;
|
||||
compositeQuery: RuletypesAlertCompositeQueryDTO;
|
||||
matchType: RuletypesMatchTypeDTO;
|
||||
op: RuletypesCompareOperatorDTO;
|
||||
matchType?: RuletypesMatchTypeDTO;
|
||||
op?: RuletypesCompareOperatorDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
|
||||
45
frontend/src/api/plannedDowntime/createDowntimeSchedule.ts
Normal file
45
frontend/src/api/plannedDowntime/createDowntimeSchedule.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { Dayjs } from 'dayjs';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
import { Recurrence } from './getAllDowntimeSchedules';
|
||||
|
||||
export interface DowntimeSchedulePayload {
|
||||
name: string;
|
||||
description?: string;
|
||||
alertIds: string[];
|
||||
schedule: {
|
||||
timezone?: string;
|
||||
startTime?: string | Dayjs;
|
||||
endTime?: string | Dayjs;
|
||||
recurrence?: Recurrence;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PayloadProps {
|
||||
status: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
const createDowntimeSchedule = async (
|
||||
props: DowntimeSchedulePayload,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post('/downtime_schedules', {
|
||||
...props,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default createDowntimeSchedule;
|
||||
19
frontend/src/api/plannedDowntime/deleteDowntimeSchedule.ts
Normal file
19
frontend/src/api/plannedDowntime/deleteDowntimeSchedule.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
import axios from 'api';
|
||||
|
||||
export interface DeleteDowntimeScheduleProps {
|
||||
id?: number;
|
||||
}
|
||||
|
||||
export interface DeleteSchedulePayloadProps {
|
||||
status: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
export const useDeleteDowntimeSchedule = (
|
||||
props: DeleteDowntimeScheduleProps,
|
||||
): UseMutationResult<DeleteSchedulePayloadProps, Error, number> =>
|
||||
useMutation({
|
||||
mutationKey: [props.id],
|
||||
mutationFn: () => axios.delete(`/downtime_schedules/${props.id}`),
|
||||
});
|
||||
51
frontend/src/api/plannedDowntime/getAllDowntimeSchedules.ts
Normal file
51
frontend/src/api/plannedDowntime/getAllDowntimeSchedules.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useQuery, UseQueryResult } from 'react-query';
|
||||
import axios from 'api';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import { Option } from 'container/PlannedDowntime/PlannedDowntimeutils';
|
||||
|
||||
export type Recurrence = {
|
||||
startTime?: string | null;
|
||||
endTime?: string | null;
|
||||
duration?: number | string | null;
|
||||
repeatType?: string | Option | null;
|
||||
repeatOn?: string[] | null;
|
||||
};
|
||||
|
||||
type Schedule = {
|
||||
timezone: string | null;
|
||||
startTime: string | null;
|
||||
endTime: string | null;
|
||||
recurrence: Recurrence | null;
|
||||
};
|
||||
|
||||
export interface DowntimeSchedules {
|
||||
id: number;
|
||||
name: string | null;
|
||||
description: string | null;
|
||||
schedule: Schedule | null;
|
||||
alertIds: string[] | null;
|
||||
createdAt: string | null;
|
||||
createdBy: string | null;
|
||||
updatedAt: string | null;
|
||||
updatedBy: string | null;
|
||||
kind: string | null;
|
||||
}
|
||||
export type PayloadProps = { data: DowntimeSchedules[] };
|
||||
|
||||
export const getAllDowntimeSchedules = async (
|
||||
props?: GetAllDowntimeSchedulesPayloadProps,
|
||||
): Promise<AxiosResponse<PayloadProps>> =>
|
||||
axios.get('/downtime_schedules', { params: props });
|
||||
|
||||
export interface GetAllDowntimeSchedulesPayloadProps {
|
||||
active?: boolean;
|
||||
recurrence?: boolean;
|
||||
}
|
||||
|
||||
export const useGetAllDowntimeSchedules = (
|
||||
props?: GetAllDowntimeSchedulesPayloadProps,
|
||||
): UseQueryResult<AxiosResponse<PayloadProps>, AxiosError> =>
|
||||
useQuery<AxiosResponse<PayloadProps>, AxiosError>({
|
||||
queryKey: ['getAllDowntimeSchedules', props],
|
||||
queryFn: () => getAllDowntimeSchedules(props),
|
||||
});
|
||||
37
frontend/src/api/plannedDowntime/updateDowntimeSchedule.ts
Normal file
37
frontend/src/api/plannedDowntime/updateDowntimeSchedule.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
import { DowntimeSchedulePayload } from './createDowntimeSchedule';
|
||||
|
||||
export interface DowntimeScheduleUpdatePayload {
|
||||
data: DowntimeSchedulePayload;
|
||||
id?: number;
|
||||
}
|
||||
|
||||
export interface PayloadProps {
|
||||
status: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
const updateDowntimeSchedule = async (
|
||||
props: DowntimeScheduleUpdatePayload,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`/downtime_schedules/${props.id}`, {
|
||||
...props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default updateDowntimeSchedule;
|
||||
@@ -1,4 +1,3 @@
|
||||
import { RuletypesAlertTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -9,17 +8,3 @@ export const ALERTS_DATA_SOURCE_MAP: Record<AlertTypes, DataSource> = {
|
||||
[AlertTypes.TRACES_BASED_ALERT]: DataSource.TRACES,
|
||||
[AlertTypes.EXCEPTIONS_BASED_ALERT]: DataSource.TRACES,
|
||||
};
|
||||
|
||||
export function dataSourceForAlertType(
|
||||
alertType: RuletypesAlertTypeDTO | undefined,
|
||||
): DataSource {
|
||||
switch (alertType) {
|
||||
case RuletypesAlertTypeDTO.LOGS_BASED_ALERT:
|
||||
return DataSource.LOGS;
|
||||
case RuletypesAlertTypeDTO.TRACES_BASED_ALERT:
|
||||
case RuletypesAlertTypeDTO.EXCEPTIONS_BASED_ALERT:
|
||||
return DataSource.TRACES;
|
||||
default:
|
||||
return DataSource.METRICS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,12 +15,8 @@ import {
|
||||
THRESHOLD_MATCH_TYPE_OPTIONS,
|
||||
THRESHOLD_OPERATOR_OPTIONS,
|
||||
} from '../context/constants';
|
||||
import {
|
||||
AlertThresholdMatchType,
|
||||
AlertThresholdOperator,
|
||||
} from '../context/types';
|
||||
import { AlertThresholdMatchType } from '../context/types';
|
||||
import EvaluationSettings from '../EvaluationSettings/EvaluationSettings';
|
||||
import { normalizeMatchType, normalizeOperator } from '../utils';
|
||||
import ThresholdItem from './ThresholdItem';
|
||||
import { AnomalyAndThresholdProps, UpdateThreshold } from './types';
|
||||
import {
|
||||
@@ -136,15 +132,12 @@ function AlertThreshold({
|
||||
}
|
||||
};
|
||||
|
||||
const normalizedOperator =
|
||||
normalizeOperator(thresholdState.operator) ?? AlertThresholdOperator.IS_ABOVE;
|
||||
|
||||
const matchTypeOptionsWithTooltips = THRESHOLD_MATCH_TYPE_OPTIONS.map(
|
||||
(option) => ({
|
||||
...option,
|
||||
label: (
|
||||
<Tooltip
|
||||
title={getMatchTypeTooltip(option.value, normalizedOperator)}
|
||||
title={getMatchTypeTooltip(option.value, thresholdState.operator)}
|
||||
placement="left"
|
||||
overlayClassName="copyable-tooltip"
|
||||
overlayStyle={{
|
||||
@@ -239,10 +232,7 @@ function AlertThreshold({
|
||||
/>
|
||||
<Typography.Text className="sentence-text">is</Typography.Text>
|
||||
<Select
|
||||
value={
|
||||
(normalizeOperator(thresholdState.operator) ??
|
||||
thresholdState.operator) as AlertThresholdOperator
|
||||
}
|
||||
value={thresholdState.operator}
|
||||
onChange={(value): void => {
|
||||
setThresholdState({
|
||||
type: 'SET_OPERATOR',
|
||||
@@ -257,10 +247,7 @@ function AlertThreshold({
|
||||
the threshold(s)
|
||||
</Typography.Text>
|
||||
<Select
|
||||
value={
|
||||
(normalizeMatchType(thresholdState.matchType) ??
|
||||
thresholdState.matchType) as AlertThresholdMatchType
|
||||
}
|
||||
value={thresholdState.matchType}
|
||||
onChange={(value): void => {
|
||||
setThresholdState({
|
||||
type: 'SET_MATCH_TYPE',
|
||||
|
||||
@@ -11,11 +11,6 @@ import {
|
||||
ANOMALY_THRESHOLD_OPERATOR_OPTIONS,
|
||||
ANOMALY_TIME_DURATION_OPTIONS,
|
||||
} from '../context/constants';
|
||||
import {
|
||||
AlertThresholdMatchType,
|
||||
AlertThresholdOperator,
|
||||
} from '../context/types';
|
||||
import { normalizeMatchType, normalizeOperator } from '../utils';
|
||||
import { AnomalyAndThresholdProps } from './types';
|
||||
import {
|
||||
getQueryNames,
|
||||
@@ -120,10 +115,7 @@ function AnomalyThreshold({
|
||||
deviations
|
||||
</Typography.Text>
|
||||
<Select
|
||||
value={
|
||||
(normalizeOperator(thresholdState.operator) ??
|
||||
thresholdState.operator) as AlertThresholdOperator
|
||||
}
|
||||
value={thresholdState.operator}
|
||||
data-testid="operator-select"
|
||||
onChange={(value): void => {
|
||||
setThresholdState({
|
||||
@@ -140,10 +132,7 @@ function AnomalyThreshold({
|
||||
the predicted data
|
||||
</Typography.Text>
|
||||
<Select
|
||||
value={
|
||||
(normalizeMatchType(thresholdState.matchType) ??
|
||||
thresholdState.matchType) as AlertThresholdMatchType
|
||||
}
|
||||
value={thresholdState.matchType}
|
||||
data-testid="match-type-select"
|
||||
onChange={(value): void => {
|
||||
setThresholdState({
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useAppContext } from 'providers/App/App';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import { AlertThresholdOperator } from '../context/types';
|
||||
import { normalizeOperator } from '../utils';
|
||||
import { ThresholdItemProps } from './types';
|
||||
import { NotificationChannelsNotFoundContent } from './utils';
|
||||
|
||||
@@ -55,7 +54,7 @@ function ThresholdItem({
|
||||
}, [units, threshold.unit, updateThreshold, threshold.id]);
|
||||
|
||||
const getOperatorSymbol = (): string => {
|
||||
switch (normalizeOperator(thresholdState.operator)) {
|
||||
switch (thresholdState.operator) {
|
||||
case AlertThresholdOperator.IS_ABOVE:
|
||||
return '>';
|
||||
case AlertThresholdOperator.IS_BELOW:
|
||||
|
||||
@@ -6,7 +6,9 @@ import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/ut
|
||||
import * as useSafeNavigateHook from 'hooks/useSafeNavigate';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import * as rulesHook from '../../../../api/generated/services/rules';
|
||||
import * as useCreateAlertRuleHook from '../../../../hooks/alerts/useCreateAlertRule';
|
||||
import * as useTestAlertRuleHook from '../../../../hooks/alerts/useTestAlertRule';
|
||||
import * as useUpdateAlertRuleHook from '../../../../hooks/alerts/useUpdateAlertRule';
|
||||
import { CreateAlertProvider } from '../../context';
|
||||
import CreateAlertHeader from '../CreateAlertHeader';
|
||||
|
||||
@@ -15,15 +17,15 @@ jest.spyOn(useSafeNavigateHook, 'useSafeNavigate').mockReturnValue({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
});
|
||||
|
||||
jest.spyOn(rulesHook, 'useCreateRule').mockReturnValue({
|
||||
jest.spyOn(useCreateAlertRuleHook, 'useCreateAlertRule').mockReturnValue({
|
||||
mutate: jest.fn(),
|
||||
isLoading: false,
|
||||
} as any);
|
||||
jest.spyOn(rulesHook, 'useTestRule').mockReturnValue({
|
||||
jest.spyOn(useTestAlertRuleHook, 'useTestAlertRule').mockReturnValue({
|
||||
mutate: jest.fn(),
|
||||
isLoading: false,
|
||||
} as any);
|
||||
jest.spyOn(rulesHook, 'useUpdateRuleByID').mockReturnValue({
|
||||
jest.spyOn(useUpdateAlertRuleHook, 'useUpdateAlertRule').mockReturnValue({
|
||||
mutate: jest.fn(),
|
||||
isLoading: false,
|
||||
} as any);
|
||||
|
||||
@@ -34,7 +34,6 @@ export const createMockAlertContextState = (
|
||||
isUpdatingAlertRule: false,
|
||||
updateAlertRule: jest.fn(),
|
||||
isEditMode: false,
|
||||
ruleId: '',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { toast } from '@signozhq/ui';
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { Check, Loader, Send, X } from 'lucide-react';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { toPostableRuleDTO } from 'types/api/alerts/convert';
|
||||
import APIError from 'types/api/error';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
@@ -36,20 +30,9 @@ function Footer(): JSX.Element {
|
||||
updateAlertRule,
|
||||
isUpdatingAlertRule,
|
||||
isEditMode,
|
||||
ruleId,
|
||||
} = useCreateAlertState();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const handleApiError = useCallback(
|
||||
(error: unknown): void => {
|
||||
showErrorModal(
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
|
||||
);
|
||||
},
|
||||
[showErrorModal],
|
||||
);
|
||||
|
||||
const handleDiscard = (e: React.MouseEvent): void => {
|
||||
discardAlertRule();
|
||||
@@ -88,21 +71,20 @@ function Footer(): JSX.Element {
|
||||
notificationSettings,
|
||||
query: currentQuery,
|
||||
});
|
||||
testAlertRule(
|
||||
{ data: toPostableRuleDTO(payload) },
|
||||
{
|
||||
onSuccess: (response) => {
|
||||
if (response.data?.alertCount === 0) {
|
||||
toast.error(
|
||||
'No alerts found during the evaluation. This happens when rule condition is unsatisfied. You may adjust the rule threshold and retry.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
toast.success('Test notification sent successfully');
|
||||
},
|
||||
onError: handleApiError,
|
||||
testAlertRule(payload, {
|
||||
onSuccess: (response) => {
|
||||
if (response.payload?.data?.alertCount === 0) {
|
||||
toast.error(
|
||||
'No alerts found during the evaluation. This happens when rule condition is unsatisfied. You may adjust the rule threshold and retry.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
toast.success('Test notification sent successfully');
|
||||
},
|
||||
);
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
}, [
|
||||
alertType,
|
||||
basicAlertState,
|
||||
@@ -125,30 +107,25 @@ function Footer(): JSX.Element {
|
||||
query: currentQuery,
|
||||
});
|
||||
if (isEditMode) {
|
||||
updateAlertRule(
|
||||
{
|
||||
pathParams: { id: ruleId },
|
||||
data: toPostableRuleDTO(payload),
|
||||
updateAlertRule(payload, {
|
||||
onSuccess: () => {
|
||||
toast.success('Alert rule updated successfully');
|
||||
safeNavigate('/alerts');
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success('Alert rule updated successfully');
|
||||
safeNavigate('/alerts');
|
||||
},
|
||||
onError: handleApiError,
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
);
|
||||
});
|
||||
} else {
|
||||
createAlertRule(
|
||||
{ data: toPostableRuleDTO(payload) },
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success('Alert rule created successfully');
|
||||
safeNavigate('/alerts');
|
||||
},
|
||||
onError: handleApiError,
|
||||
createAlertRule(payload, {
|
||||
onSuccess: () => {
|
||||
toast.success('Alert rule created successfully');
|
||||
safeNavigate('/alerts');
|
||||
},
|
||||
);
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [
|
||||
alertType,
|
||||
@@ -159,11 +136,9 @@ function Footer(): JSX.Element {
|
||||
notificationSettings,
|
||||
currentQuery,
|
||||
isEditMode,
|
||||
ruleId,
|
||||
updateAlertRule,
|
||||
createAlertRule,
|
||||
safeNavigate,
|
||||
handleApiError,
|
||||
]);
|
||||
|
||||
const disableButtons =
|
||||
|
||||
@@ -12,11 +12,6 @@ import Footer from '../Footer';
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: jest.fn(),
|
||||
}));
|
||||
jest.mock('providers/ErrorModalProvider', () => ({
|
||||
useErrorModal: (): { showErrorModal: jest.Mock } => ({
|
||||
showErrorModal: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: jest.fn(),
|
||||
|
||||
@@ -474,9 +474,9 @@ describe('Footer utils', () => {
|
||||
spec: [
|
||||
{
|
||||
channels: [],
|
||||
matchType: 'at_least_once',
|
||||
matchType: '1',
|
||||
name: 'critical',
|
||||
op: 'above',
|
||||
op: '1',
|
||||
target: 0,
|
||||
targetUnit: '',
|
||||
},
|
||||
@@ -520,33 +520,5 @@ describe('Footer utils', () => {
|
||||
expect(props.condition.compositeQuery.queryType).toBe('promql');
|
||||
expect(props.ruleType).toBe('promql_rule');
|
||||
});
|
||||
|
||||
// Backward compatibility: a rule loaded with a legacy op/matchType
|
||||
// serialization ("1", ">", "eq", "above_or_equal", ...) must round-trip
|
||||
// back to the backend unchanged when the user hasn't touched those
|
||||
// fields. If the submit payload silently rewrites them, existing rules
|
||||
// would drift away from their persisted form on every edit.
|
||||
it.each([
|
||||
['numeric', '1', '1'],
|
||||
['symbol', '>', 'at_least_once'],
|
||||
['literal', 'above', 'at_least_once'],
|
||||
['short', 'eq', 'avg'],
|
||||
['UI-unexposed', 'above_or_equal', 'at_least_once'],
|
||||
])(
|
||||
'round-trips %s op/matchType unchanged through the submit payload (%s / %s)',
|
||||
(_desc, op, matchType) => {
|
||||
const args: BuildCreateAlertRulePayloadArgs = {
|
||||
...INITIAL_BUILD_CREATE_ALERT_RULE_PAYLOAD_ARGS,
|
||||
thresholdState: {
|
||||
...INITIAL_BUILD_CREATE_ALERT_RULE_PAYLOAD_ARGS.thresholdState,
|
||||
operator: op,
|
||||
matchType,
|
||||
},
|
||||
};
|
||||
const props = buildCreateThresholdAlertRulePayload(args);
|
||||
expect(props.condition.thresholds?.spec[0].op).toBe(op);
|
||||
expect(props.condition.thresholds?.spec[0].matchType).toBe(matchType);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,8 +15,6 @@ import {
|
||||
getEvaluationWindowStateFromAlertDef,
|
||||
getNotificationSettingsStateFromAlertDef,
|
||||
getThresholdStateFromAlertDef,
|
||||
normalizeMatchType,
|
||||
normalizeOperator,
|
||||
parseGoTime,
|
||||
} from '../utils';
|
||||
|
||||
@@ -316,137 +314,6 @@ describe('CreateAlertV2 utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeOperator', () => {
|
||||
it.each([
|
||||
['1', AlertThresholdOperator.IS_ABOVE],
|
||||
['above', AlertThresholdOperator.IS_ABOVE],
|
||||
['>', AlertThresholdOperator.IS_ABOVE],
|
||||
['2', AlertThresholdOperator.IS_BELOW],
|
||||
['below', AlertThresholdOperator.IS_BELOW],
|
||||
['<', AlertThresholdOperator.IS_BELOW],
|
||||
['3', AlertThresholdOperator.IS_EQUAL_TO],
|
||||
['equal', AlertThresholdOperator.IS_EQUAL_TO],
|
||||
['eq', AlertThresholdOperator.IS_EQUAL_TO],
|
||||
['=', AlertThresholdOperator.IS_EQUAL_TO],
|
||||
['4', AlertThresholdOperator.IS_NOT_EQUAL_TO],
|
||||
['not_equal', AlertThresholdOperator.IS_NOT_EQUAL_TO],
|
||||
['not_eq', AlertThresholdOperator.IS_NOT_EQUAL_TO],
|
||||
['!=', AlertThresholdOperator.IS_NOT_EQUAL_TO],
|
||||
['7', AlertThresholdOperator.ABOVE_BELOW],
|
||||
['outside_bounds', AlertThresholdOperator.ABOVE_BELOW],
|
||||
])('maps backend alias %s to canonical enum', (alias, expected) => {
|
||||
expect(normalizeOperator(alias)).toBe(expected);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['5', 'above_or_equal'],
|
||||
['above_or_equal', 'above_or_equal'],
|
||||
['above_or_eq', 'above_or_equal'],
|
||||
['>=', 'above_or_equal'],
|
||||
['6', 'below_or_equal'],
|
||||
['below_or_equal', 'below_or_equal'],
|
||||
['below_or_eq', 'below_or_equal'],
|
||||
['<=', 'below_or_equal'],
|
||||
])('returns undefined for UI-unexposed alias %s (%s family)', (alias) => {
|
||||
expect(normalizeOperator(alias)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for unknown values', () => {
|
||||
expect(normalizeOperator('gibberish')).toBeUndefined();
|
||||
expect(normalizeOperator(undefined)).toBeUndefined();
|
||||
expect(normalizeOperator('')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeMatchType', () => {
|
||||
it.each([
|
||||
['1', AlertThresholdMatchType.AT_LEAST_ONCE],
|
||||
['at_least_once', AlertThresholdMatchType.AT_LEAST_ONCE],
|
||||
['2', AlertThresholdMatchType.ALL_THE_TIME],
|
||||
['all_the_times', AlertThresholdMatchType.ALL_THE_TIME],
|
||||
['3', AlertThresholdMatchType.ON_AVERAGE],
|
||||
['on_average', AlertThresholdMatchType.ON_AVERAGE],
|
||||
['avg', AlertThresholdMatchType.ON_AVERAGE],
|
||||
['4', AlertThresholdMatchType.IN_TOTAL],
|
||||
['in_total', AlertThresholdMatchType.IN_TOTAL],
|
||||
['sum', AlertThresholdMatchType.IN_TOTAL],
|
||||
['5', AlertThresholdMatchType.LAST],
|
||||
['last', AlertThresholdMatchType.LAST],
|
||||
])('maps backend alias %s to canonical enum', (alias, expected) => {
|
||||
expect(normalizeMatchType(alias)).toBe(expected);
|
||||
});
|
||||
|
||||
it('returns undefined for unknown values', () => {
|
||||
expect(normalizeMatchType('gibberish')).toBeUndefined();
|
||||
expect(normalizeMatchType(undefined)).toBeUndefined();
|
||||
expect(normalizeMatchType('')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getThresholdStateFromAlertDef backward compatibility', () => {
|
||||
const buildDef = (op: string, matchType: string): PostableAlertRuleV2 => ({
|
||||
...defaultPostableAlertRuleV2,
|
||||
condition: {
|
||||
...defaultPostableAlertRuleV2.condition,
|
||||
thresholds: {
|
||||
kind: 'basic',
|
||||
spec: [
|
||||
{
|
||||
name: 'critical',
|
||||
target: 1,
|
||||
targetUnit: UniversalYAxisUnit.MINUTES,
|
||||
channels: [],
|
||||
matchType,
|
||||
op,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Each row covers a distinct historical serialization shape the backend
|
||||
// may have persisted. The frontend must not rewrite these on load —
|
||||
// otherwise opening and saving an old rule silently changes its op.
|
||||
it.each([
|
||||
['numeric', '1', '1'],
|
||||
['literal', 'above', 'at_least_once'],
|
||||
['symbol', '>', 'at_least_once'],
|
||||
['short form', 'eq', 'avg'],
|
||||
['mixed numeric and literal', '7', 'last'],
|
||||
['UI-unexposed operator', 'above_or_equal', 'at_least_once'],
|
||||
['UI-unexposed numeric operator', '5', 'at_least_once'],
|
||||
])('preserves %s op/matchType verbatim (%s / %s)', (_desc, op, matchType) => {
|
||||
const state = getThresholdStateFromAlertDef(buildDef(op, matchType));
|
||||
expect(state.operator).toBe(op);
|
||||
expect(state.matchType).toBe(matchType);
|
||||
});
|
||||
|
||||
it('falls back to IS_ABOVE / AT_LEAST_ONCE when op and matchType are missing', () => {
|
||||
const def: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
condition: {
|
||||
...defaultPostableAlertRuleV2.condition,
|
||||
thresholds: {
|
||||
kind: 'basic',
|
||||
spec: [
|
||||
{
|
||||
name: 'critical',
|
||||
target: 1,
|
||||
targetUnit: UniversalYAxisUnit.MINUTES,
|
||||
channels: [],
|
||||
matchType: '',
|
||||
op: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const state = getThresholdStateFromAlertDef(def);
|
||||
expect(state.operator).toBe(AlertThresholdOperator.IS_ABOVE);
|
||||
expect(state.matchType).toBe(AlertThresholdMatchType.AT_LEAST_ONCE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCreateAlertLocalStateFromAlertDef', () => {
|
||||
it('should return the correct create alert local state for the given alert def', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
|
||||
@@ -10,13 +10,11 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
useCreateRule,
|
||||
useTestRule,
|
||||
useUpdateRuleByID,
|
||||
} from 'api/generated/services/rules';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||
import { useCreateAlertRule } from 'hooks/alerts/useCreateAlertRule';
|
||||
import { useTestAlertRule } from 'hooks/alerts/useTestAlertRule';
|
||||
import { useUpdateAlertRule } from 'hooks/alerts/useUpdateAlertRule';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
@@ -217,14 +215,17 @@ export function CreateAlertProvider(
|
||||
const {
|
||||
mutate: createAlertRule,
|
||||
isLoading: isCreatingAlertRule,
|
||||
} = useCreateRule();
|
||||
} = useCreateAlertRule();
|
||||
|
||||
const { mutate: testAlertRule, isLoading: isTestingAlertRule } = useTestRule();
|
||||
const {
|
||||
mutate: testAlertRule,
|
||||
isLoading: isTestingAlertRule,
|
||||
} = useTestAlertRule();
|
||||
|
||||
const {
|
||||
mutate: updateAlertRule,
|
||||
isLoading: isUpdatingAlertRule,
|
||||
} = useUpdateRuleByID();
|
||||
} = useUpdateAlertRule(ruleId || '');
|
||||
|
||||
const contextValue: ICreateAlertContextProps = useMemo(
|
||||
() => ({
|
||||
@@ -248,7 +249,6 @@ export function CreateAlertProvider(
|
||||
updateAlertRule,
|
||||
isUpdatingAlertRule,
|
||||
isEditMode: isEditMode || false,
|
||||
ruleId: ruleId || '',
|
||||
}),
|
||||
[
|
||||
createAlertState,
|
||||
@@ -267,7 +267,6 @@ export function CreateAlertProvider(
|
||||
updateAlertRule,
|
||||
isUpdatingAlertRule,
|
||||
isEditMode,
|
||||
ruleId,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { Dispatch } from 'react';
|
||||
import { UseMutateFunction } from 'react-query';
|
||||
import type {
|
||||
CreateRule201,
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesPostableRuleDTO,
|
||||
TestRule200,
|
||||
UpdateRuleByIDPathParameters,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { BodyType, ErrorType } from 'api/generatedAPIInstance';
|
||||
import { CreateAlertRuleResponse } from 'api/alerts/createAlertRule';
|
||||
import { TestAlertRuleResponse } from 'api/alerts/testAlertRule';
|
||||
import { UpdateAlertRuleResponse } from 'api/alerts/updateAlertRule';
|
||||
import { Dayjs } from 'dayjs';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
import { Labels } from 'types/api/alerts/def';
|
||||
|
||||
export interface ICreateAlertContextProps {
|
||||
@@ -27,33 +24,27 @@ export interface ICreateAlertContextProps {
|
||||
setNotificationSettings: Dispatch<NotificationSettingsAction>;
|
||||
isCreatingAlertRule: boolean;
|
||||
createAlertRule: UseMutateFunction<
|
||||
CreateRule201,
|
||||
ErrorType<unknown>,
|
||||
{ data: BodyType<RuletypesPostableRuleDTO> },
|
||||
SuccessResponse<CreateAlertRuleResponse, unknown> | ErrorResponse,
|
||||
Error,
|
||||
PostableAlertRuleV2,
|
||||
unknown
|
||||
>;
|
||||
isTestingAlertRule: boolean;
|
||||
testAlertRule: UseMutateFunction<
|
||||
TestRule200,
|
||||
ErrorType<unknown>,
|
||||
{ data: BodyType<RuletypesPostableRuleDTO> },
|
||||
SuccessResponse<TestAlertRuleResponse, unknown> | ErrorResponse,
|
||||
Error,
|
||||
PostableAlertRuleV2,
|
||||
unknown
|
||||
>;
|
||||
discardAlertRule: () => void;
|
||||
isUpdatingAlertRule: boolean;
|
||||
updateAlertRule: UseMutateFunction<
|
||||
Awaited<
|
||||
ReturnType<typeof import('api/generated/services/rules').updateRuleByID>
|
||||
>,
|
||||
ErrorType<RenderErrorResponseDTO>,
|
||||
{
|
||||
pathParams: UpdateRuleByIDPathParameters;
|
||||
data: BodyType<RuletypesPostableRuleDTO>;
|
||||
},
|
||||
SuccessResponse<UpdateAlertRuleResponse, unknown> | ErrorResponse,
|
||||
Error,
|
||||
PostableAlertRuleV2,
|
||||
unknown
|
||||
>;
|
||||
isEditMode: boolean;
|
||||
ruleId: string;
|
||||
}
|
||||
|
||||
export interface ICreateAlertProviderProps {
|
||||
@@ -95,28 +86,25 @@ export interface Threshold {
|
||||
}
|
||||
|
||||
export enum AlertThresholdOperator {
|
||||
IS_ABOVE = 'above',
|
||||
IS_BELOW = 'below',
|
||||
IS_EQUAL_TO = 'equal',
|
||||
IS_NOT_EQUAL_TO = 'not_equal',
|
||||
ABOVE_BELOW = 'outside_bounds',
|
||||
IS_ABOVE = '1',
|
||||
IS_BELOW = '2',
|
||||
IS_EQUAL_TO = '3',
|
||||
IS_NOT_EQUAL_TO = '4',
|
||||
ABOVE_BELOW = '7',
|
||||
}
|
||||
|
||||
export enum AlertThresholdMatchType {
|
||||
AT_LEAST_ONCE = 'at_least_once',
|
||||
ALL_THE_TIME = 'all_the_times',
|
||||
ON_AVERAGE = 'on_average',
|
||||
IN_TOTAL = 'in_total',
|
||||
LAST = 'last',
|
||||
AT_LEAST_ONCE = '1',
|
||||
ALL_THE_TIME = '2',
|
||||
ON_AVERAGE = '3',
|
||||
IN_TOTAL = '4',
|
||||
LAST = '5',
|
||||
}
|
||||
|
||||
export interface AlertThresholdState {
|
||||
selectedQuery: string;
|
||||
// Stored as a raw string so backend aliases ("1", ">", "above_or_eq", ...)
|
||||
// survive a load/save round-trip. User edits from the UI write the
|
||||
// canonical enum value.
|
||||
operator: AlertThresholdOperator | string;
|
||||
matchType: AlertThresholdMatchType | string;
|
||||
operator: AlertThresholdOperator;
|
||||
matchType: AlertThresholdMatchType;
|
||||
evaluationWindow: string;
|
||||
algorithm: string;
|
||||
seasonality: string;
|
||||
|
||||
@@ -237,68 +237,6 @@ export function getAdvancedOptionsStateFromAlertDef(
|
||||
};
|
||||
}
|
||||
|
||||
// Mirrors the backend's CompareOperator.Normalize() in
|
||||
// pkg/types/ruletypes/compare.go. Maps any accepted alias to the enum value
|
||||
// the dropdown understands. Returns undefined for aliases the UI does not
|
||||
// expose (e.g. above_or_equal, below_or_equal) so callers can keep the raw
|
||||
// value on screen instead of silently rewriting it.
|
||||
export function normalizeOperator(
|
||||
raw: string | undefined,
|
||||
): AlertThresholdOperator | undefined {
|
||||
switch (raw) {
|
||||
case '1':
|
||||
case 'above':
|
||||
case '>':
|
||||
return AlertThresholdOperator.IS_ABOVE;
|
||||
case '2':
|
||||
case 'below':
|
||||
case '<':
|
||||
return AlertThresholdOperator.IS_BELOW;
|
||||
case '3':
|
||||
case 'equal':
|
||||
case 'eq':
|
||||
case '=':
|
||||
return AlertThresholdOperator.IS_EQUAL_TO;
|
||||
case '4':
|
||||
case 'not_equal':
|
||||
case 'not_eq':
|
||||
case '!=':
|
||||
return AlertThresholdOperator.IS_NOT_EQUAL_TO;
|
||||
case '7':
|
||||
case 'outside_bounds':
|
||||
return AlertThresholdOperator.ABOVE_BELOW;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors the backend's MatchType.Normalize() in pkg/types/ruletypes/match.go.
|
||||
export function normalizeMatchType(
|
||||
raw: string | undefined,
|
||||
): AlertThresholdMatchType | undefined {
|
||||
switch (raw) {
|
||||
case '1':
|
||||
case 'at_least_once':
|
||||
return AlertThresholdMatchType.AT_LEAST_ONCE;
|
||||
case '2':
|
||||
case 'all_the_times':
|
||||
return AlertThresholdMatchType.ALL_THE_TIME;
|
||||
case '3':
|
||||
case 'on_average':
|
||||
case 'avg':
|
||||
return AlertThresholdMatchType.ON_AVERAGE;
|
||||
case '4':
|
||||
case 'in_total':
|
||||
case 'sum':
|
||||
return AlertThresholdMatchType.IN_TOTAL;
|
||||
case '5':
|
||||
case 'last':
|
||||
return AlertThresholdMatchType.LAST;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getThresholdStateFromAlertDef(
|
||||
alertDef: PostableAlertRuleV2,
|
||||
): AlertThresholdState {
|
||||
@@ -316,9 +254,11 @@ export function getThresholdStateFromAlertDef(
|
||||
})) || [],
|
||||
selectedQuery: alertDef.condition.selectedQueryName || '',
|
||||
operator:
|
||||
alertDef.condition.thresholds?.spec[0].op || AlertThresholdOperator.IS_ABOVE,
|
||||
(alertDef.condition.thresholds?.spec[0].op as AlertThresholdOperator) ||
|
||||
AlertThresholdOperator.IS_ABOVE,
|
||||
matchType:
|
||||
alertDef.condition.thresholds?.spec[0].matchType ||
|
||||
(alertDef.condition.thresholds?.spec[0]
|
||||
.matchType as AlertThresholdMatchType) ||
|
||||
AlertThresholdMatchType.AT_LEAST_ONCE,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import ChartLayout from 'container/DashboardContainer/visualization/layout/ChartLayout/ChartLayout';
|
||||
import Legend from 'lib/uPlotV2/components/Legend/Legend';
|
||||
import {
|
||||
@@ -30,6 +30,7 @@ export default function ChartWrapper({
|
||||
onDestroy = noop,
|
||||
children,
|
||||
layoutChildren,
|
||||
yAxisUnit,
|
||||
customTooltip,
|
||||
pinnedTooltipElement,
|
||||
'data-testid': testId,
|
||||
@@ -62,6 +63,13 @@ export default function ChartWrapper({
|
||||
[customTooltip],
|
||||
);
|
||||
|
||||
const syncMetadata = useMemo(
|
||||
() => ({
|
||||
yAxisUnit,
|
||||
}),
|
||||
[yAxisUnit],
|
||||
);
|
||||
|
||||
return (
|
||||
<PlotContextProvider>
|
||||
<ChartLayout
|
||||
@@ -99,6 +107,7 @@ export default function ChartWrapper({
|
||||
averageLegendWidth + TOOLTIP_WIDTH_PADDING,
|
||||
)}
|
||||
syncKey={syncKey}
|
||||
syncMetadata={syncMetadata}
|
||||
render={renderTooltipCallback}
|
||||
pinnedTooltipElement={pinnedTooltipElement}
|
||||
/>
|
||||
|
||||
@@ -24,13 +24,12 @@ export default function Histogram(props: HistogramChartProps): JSX.Element {
|
||||
}
|
||||
const tooltipProps: HistogramTooltipProps = {
|
||||
...props,
|
||||
timezone: rest.timezone,
|
||||
yAxisUnit: rest.yAxisUnit,
|
||||
decimalPrecision: rest.decimalPrecision,
|
||||
};
|
||||
return <HistogramTooltip {...tooltipProps} />;
|
||||
},
|
||||
[customTooltip, rest.timezone, rest.yAxisUnit, rest.decimalPrecision],
|
||||
[customTooltip, rest.yAxisUnit, rest.decimalPrecision],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,10 +12,7 @@ interface BaseChartProps {
|
||||
height: number;
|
||||
showTooltip?: boolean;
|
||||
showLegend?: boolean;
|
||||
timezone?: Timezone;
|
||||
canPinTooltip?: boolean;
|
||||
yAxisUnit?: string;
|
||||
decimalPrecision?: PrecisionOption;
|
||||
pinnedTooltipElement?: (clickData: TooltipClickData) => React.ReactNode;
|
||||
customTooltip?: (props: TooltipRenderArgs) => React.ReactNode;
|
||||
'data-testid'?: string;
|
||||
@@ -32,18 +29,31 @@ interface UPlotBasedChartProps {
|
||||
layoutChildren?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface UPlotChartDataProps {
|
||||
yAxisUnit?: string;
|
||||
decimalPrecision?: PrecisionOption;
|
||||
}
|
||||
|
||||
export interface TimeSeriesChartProps
|
||||
extends BaseChartProps,
|
||||
UPlotBasedChartProps {}
|
||||
UPlotBasedChartProps,
|
||||
UPlotChartDataProps {
|
||||
timezone?: Timezone;
|
||||
}
|
||||
|
||||
export interface HistogramChartProps
|
||||
extends BaseChartProps,
|
||||
UPlotBasedChartProps {
|
||||
UPlotBasedChartProps,
|
||||
UPlotChartDataProps {
|
||||
isQueriesMerged?: boolean;
|
||||
}
|
||||
|
||||
export interface BarChartProps extends BaseChartProps, UPlotBasedChartProps {
|
||||
export interface BarChartProps
|
||||
extends BaseChartProps,
|
||||
UPlotBasedChartProps,
|
||||
UPlotChartDataProps {
|
||||
isStackedBarChart?: boolean;
|
||||
timezone?: Timezone;
|
||||
}
|
||||
|
||||
export type ChartProps =
|
||||
|
||||
@@ -123,13 +123,13 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
}}
|
||||
plotRef={onPlotRef}
|
||||
onDestroy={onPlotDestroy}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
decimalPrecision={widget.decimalPrecision}
|
||||
data={chartData as uPlot.AlignedData}
|
||||
width={containerDimensions.width}
|
||||
height={containerDimensions.height}
|
||||
layoutChildren={layoutChildren}
|
||||
isStackedBarChart={widget.stackedBarChart ?? false}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
decimalPrecision={widget.decimalPrecision}
|
||||
timezone={timezone}
|
||||
>
|
||||
<ContextMenu
|
||||
|
||||
@@ -3,8 +3,6 @@ import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { DashboardCursorSync } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import Histogram from '../../charts/Histogram/Histogram';
|
||||
@@ -29,7 +27,6 @@ function HistogramPanel(props: PanelWrapperProps): JSX.Element {
|
||||
const containerDimensions = useResizeObserver(graphRef);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
const config = useMemo(() => {
|
||||
return prepareHistogramPanelConfig({
|
||||
@@ -92,11 +89,9 @@ function HistogramPanel(props: PanelWrapperProps): JSX.Element {
|
||||
onDestroy={(): void => {
|
||||
uPlotRef.current = null;
|
||||
}}
|
||||
isQueriesMerged={widget.mergeAllActiveQueries}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
decimalPrecision={widget.decimalPrecision}
|
||||
syncMode={DashboardCursorSync.Crosshair}
|
||||
timezone={timezone}
|
||||
isQueriesMerged={widget.mergeAllActiveQueries}
|
||||
data={chartData as uPlot.AlignedData}
|
||||
width={containerDimensions.width}
|
||||
height={containerDimensions.height}
|
||||
|
||||
@@ -48,8 +48,8 @@ jest.mock(
|
||||
{JSON.stringify({
|
||||
legendPosition: props.legendConfig?.position,
|
||||
isQueriesMerged: props.isQueriesMerged,
|
||||
yAxisUnit: props.yAxisUnit,
|
||||
decimalPrecision: props.decimalPrecision,
|
||||
yAxisUnit: props?.yAxisUnit,
|
||||
decimalPrecision: props?.decimalPrecision,
|
||||
})}
|
||||
</div>
|
||||
{props.layoutChildren}
|
||||
|
||||
@@ -112,9 +112,9 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
legendConfig={{
|
||||
position: widget?.legendPosition ?? LegendPosition.BOTTOM,
|
||||
}}
|
||||
timezone={timezone}
|
||||
yAxisUnit={widget.yAxisUnit}
|
||||
decimalPrecision={widget.decimalPrecision}
|
||||
timezone={timezone}
|
||||
data={chartData as uPlot.AlignedData}
|
||||
width={containerDimensions.width}
|
||||
height={containerDimensions.height}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { RuletypesAlertTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { dataSourceForAlertType } from 'constants/alerts';
|
||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export function sanitizeDefaultAlertQuery(
|
||||
query: Query,
|
||||
alertType: RuletypesAlertTypeDTO | undefined,
|
||||
alertType: AlertTypes,
|
||||
): Query {
|
||||
// If there are no queries, add a default one based on the alert type
|
||||
if (query.builder.queryData.length === 0) {
|
||||
const dataSource = dataSourceForAlertType(alertType);
|
||||
const dataSource = ALERTS_DATA_SOURCE_MAP[alertType];
|
||||
query.builder.queryData.push(initialQueryBuilderFormValuesMap[dataSource]);
|
||||
}
|
||||
return query;
|
||||
|
||||
@@ -6,15 +6,9 @@ import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { ExclamationCircleOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
import { Button, FormInstance, Modal, SelectProps, Typography } from 'antd';
|
||||
import saveAlertApi from 'api/alerts/save';
|
||||
import testAlertApi from 'api/alerts/testAlert';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
createRule,
|
||||
testRule,
|
||||
updateRuleByID,
|
||||
} from 'api/generated/services/rules';
|
||||
import type { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
|
||||
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
||||
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
||||
@@ -38,16 +32,13 @@ import { isEmpty, isEqual } from 'lodash-es';
|
||||
import { BellDot, ExternalLink } from 'lucide-react';
|
||||
import Tabs2 from 'periscope/components/Tabs2';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { toPostableRuleDTOFromAlertDef } from 'types/api/alerts/convert';
|
||||
import {
|
||||
AlertDef,
|
||||
defaultEvalWindow,
|
||||
defaultMatchType,
|
||||
} from 'types/api/alerts/def';
|
||||
import APIError from 'types/api/error';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryFunction } from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
@@ -377,7 +368,6 @@ function FormAlertRules({
|
||||
redirectWithQueryBuilderData(query);
|
||||
};
|
||||
const { notifications } = useNotifications();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const validatePromParams = useCallback((): boolean => {
|
||||
let retval = true;
|
||||
@@ -543,47 +533,59 @@ function FormAlertRules({
|
||||
};
|
||||
|
||||
try {
|
||||
if (ruleId && !isEmpty(ruleId)) {
|
||||
await updateRuleByID(
|
||||
{ id: ruleId },
|
||||
toPostableRuleDTOFromAlertDef(postableAlert),
|
||||
);
|
||||
const apiReq =
|
||||
ruleId && !isEmpty(ruleId)
|
||||
? { data: postableAlert, id: ruleId }
|
||||
: { data: postableAlert };
|
||||
|
||||
const response = await saveAlertApi(apiReq);
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
logData = {
|
||||
status: 'success',
|
||||
statusMessage: isNewRule ? t('rule_created') : t('rule_edited'),
|
||||
};
|
||||
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: logData.statusMessage,
|
||||
});
|
||||
|
||||
// invalidate rule in cache
|
||||
ruleCache.invalidateQueries([
|
||||
REACT_QUERY_KEY.ALERT_RULE_DETAILS,
|
||||
`${ruleId}`,
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setTimeout(() => {
|
||||
urlQuery.delete(QueryParams.compositeQuery);
|
||||
urlQuery.delete(QueryParams.panelTypes);
|
||||
urlQuery.delete(QueryParams.ruleId);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
|
||||
}, 2000);
|
||||
} else {
|
||||
await createRule(toPostableRuleDTOFromAlertDef(postableAlert));
|
||||
logData = {
|
||||
status: 'error',
|
||||
statusMessage: response.error || t('unexpected_error'),
|
||||
};
|
||||
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: logData.statusMessage,
|
||||
});
|
||||
}
|
||||
|
||||
logData = {
|
||||
status: 'success',
|
||||
statusMessage: isNewRule ? t('rule_created') : t('rule_edited'),
|
||||
};
|
||||
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: logData.statusMessage,
|
||||
});
|
||||
|
||||
// invalidate rule in cache
|
||||
ruleCache.invalidateQueries([
|
||||
REACT_QUERY_KEY.ALERT_RULE_DETAILS,
|
||||
`${ruleId}`,
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setTimeout(() => {
|
||||
urlQuery.delete(QueryParams.compositeQuery);
|
||||
urlQuery.delete(QueryParams.panelTypes);
|
||||
urlQuery.delete(QueryParams.ruleId);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
const apiError = convertToApiError(e as AxiosError<RenderErrorResponseDTO>);
|
||||
logData = {
|
||||
status: 'error',
|
||||
statusMessage: apiError?.getErrorMessage() || t('unexpected_error'),
|
||||
statusMessage: t('unexpected_error'),
|
||||
};
|
||||
|
||||
showErrorModal(apiError as APIError);
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: logData.statusMessage,
|
||||
});
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
@@ -639,30 +641,39 @@ function FormAlertRules({
|
||||
let statusResponse = { status: 'failed', message: '' };
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await testRule(
|
||||
toPostableRuleDTOFromAlertDef(postableAlert),
|
||||
);
|
||||
const response = await testAlertApi({ data: postableAlert });
|
||||
|
||||
if (response.data?.alertCount === 0) {
|
||||
if (response.statusCode === 200) {
|
||||
const { payload } = response;
|
||||
if (payload?.alertCount === 0) {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('no_alerts_found'),
|
||||
});
|
||||
statusResponse = { status: 'failed', message: t('no_alerts_found') };
|
||||
} else {
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: t('rule_test_fired'),
|
||||
});
|
||||
statusResponse = { status: 'success', message: t('rule_test_fired') };
|
||||
}
|
||||
} else {
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('no_alerts_found'),
|
||||
description: response.error || t('unexpected_error'),
|
||||
});
|
||||
statusResponse = { status: 'failed', message: t('no_alerts_found') };
|
||||
} else {
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: t('rule_test_fired'),
|
||||
});
|
||||
statusResponse = { status: 'success', message: t('rule_test_fired') };
|
||||
statusResponse = {
|
||||
status: 'failed',
|
||||
message: response.error || t('unexpected_error'),
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
const apiError = convertToApiError(e as AxiosError<RenderErrorResponseDTO>);
|
||||
statusResponse = {
|
||||
status: 'failed',
|
||||
message: apiError?.getErrorMessage() || t('unexpected_error'),
|
||||
};
|
||||
showErrorModal(apiError as APIError);
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description: t('unexpected_error'),
|
||||
});
|
||||
statusResponse = { status: 'failed', message: t('unexpected_error') };
|
||||
}
|
||||
setLoading(false);
|
||||
logEvent('Alert: Test notification', {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Button, Skeleton, Tag } from 'antd';
|
||||
import getAll from 'api/alerts/getAll';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useListRules } from 'api/generated/services/rules';
|
||||
import type { RuletypesRuleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
@@ -11,7 +11,7 @@ import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/map
|
||||
import { ArrowRight, ArrowUpRight, Plus } from 'lucide-react';
|
||||
import Card from 'periscope/components/Card/Card';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { toCompositeMetricQuery } from 'types/api/alerts/convert';
|
||||
import { GettableAlert } from 'types/api/alerts/get';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import beaconUrl from '@/assets/Icons/beacon.svg';
|
||||
@@ -28,23 +28,22 @@ export default function AlertRules({
|
||||
const { user } = useAppContext();
|
||||
const [rulesExist, setRulesExist] = useState(false);
|
||||
|
||||
const [sortedAlertRules, setSortedAlertRules] = useState<RuletypesRuleDTO[]>(
|
||||
[],
|
||||
);
|
||||
const [sortedAlertRules, setSortedAlertRules] = useState<GettableAlert[]>([]);
|
||||
|
||||
const location = useLocation();
|
||||
const params = new URLSearchParams(location.search);
|
||||
|
||||
// Fetch Alerts
|
||||
const { data: alerts, isError, isLoading } = useListRules({
|
||||
query: { cacheTime: 0 },
|
||||
const { data: alerts, isError, isLoading } = useQuery('allAlerts', {
|
||||
queryFn: getAll,
|
||||
cacheTime: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const rules = alerts?.data ?? [];
|
||||
const rules = alerts?.payload || [];
|
||||
setRulesExist(rules.length > 0);
|
||||
|
||||
const sortedRules = [...rules].sort((a, b) => {
|
||||
const sortedRules = rules.sort((a, b) => {
|
||||
// First, prioritize firing alerts
|
||||
if (a.state === 'firing' && b.state !== 'firing') {
|
||||
return -1;
|
||||
@@ -53,10 +52,10 @@ export default function AlertRules({
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Then sort by updatedAt timestamp
|
||||
return (
|
||||
new Date(b.updatedAt ?? 0).getTime() - new Date(a.updatedAt ?? 0).getTime()
|
||||
);
|
||||
// Then sort by updateAt timestamp
|
||||
const aUpdateAt = new Date(a.updateAt).getTime();
|
||||
const bUpdateAt = new Date(b.updateAt).getTime();
|
||||
return bUpdateAt - aUpdateAt;
|
||||
});
|
||||
|
||||
if (sortedRules.length > 0 && !loadingUserPreferences) {
|
||||
@@ -119,27 +118,22 @@ export default function AlertRules({
|
||||
</div>
|
||||
);
|
||||
|
||||
const onEditHandler = (record: RuletypesRuleDTO) => (): void => {
|
||||
const onEditHandler = (record: GettableAlert) => (): void => {
|
||||
logEvent('Homepage: Alert clicked', {
|
||||
ruleId: record.id,
|
||||
ruleName: record.alert,
|
||||
ruleState: record.state,
|
||||
});
|
||||
|
||||
const compositeQuery = mapQueryDataFromApi(
|
||||
toCompositeMetricQuery(record.condition.compositeQuery),
|
||||
);
|
||||
const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery);
|
||||
params.set(
|
||||
QueryParams.compositeQuery,
|
||||
encodeURIComponent(JSON.stringify(compositeQuery)),
|
||||
);
|
||||
|
||||
const panelType = record.condition.compositeQuery.panelType;
|
||||
if (panelType) {
|
||||
params.set(QueryParams.panelTypes, panelType);
|
||||
}
|
||||
params.set(QueryParams.panelTypes, record.condition.compositeQuery.panelType);
|
||||
|
||||
params.set(QueryParams.ruleId, record.id);
|
||||
params.set(QueryParams.ruleId, record.id.toString());
|
||||
|
||||
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
||||
};
|
||||
@@ -175,9 +169,9 @@ export default function AlertRules({
|
||||
<div className="alert-rule-item-description home-data-item-tag">
|
||||
<Tag color={rule?.labels?.severity}>{rule?.labels?.severity}</Tag>
|
||||
|
||||
{rule.state === 'firing' && (
|
||||
{rule?.state === 'firing' && (
|
||||
<Tag color="red" className="firing-tag">
|
||||
{rule.state}
|
||||
{rule?.state}
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
import type { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { deleteRuleByID } from 'api/generated/services/rules';
|
||||
import type {
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesRuleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import deleteAlerts from 'api/alerts/delete';
|
||||
import { State } from 'hooks/useFetch';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { PayloadProps as DeleteAlertPayloadProps } from 'types/api/alerts/delete';
|
||||
import APIError from 'types/api/error';
|
||||
import { GettableAlert } from 'types/api/alerts/get';
|
||||
|
||||
import { ColumnButton } from './styles';
|
||||
|
||||
@@ -29,31 +22,48 @@ function DeleteAlert({
|
||||
payload: undefined,
|
||||
});
|
||||
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const defaultErrorMessage = 'Something went wrong';
|
||||
|
||||
const onDeleteHandler = async (id: string): Promise<void> => {
|
||||
try {
|
||||
await deleteRuleByID({ id });
|
||||
|
||||
setData((state) => state.filter((alert) => alert.id !== id));
|
||||
|
||||
setDeleteAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
}));
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
const response = await deleteAlerts({
|
||||
id,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
setData((state) => state.filter((alert) => alert.id !== id));
|
||||
|
||||
setDeleteAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
payload: response.payload,
|
||||
}));
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
});
|
||||
} else {
|
||||
setDeleteAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
error: true,
|
||||
errorMessage: response.error || defaultErrorMessage,
|
||||
}));
|
||||
|
||||
notifications.error({
|
||||
message: response.error || defaultErrorMessage,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setDeleteAlertState((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
error: true,
|
||||
errorMessage: defaultErrorMessage,
|
||||
}));
|
||||
|
||||
showErrorModal(
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
|
||||
);
|
||||
notifications.error({
|
||||
message: defaultErrorMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -78,8 +88,8 @@ function DeleteAlert({
|
||||
}
|
||||
|
||||
interface DeleteAlertProps {
|
||||
id: string;
|
||||
setData: Dispatch<SetStateAction<RuletypesRuleDTO[]>>;
|
||||
id: GettableAlert['id'];
|
||||
setData: Dispatch<SetStateAction<GettableAlert[]>>;
|
||||
notifications: NotificationInstance;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,16 +4,8 @@ import { UseQueryResult } from 'react-query';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Flex, Input, Typography } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table/interface';
|
||||
import saveAlertApi from 'api/alerts/save';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { createRule } from 'api/generated/services/rules';
|
||||
import type {
|
||||
ListRules200,
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesRuleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { ErrorType } from 'api/generatedAPIInstance';
|
||||
import { AxiosError } from 'axios';
|
||||
import DropDown from 'components/DropDown/DropDown';
|
||||
import {
|
||||
DynamicColumnsKey,
|
||||
@@ -35,9 +27,9 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { toCompositeMetricQuery } from 'types/api/alerts/convert';
|
||||
import APIError from 'types/api/error';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { GettableAlert } from 'types/api/alerts/get';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import DeleteAlert from './DeleteAlert';
|
||||
@@ -66,7 +58,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
const paginationParam = params.get('page');
|
||||
const searchParams = params.get('search');
|
||||
const [searchString, setSearchString] = useState<string>(searchParams || '');
|
||||
const [data, setData] = useState<RuletypesRuleDTO[]>(() => {
|
||||
const [data, setData] = useState<GettableAlert[]>(() => {
|
||||
const value = searchString.toLowerCase();
|
||||
const filteredData = filterAlerts(allAlertRules, value);
|
||||
return filteredData || [];
|
||||
@@ -78,7 +70,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
? orderQueryParam
|
||||
: null;
|
||||
|
||||
const { sortedInfo, handleChange } = useSortableTable<RuletypesRuleDTO>(
|
||||
const { sortedInfo, handleChange } = useSortableTable<GettableAlert>(
|
||||
sortingOrder,
|
||||
orderColumnParam || '',
|
||||
searchString,
|
||||
@@ -91,7 +83,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
const { data: refetchData, status } = await refetch();
|
||||
if (status === 'success') {
|
||||
const value = searchString.toLowerCase();
|
||||
const filteredData = filterAlerts(refetchData?.data ?? [], value);
|
||||
const filteredData = filterAlerts(refetchData.payload || [], value);
|
||||
setData(filteredData || []);
|
||||
}
|
||||
if (status === 'error') {
|
||||
@@ -102,7 +94,11 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
})();
|
||||
}, 30000);
|
||||
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const handleError = useCallback((): void => {
|
||||
notificationsApi.error({
|
||||
message: t('something_went_wrong'),
|
||||
});
|
||||
}, [notificationsApi, t]);
|
||||
|
||||
const onClickNewAlertHandler = useCallback(
|
||||
(e: React.MouseEvent): void => {
|
||||
@@ -119,24 +115,21 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
);
|
||||
|
||||
const onEditHandler = (
|
||||
record: RuletypesRuleDTO,
|
||||
record: GettableAlert,
|
||||
options?: { newTab?: boolean },
|
||||
): void => {
|
||||
const compositeQuery = sanitizeDefaultAlertQuery(
|
||||
mapQueryDataFromApi(toCompositeMetricQuery(record.condition.compositeQuery)),
|
||||
record.alertType,
|
||||
mapQueryDataFromApi(record.condition.compositeQuery),
|
||||
record.alertType as AlertTypes,
|
||||
);
|
||||
params.set(
|
||||
QueryParams.compositeQuery,
|
||||
encodeURIComponent(JSON.stringify(compositeQuery)),
|
||||
);
|
||||
|
||||
const panelType = record.condition.compositeQuery.panelType;
|
||||
if (panelType) {
|
||||
params.set(QueryParams.panelTypes, panelType);
|
||||
}
|
||||
params.set(QueryParams.panelTypes, record.condition.compositeQuery.panelType);
|
||||
|
||||
params.set(QueryParams.ruleId, record.id);
|
||||
params.set(QueryParams.ruleId, record.id.toString());
|
||||
|
||||
setEditLoader(false);
|
||||
|
||||
@@ -146,41 +139,47 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
};
|
||||
|
||||
const onCloneHandler = (
|
||||
originalAlert: RuletypesRuleDTO,
|
||||
originalAlert: GettableAlert,
|
||||
) => async (): Promise<void> => {
|
||||
const copyAlert: RuletypesRuleDTO = {
|
||||
const copyAlert = {
|
||||
...originalAlert,
|
||||
alert: `${originalAlert.alert} - Copy`,
|
||||
alert: originalAlert.alert.concat(' - Copy'),
|
||||
};
|
||||
const apiReq = { data: copyAlert };
|
||||
|
||||
try {
|
||||
setCloneLoader(true);
|
||||
await createRule(copyAlert);
|
||||
const response = await saveAlertApi(apiReq);
|
||||
|
||||
notificationsApi.success({
|
||||
message: 'Success',
|
||||
description: 'Alert cloned successfully',
|
||||
});
|
||||
if (response.statusCode === 200) {
|
||||
notificationsApi.success({
|
||||
message: 'Success',
|
||||
description: 'Alert cloned successfully',
|
||||
});
|
||||
|
||||
const { data: refetchData, status } = await refetch();
|
||||
const rules = refetchData?.data;
|
||||
if (status === 'success' && rules) {
|
||||
setData(rules);
|
||||
setTimeout(() => {
|
||||
const clonedAlert = rules[rules.length - 1];
|
||||
params.set(QueryParams.ruleId, String(clonedAlert.id));
|
||||
safeNavigate(`${ROUTES.EDIT_ALERTS}?${params.toString()}`);
|
||||
}, 2000);
|
||||
}
|
||||
if (status === 'error') {
|
||||
const { data: refetchData, status } = await refetch();
|
||||
if (status === 'success' && refetchData.payload) {
|
||||
setData(refetchData.payload || []);
|
||||
setTimeout(() => {
|
||||
const clonedAlert = refetchData.payload[refetchData.payload.length - 1];
|
||||
params.set(QueryParams.ruleId, String(clonedAlert.id));
|
||||
safeNavigate(`${ROUTES.EDIT_ALERTS}?${params.toString()}`);
|
||||
}, 2000);
|
||||
}
|
||||
if (status === 'error') {
|
||||
notificationsApi.error({
|
||||
message: t('something_went_wrong'),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
notificationsApi.error({
|
||||
message: t('something_went_wrong'),
|
||||
message: 'Error',
|
||||
description: response.error || t('something_went_wrong'),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
showErrorModal(
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
|
||||
);
|
||||
handleError();
|
||||
console.error(error);
|
||||
} finally {
|
||||
setCloneLoader(false);
|
||||
}
|
||||
@@ -193,16 +192,16 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
setData(filteredData);
|
||||
});
|
||||
|
||||
const dynamicColumns: ColumnsType<RuletypesRuleDTO> = [
|
||||
const dynamicColumns: ColumnsType<GettableAlert> = [
|
||||
{
|
||||
title: 'Created At',
|
||||
dataIndex: 'createdAt',
|
||||
dataIndex: 'createAt',
|
||||
width: 80,
|
||||
key: DynamicColumnsKey.CreatedAt,
|
||||
align: 'center',
|
||||
sorter: (a: RuletypesRuleDTO, b: RuletypesRuleDTO): number => {
|
||||
const prev = a.createdAt ? new Date(a.createdAt).getTime() : 0;
|
||||
const next = b.createdAt ? new Date(b.createdAt).getTime() : 0;
|
||||
sorter: (a: GettableAlert, b: GettableAlert): number => {
|
||||
const prev = new Date(a.createAt).getTime();
|
||||
const next = new Date(b.createAt).getTime();
|
||||
|
||||
return prev - next;
|
||||
},
|
||||
@@ -214,20 +213,20 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
},
|
||||
{
|
||||
title: 'Created By',
|
||||
dataIndex: 'createdBy',
|
||||
dataIndex: 'createBy',
|
||||
width: 80,
|
||||
key: DynamicColumnsKey.CreatedBy,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: 'Updated At',
|
||||
dataIndex: 'updatedAt',
|
||||
dataIndex: 'updateAt',
|
||||
width: 80,
|
||||
key: DynamicColumnsKey.UpdatedAt,
|
||||
align: 'center',
|
||||
sorter: (a: RuletypesRuleDTO, b: RuletypesRuleDTO): number => {
|
||||
const prev = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
||||
const next = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
||||
sorter: (a: GettableAlert, b: GettableAlert): number => {
|
||||
const prev = new Date(a.updateAt).getTime();
|
||||
const next = new Date(b.updateAt).getTime();
|
||||
|
||||
return prev - next;
|
||||
},
|
||||
@@ -239,14 +238,14 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
},
|
||||
{
|
||||
title: 'Updated By',
|
||||
dataIndex: 'updatedBy',
|
||||
dataIndex: 'updateBy',
|
||||
width: 80,
|
||||
key: DynamicColumnsKey.UpdatedBy,
|
||||
align: 'center',
|
||||
},
|
||||
];
|
||||
|
||||
const columns: ColumnsType<RuletypesRuleDTO> = [
|
||||
const columns: ColumnsType<GettableAlert> = [
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'state',
|
||||
@@ -323,7 +322,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
dataIndex: 'id',
|
||||
key: 'action',
|
||||
width: 10,
|
||||
render: (id: RuletypesRuleDTO['id'], record): JSX.Element => (
|
||||
render: (id: GettableAlert['id'], record): JSX.Element => (
|
||||
<div data-testid="alert-actions">
|
||||
<DropDown
|
||||
onDropDownItemClick={(item): void =>
|
||||
@@ -332,9 +331,9 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
element={[
|
||||
<ToggleAlertState
|
||||
key="1"
|
||||
disabled={record.disabled ?? false}
|
||||
disabled={record.disabled}
|
||||
setData={setData}
|
||||
id={id ?? ''}
|
||||
id={id}
|
||||
/>,
|
||||
<ColumnButton
|
||||
key="2"
|
||||
@@ -366,7 +365,7 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
key="4"
|
||||
notifications={notificationsApi}
|
||||
setData={setData}
|
||||
id={id ?? ''}
|
||||
id={id}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
@@ -421,10 +420,9 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
}
|
||||
|
||||
interface ListAlertProps {
|
||||
allAlertRules: RuletypesRuleDTO[];
|
||||
allAlertRules: GettableAlert[];
|
||||
refetch: UseQueryResult<
|
||||
ListRules200,
|
||||
ErrorType<RenderErrorResponseDTO>
|
||||
ErrorResponse | SuccessResponse<GettableAlert[]>
|
||||
>['refetch'];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Tag } from 'antd';
|
||||
import type { RuletypesRuleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { GettableAlert } from 'types/api/alerts/get';
|
||||
|
||||
function Status({ status }: StatusProps): JSX.Element {
|
||||
switch (status) {
|
||||
@@ -26,7 +26,7 @@ function Status({ status }: StatusProps): JSX.Element {
|
||||
}
|
||||
|
||||
interface StatusProps {
|
||||
status: RuletypesRuleDTO['state'];
|
||||
status: GettableAlert['state'];
|
||||
}
|
||||
|
||||
export default Status;
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
import { patchRulePartial } from 'api/alerts/patchRulePartial';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import type {
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesRuleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import patchAlert from 'api/alerts/patch';
|
||||
import { State } from 'hooks/useFetch';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
import { GettableAlert } from 'types/api/alerts/get';
|
||||
import { PayloadProps as PatchPayloadProps } from 'types/api/alerts/patch';
|
||||
|
||||
import { ColumnButton } from './styles';
|
||||
|
||||
@@ -18,7 +12,7 @@ function ToggleAlertState({
|
||||
disabled,
|
||||
setData,
|
||||
}: ToggleAlertStateProps): JSX.Element {
|
||||
const [apiStatus, setAPIStatus] = useState<State<RuletypesRuleDTO>>({
|
||||
const [apiStatus, setAPIStatus] = useState<State<PatchPayloadProps>>({
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: false,
|
||||
@@ -27,7 +21,8 @@ function ToggleAlertState({
|
||||
});
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const defaultErrorMessage = 'Something went wrong';
|
||||
|
||||
const onToggleHandler = async (
|
||||
id: string,
|
||||
@@ -39,40 +34,58 @@ function ToggleAlertState({
|
||||
loading: true,
|
||||
}));
|
||||
|
||||
const response = await patchRulePartial(id, { disabled });
|
||||
const { data: updatedRule } = response;
|
||||
|
||||
setData((state) =>
|
||||
state.map((alert) => {
|
||||
if (alert.id === id) {
|
||||
return {
|
||||
...alert,
|
||||
disabled: updatedRule.disabled,
|
||||
state: updatedRule.state,
|
||||
};
|
||||
}
|
||||
return alert;
|
||||
}),
|
||||
);
|
||||
|
||||
setAPIStatus((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
payload: updatedRule,
|
||||
}));
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
const response = await patchAlert({
|
||||
id,
|
||||
data: {
|
||||
disabled,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
setData((state) =>
|
||||
state.map((alert) => {
|
||||
if (alert.id === id) {
|
||||
return {
|
||||
...alert,
|
||||
disabled: response.payload.disabled,
|
||||
state: response.payload.state,
|
||||
};
|
||||
}
|
||||
return alert;
|
||||
}),
|
||||
);
|
||||
|
||||
setAPIStatus((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
payload: response.payload,
|
||||
}));
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
});
|
||||
} else {
|
||||
setAPIStatus((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
error: true,
|
||||
errorMessage: response.error || defaultErrorMessage,
|
||||
}));
|
||||
|
||||
notifications.error({
|
||||
message: response.error || defaultErrorMessage,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setAPIStatus((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
error: true,
|
||||
errorMessage: defaultErrorMessage,
|
||||
}));
|
||||
|
||||
showErrorModal(
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
|
||||
);
|
||||
notifications.error({
|
||||
message: defaultErrorMessage,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -89,9 +102,9 @@ function ToggleAlertState({
|
||||
}
|
||||
|
||||
interface ToggleAlertStateProps {
|
||||
id: string;
|
||||
id: GettableAlert['id'];
|
||||
disabled: boolean;
|
||||
setData: Dispatch<SetStateAction<RuletypesRuleDTO[]>>;
|
||||
setData: Dispatch<SetStateAction<GettableAlert[]>>;
|
||||
}
|
||||
|
||||
export default ToggleAlertState;
|
||||
|
||||
@@ -1,69 +1,52 @@
|
||||
import type {
|
||||
RuletypesAlertStateDTO,
|
||||
RuletypesCompareOperatorDTO,
|
||||
RuletypesMatchTypeDTO,
|
||||
RuletypesPanelTypeDTO,
|
||||
RuletypesQueryTypeDTO,
|
||||
RuletypesRuleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { GettableAlert } from 'types/api/alerts/get';
|
||||
|
||||
import { filterAlerts } from '../utils';
|
||||
|
||||
describe('filterAlerts', () => {
|
||||
const mockAlertBase: Partial<RuletypesRuleDTO> = {
|
||||
state: 'active' as RuletypesAlertStateDTO,
|
||||
const mockAlertBase: Partial<GettableAlert> = {
|
||||
state: 'active',
|
||||
disabled: false,
|
||||
createdAt: new Date('2024-01-01T00:00:00Z'),
|
||||
createdBy: 'test-user',
|
||||
updatedAt: new Date('2024-01-01T00:00:00Z'),
|
||||
updatedBy: 'test-user',
|
||||
createAt: '2024-01-01T00:00:00Z',
|
||||
createBy: 'test-user',
|
||||
updateAt: '2024-01-01T00:00:00Z',
|
||||
updateBy: 'test-user',
|
||||
version: '1',
|
||||
condition: {
|
||||
compositeQuery: {
|
||||
queries: [],
|
||||
panelType: 'graph' as RuletypesPanelTypeDTO,
|
||||
queryType: 'builder' as RuletypesQueryTypeDTO,
|
||||
},
|
||||
matchType: 'at_least_once' as RuletypesMatchTypeDTO,
|
||||
op: 'above' as RuletypesCompareOperatorDTO,
|
||||
},
|
||||
ruleType: 'threshold_rule' as RuletypesRuleDTO['ruleType'],
|
||||
};
|
||||
|
||||
const mockAlerts: RuletypesRuleDTO[] = [
|
||||
const mockAlerts: GettableAlert[] = [
|
||||
{
|
||||
...mockAlertBase,
|
||||
id: '1',
|
||||
alert: 'High CPU Usage',
|
||||
alertType: 'METRIC_BASED_ALERT',
|
||||
alertType: 'metrics',
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
status: 'ok',
|
||||
environment: 'production',
|
||||
},
|
||||
} as RuletypesRuleDTO,
|
||||
} as GettableAlert,
|
||||
{
|
||||
...mockAlertBase,
|
||||
id: '2',
|
||||
alert: 'Memory Leak Detected',
|
||||
alertType: 'METRIC_BASED_ALERT',
|
||||
alertType: 'metrics',
|
||||
labels: {
|
||||
severity: 'critical',
|
||||
status: 'firing',
|
||||
environment: 'staging',
|
||||
},
|
||||
} as RuletypesRuleDTO,
|
||||
} as GettableAlert,
|
||||
{
|
||||
...mockAlertBase,
|
||||
id: '3',
|
||||
alert: 'Database Connection Error',
|
||||
alertType: 'METRIC_BASED_ALERT',
|
||||
alertType: 'metrics',
|
||||
labels: {
|
||||
severity: 'error',
|
||||
status: 'pending',
|
||||
environment: 'production',
|
||||
},
|
||||
} as RuletypesRuleDTO,
|
||||
} as GettableAlert,
|
||||
];
|
||||
|
||||
it('should return all alerts when filter is empty', () => {
|
||||
@@ -114,14 +97,14 @@ describe('filterAlerts', () => {
|
||||
});
|
||||
|
||||
it('should handle alerts with missing labels', () => {
|
||||
const alertsWithMissingLabels: RuletypesRuleDTO[] = [
|
||||
const alertsWithMissingLabels: GettableAlert[] = [
|
||||
{
|
||||
...mockAlertBase,
|
||||
id: '4',
|
||||
alert: 'Test Alert',
|
||||
alertType: 'METRIC_BASED_ALERT',
|
||||
alertType: 'metrics',
|
||||
labels: undefined,
|
||||
} as RuletypesRuleDTO,
|
||||
} as GettableAlert,
|
||||
];
|
||||
const result = filterAlerts(alertsWithMissingLabels, 'test');
|
||||
expect(result).toHaveLength(1);
|
||||
@@ -129,16 +112,16 @@ describe('filterAlerts', () => {
|
||||
});
|
||||
|
||||
it('should handle alerts with missing alert name', () => {
|
||||
const alertsWithMissingName: RuletypesRuleDTO[] = [
|
||||
const alertsWithMissingName: GettableAlert[] = [
|
||||
{
|
||||
...mockAlertBase,
|
||||
id: '5',
|
||||
alert: '',
|
||||
alertType: 'METRIC_BASED_ALERT',
|
||||
alertType: 'metrics',
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
},
|
||||
} as RuletypesRuleDTO,
|
||||
} as GettableAlert,
|
||||
];
|
||||
const result = filterAlerts(alertsWithMissingName, 'warning');
|
||||
expect(result).toHaveLength(1);
|
||||
|
||||
@@ -1,66 +1,78 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Space } from 'antd';
|
||||
import getAll from 'api/alerts/getAll';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { useListRules } from 'api/generated/services/rules';
|
||||
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
|
||||
import { AlertsEmptyState } from './AlertsEmptyState/AlertsEmptyState';
|
||||
import ListAlert from './ListAlert';
|
||||
|
||||
function ListAlertRules(): JSX.Element {
|
||||
const { t } = useTranslation('common');
|
||||
const { data, isError, isLoading, refetch, error } = useListRules({
|
||||
query: { cacheTime: 0 },
|
||||
const { data, isError, isLoading, refetch, status } = useQuery('allAlerts', {
|
||||
queryFn: getAll,
|
||||
cacheTime: 0,
|
||||
});
|
||||
|
||||
const rules = data?.data ?? [];
|
||||
const hasLoaded = !isLoading && data !== undefined;
|
||||
const logEventCalledRef = useRef(false);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const apiError = useMemo(
|
||||
() => convertToApiError(error as AxiosError<RenderErrorResponseDTO> | null),
|
||||
[error],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current && hasLoaded) {
|
||||
if (!logEventCalledRef.current && !isUndefined(data?.payload)) {
|
||||
logEvent('Alert: List page visited', {
|
||||
number: rules.length,
|
||||
number: data?.payload?.length,
|
||||
});
|
||||
logEventCalledRef.current = true;
|
||||
}
|
||||
}, [hasLoaded, rules.length]);
|
||||
}, [data?.payload]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
if (status === 'error' || (status === 'success' && data.statusCode >= 400)) {
|
||||
notifications.error({
|
||||
message: apiError?.getErrorMessage() || t('something_went_wrong'),
|
||||
message: data?.error || t('something_went_wrong'),
|
||||
});
|
||||
}
|
||||
}, [isError, apiError, t, notifications]);
|
||||
}, [data?.error, data?.statusCode, status, t, notifications]);
|
||||
|
||||
// api failed to load the data
|
||||
if (isError) {
|
||||
return <div>{apiError?.getErrorMessage() || t('something_went_wrong')}</div>;
|
||||
return <div>{data?.error || t('something_went_wrong')}</div>;
|
||||
}
|
||||
|
||||
if (isLoading || !data) {
|
||||
return <Spinner height="75vh" tip="Loading Rules..." />;
|
||||
// api is successful but error is present
|
||||
if (status === 'success' && data.statusCode >= 400) {
|
||||
return (
|
||||
<ListAlert
|
||||
{...{
|
||||
allAlertRules: [],
|
||||
refetch,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (rules.length === 0) {
|
||||
if (status === 'success' && !data.payload?.length) {
|
||||
return <AlertsEmptyState />;
|
||||
}
|
||||
|
||||
// in case of loading
|
||||
if (isLoading || !data?.payload) {
|
||||
return <Spinner height="75vh" tip="Loading Rules..." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<ListAlert allAlertRules={rules} refetch={refetch} />
|
||||
<ListAlert
|
||||
{...{
|
||||
allAlertRules: data.payload,
|
||||
refetch,
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import type { RuletypesRuleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { dataSourceForAlertType } from 'constants/alerts';
|
||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { GettableAlert } from 'types/api/alerts/get';
|
||||
|
||||
export const filterAlerts = (
|
||||
allAlertRules: RuletypesRuleDTO[],
|
||||
allAlertRules: GettableAlert[],
|
||||
filter: string,
|
||||
): RuletypesRuleDTO[] => {
|
||||
): GettableAlert[] => {
|
||||
if (!filter.trim()) {
|
||||
return allAlertRules;
|
||||
}
|
||||
|
||||
const value = filter.trim().toLowerCase();
|
||||
return allAlertRules.filter((alert) => {
|
||||
const alertName = alert.alert.toLowerCase();
|
||||
const alertName = alert.alert?.toLowerCase();
|
||||
const severity = alert.labels?.severity?.toLowerCase();
|
||||
|
||||
// Create a string of all label keys and values for searching
|
||||
@@ -22,7 +23,7 @@ export const filterAlerts = (
|
||||
.toLowerCase();
|
||||
|
||||
return (
|
||||
alertName.includes(value) ||
|
||||
alertName?.includes(value) ||
|
||||
severity?.includes(value) ||
|
||||
labelSearchString.includes(value)
|
||||
);
|
||||
@@ -31,7 +32,7 @@ export const filterAlerts = (
|
||||
|
||||
export const alertActionLogEvent = (
|
||||
action: string,
|
||||
record: RuletypesRuleDTO,
|
||||
record: GettableAlert,
|
||||
): void => {
|
||||
let actionValue = '';
|
||||
switch (action) {
|
||||
@@ -51,9 +52,9 @@ export const alertActionLogEvent = (
|
||||
break;
|
||||
}
|
||||
logEvent('Alert: Action', {
|
||||
ruleId: record.id,
|
||||
dataSource: dataSourceForAlertType(record.alertType),
|
||||
name: record.alert,
|
||||
ruleId: record?.id,
|
||||
dataSource: ALERTS_DATA_SOURCE_MAP[record.alertType as AlertTypes],
|
||||
name: record?.alert,
|
||||
action: actionValue,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import React, { ChangeEvent, useEffect, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Flex, Form, Input, Tooltip, Typography } from 'antd';
|
||||
import getAll from 'api/alerts/getAll';
|
||||
import { useDeleteDowntimeSchedule } from 'api/plannedDowntime/deleteDowntimeSchedule';
|
||||
import {
|
||||
useDeleteDowntimeScheduleByID,
|
||||
useListDowntimeSchedules,
|
||||
} from 'api/generated/services/downtimeschedules';
|
||||
import { useListRules } from 'api/generated/services/rules';
|
||||
import type { RuletypesPlannedMaintenanceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
DowntimeSchedules,
|
||||
useGetAllDowntimeSchedules,
|
||||
} from 'api/plannedDowntime/getAllDowntimeSchedules';
|
||||
import dayjs from 'dayjs';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { Search } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import 'dayjs/locale/en';
|
||||
@@ -33,28 +33,28 @@ import './PlannedDowntime.styles.scss';
|
||||
dayjs.locale('en');
|
||||
|
||||
export function PlannedDowntime(): JSX.Element {
|
||||
const { data: alertsData, isError, isLoading } = useListRules({
|
||||
query: { cacheTime: 0 },
|
||||
const { data, isError, isLoading } = useQuery('allAlerts', {
|
||||
queryFn: getAll,
|
||||
cacheTime: 0,
|
||||
});
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const { user } = useAppContext();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const history = useHistory();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const [initialValues, setInitialValues] = useState<
|
||||
Partial<RuletypesPlannedMaintenanceDTO & { editMode: boolean }>
|
||||
Partial<DowntimeSchedules & { editMode: boolean }>
|
||||
>(defautlInitialValues);
|
||||
|
||||
const downtimeSchedules = useListDowntimeSchedules();
|
||||
const downtimeSchedules = useGetAllDowntimeSchedules();
|
||||
const alertOptions = React.useMemo(
|
||||
() =>
|
||||
alertsData?.data?.map((i) => ({
|
||||
data?.payload?.map((i) => ({
|
||||
label: i.alert,
|
||||
value: i.id,
|
||||
})),
|
||||
[alertsData],
|
||||
[data],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -66,7 +66,7 @@ export function PlannedDowntime(): JSX.Element {
|
||||
const [searchValue, setSearchValue] = React.useState<string | number>(
|
||||
urlQuery.get('search') || '',
|
||||
);
|
||||
const [deleteData, setDeleteData] = useState<{ id: string; name: string }>();
|
||||
const [deleteData, setDeleteData] = useState<{ id: number; name: string }>();
|
||||
const [isEditMode, setEditMode] = useState<boolean>(false);
|
||||
|
||||
const updateUrlWithSearch = useDebouncedFn((value) => {
|
||||
@@ -105,13 +105,12 @@ export function PlannedDowntime(): JSX.Element {
|
||||
const {
|
||||
mutateAsync: deleteDowntimeScheduleAsync,
|
||||
isLoading: isDeleteLoading,
|
||||
} = useDeleteDowntimeScheduleByID();
|
||||
} = useDeleteDowntimeSchedule({ id: deleteData?.id });
|
||||
|
||||
const onDeleteHandler = (): void => {
|
||||
deleteDowntimeHandler({
|
||||
deleteDowntimeScheduleAsync,
|
||||
notifications,
|
||||
showErrorModal,
|
||||
refetchAllSchedules,
|
||||
deleteId: deleteData?.id,
|
||||
hideDeleteDowntimeScheduleModal,
|
||||
|
||||
@@ -14,18 +14,11 @@ import {
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { DefaultOptionType } from 'antd/es/select';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
createDowntimeSchedule,
|
||||
updateDowntimeScheduleByID,
|
||||
} from 'api/generated/services/downtimeschedules';
|
||||
import type {
|
||||
RuletypesPlannedMaintenanceDTO,
|
||||
RuletypesPostablePlannedMaintenanceDTO,
|
||||
RuletypesRecurrenceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
DowntimeSchedules,
|
||||
Recurrence,
|
||||
} from 'api/plannedDowntime/getAllDowntimeSchedules';
|
||||
import { DowntimeScheduleUpdatePayload } from 'api/plannedDowntime/updateDowntimeSchedule';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import {
|
||||
ModalButtonWrapper,
|
||||
@@ -36,14 +29,15 @@ import timezone from 'dayjs/plugin/timezone';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { defaultTo, isEmpty } from 'lodash-es';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
import { ALL_TIME_ZONES } from 'utils/timeZoneUtil';
|
||||
|
||||
import 'dayjs/locale/en';
|
||||
|
||||
import { SOMETHING_WENT_WRONG } from '../../constants/api';
|
||||
import { showErrorNotification } from '../../utils/error';
|
||||
import { AlertRuleTags } from './PlannedDowntimeList';
|
||||
import {
|
||||
createEditDowntimeSchedule,
|
||||
getAlertOptionsFromIds,
|
||||
getDurationInfo,
|
||||
getEndTime,
|
||||
@@ -68,9 +62,9 @@ interface PlannedDowntimeFormData {
|
||||
name: string;
|
||||
startTime: dayjs.Dayjs | string;
|
||||
endTime: dayjs.Dayjs | string;
|
||||
recurrence?: RuletypesRecurrenceDTO | null;
|
||||
recurrence?: Recurrence | null;
|
||||
alertRules: DefaultOptionType[];
|
||||
recurrenceSelect?: RuletypesRecurrenceDTO;
|
||||
recurrenceSelect?: Recurrence;
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
@@ -78,7 +72,7 @@ const customFormat = DATE_TIME_FORMATS.ORDINAL_DATETIME;
|
||||
|
||||
interface PlannedDowntimeFormProps {
|
||||
initialValues: Partial<
|
||||
RuletypesPlannedMaintenanceDTO & {
|
||||
DowntimeSchedules & {
|
||||
editMode: boolean;
|
||||
}
|
||||
>;
|
||||
@@ -117,9 +111,9 @@ export function PlannedDowntimeForm(
|
||||
?.unit || 'm',
|
||||
);
|
||||
|
||||
const [formData, setFormData] = useState<Partial<PlannedDowntimeFormData>>({
|
||||
timezone: initialValues.schedule?.timezone,
|
||||
});
|
||||
const [formData, setFormData] = useState<PlannedDowntimeFormData>(
|
||||
initialValues?.schedule as PlannedDowntimeFormData,
|
||||
);
|
||||
|
||||
const [recurrenceType, setRecurrenceType] = useState<string | null>(
|
||||
(initialValues.schedule?.recurrence?.repeatType as string) ||
|
||||
@@ -131,7 +125,6 @@ export function PlannedDowntimeForm(
|
||||
: undefined;
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const datePickerFooter = (mode: any): any =>
|
||||
mode === 'time' ? (
|
||||
@@ -141,54 +134,57 @@ export function PlannedDowntimeForm(
|
||||
const saveHanlder = useCallback(
|
||||
async (values: PlannedDowntimeFormData) => {
|
||||
const shouldKeepLocalTime = !isEditMode;
|
||||
const data: RuletypesPostablePlannedMaintenanceDTO = {
|
||||
alertIds: values.alertRules
|
||||
.map((alert) => alert.value)
|
||||
.filter((alert) => alert !== undefined) as string[],
|
||||
name: values.name,
|
||||
schedule: {
|
||||
startTime: new Date(
|
||||
handleTimeConversion(
|
||||
const createEditProps: DowntimeScheduleUpdatePayload = {
|
||||
data: {
|
||||
alertIds: values.alertRules
|
||||
.map((alert) => alert.value)
|
||||
.filter((alert) => alert !== undefined) as string[],
|
||||
name: values.name,
|
||||
schedule: {
|
||||
startTime: handleTimeConversion(
|
||||
values.startTime,
|
||||
timezoneInitialValue,
|
||||
values.timezone,
|
||||
shouldKeepLocalTime,
|
||||
),
|
||||
),
|
||||
timezone: values.timezone as string,
|
||||
endTime: values.endTime
|
||||
? new Date(
|
||||
handleTimeConversion(
|
||||
timezone: values.timezone,
|
||||
endTime: values.endTime
|
||||
? handleTimeConversion(
|
||||
values.endTime,
|
||||
timezoneInitialValue,
|
||||
values.timezone,
|
||||
shouldKeepLocalTime,
|
||||
),
|
||||
)
|
||||
: undefined,
|
||||
recurrence: values.recurrence as RuletypesRecurrenceDTO,
|
||||
)
|
||||
: undefined,
|
||||
recurrence: values.recurrence as Recurrence,
|
||||
},
|
||||
},
|
||||
id: isEditMode ? initialValues.id : undefined,
|
||||
};
|
||||
|
||||
setSaveLoading(true);
|
||||
try {
|
||||
if (isEditMode && initialValues.id) {
|
||||
await updateDowntimeScheduleByID({ id: initialValues.id }, data);
|
||||
const response = await createEditDowntimeSchedule({ ...createEditProps });
|
||||
if (response.message === 'success') {
|
||||
setIsOpen(false);
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: isEditMode
|
||||
? 'Schedule updated successfully'
|
||||
: 'Schedule created successfully',
|
||||
});
|
||||
refetchAllSchedules();
|
||||
} else {
|
||||
await createDowntimeSchedule(data);
|
||||
notifications.error({
|
||||
message: 'Error',
|
||||
description:
|
||||
typeof response.error === 'string'
|
||||
? response.error
|
||||
: response.error?.message || SOMETHING_WENT_WRONG,
|
||||
});
|
||||
}
|
||||
setIsOpen(false);
|
||||
notifications.success({
|
||||
message: 'Success',
|
||||
description: isEditMode
|
||||
? 'Schedule updated successfully'
|
||||
: 'Schedule created successfully',
|
||||
});
|
||||
refetchAllSchedules();
|
||||
} catch (e: unknown) {
|
||||
showErrorModal(
|
||||
convertToApiError(e as AxiosError<RenderErrorResponseDTO>) as APIError,
|
||||
);
|
||||
showErrorNotification(notifications, e as Error);
|
||||
}
|
||||
setSaveLoading(false);
|
||||
},
|
||||
@@ -199,11 +195,10 @@ export function PlannedDowntimeForm(
|
||||
refetchAllSchedules,
|
||||
setIsOpen,
|
||||
timezoneInitialValue,
|
||||
showErrorModal,
|
||||
],
|
||||
);
|
||||
const onFinish = async (values: PlannedDowntimeFormData): Promise<void> => {
|
||||
const recurrenceData =
|
||||
const recurrenceData: Recurrence | undefined =
|
||||
values?.recurrence?.repeatType === recurrenceOptions.doesNotRepeat.value
|
||||
? undefined
|
||||
: {
|
||||
@@ -230,10 +225,7 @@ export function PlannedDowntimeForm(
|
||||
repeatType: values.recurrence?.repeatType,
|
||||
};
|
||||
|
||||
const payloadValues = {
|
||||
...values,
|
||||
recurrence: recurrenceData as RuletypesRecurrenceDTO | undefined,
|
||||
};
|
||||
const payloadValues = { ...values, recurrence: recurrenceData };
|
||||
await saveHanlder(payloadValues);
|
||||
};
|
||||
|
||||
@@ -244,9 +236,11 @@ export function PlannedDowntimeForm(
|
||||
];
|
||||
|
||||
const handleOk = async (): Promise<void> => {
|
||||
await form.validateFields().catch(() => {
|
||||
// antd renders inline field-level errors; nothing more to do here.
|
||||
});
|
||||
try {
|
||||
await form.validateFields();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = (): void => {
|
||||
@@ -287,19 +281,18 @@ export function PlannedDowntimeForm(
|
||||
: '',
|
||||
recurrence: {
|
||||
...initialValues.schedule?.recurrence,
|
||||
repeatType: (!isScheduleRecurring(initialValues?.schedule)
|
||||
repeatType: !isScheduleRecurring(initialValues?.schedule)
|
||||
? recurrenceOptions.doesNotRepeat.value
|
||||
: initialValues.schedule?.recurrence
|
||||
?.repeatType) as RuletypesRecurrenceDTO['repeatType'],
|
||||
duration: String(
|
||||
getDurationInfo(initialValues.schedule?.recurrence?.duration as string)
|
||||
?.value ?? '',
|
||||
),
|
||||
} as RuletypesRecurrenceDTO,
|
||||
: (initialValues.schedule?.recurrence?.repeatType as string),
|
||||
duration: getDurationInfo(
|
||||
initialValues.schedule?.recurrence?.duration as string,
|
||||
)?.value,
|
||||
},
|
||||
timezone: initialValues.schedule?.timezone as string,
|
||||
};
|
||||
return formData;
|
||||
}, [initialValues, alertOptions]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialValues]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTags(formatedInitialValues.alertRules);
|
||||
@@ -332,12 +325,7 @@ export function PlannedDowntimeForm(
|
||||
const startTimeText = useMemo((): string => {
|
||||
let startTime = formData?.startTime;
|
||||
if (recurrenceType !== recurrenceOptions.doesNotRepeat.value) {
|
||||
startTime =
|
||||
(formData?.recurrence?.startTime
|
||||
? dayjs(formData.recurrence.startTime).toISOString()
|
||||
: '') ||
|
||||
formData?.startTime ||
|
||||
'';
|
||||
startTime = formData?.recurrence?.startTime || formData?.startTime || '';
|
||||
}
|
||||
|
||||
if (!startTime) {
|
||||
@@ -393,10 +381,7 @@ export function PlannedDowntimeForm(
|
||||
const endTimeText = useMemo((): string => {
|
||||
let endTime = formData?.endTime;
|
||||
if (recurrenceType !== recurrenceOptions.doesNotRepeat.value) {
|
||||
endTime =
|
||||
(formData?.recurrence?.endTime
|
||||
? dayjs(formData.recurrence.endTime).toISOString()
|
||||
: '') || '';
|
||||
endTime = formData?.recurrence?.endTime || '';
|
||||
|
||||
if (!isEditMode && !endTime) {
|
||||
endTime = formData?.endTime || '';
|
||||
|
||||
@@ -12,15 +12,13 @@ import {
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import type { DefaultOptionType } from 'antd/es/select';
|
||||
import type {
|
||||
ListDowntimeSchedules200,
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesPlannedMaintenanceDTO,
|
||||
RuletypesRecurrenceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { ErrorType } from 'api/generatedAPIInstance';
|
||||
import {
|
||||
DowntimeSchedules,
|
||||
PayloadProps,
|
||||
Recurrence,
|
||||
} from 'api/plannedDowntime/getAllDowntimeSchedules';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import cx from 'classnames';
|
||||
import dayjs from 'dayjs';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { defaultTo } from 'lodash-es';
|
||||
import { CalendarClock, PenLine, Trash2 } from 'lucide-react';
|
||||
@@ -145,7 +143,7 @@ export function CollapseListContent({
|
||||
created_by_name?: string;
|
||||
created_by_email?: string;
|
||||
timeframe: [string | undefined | null, string | undefined | null];
|
||||
repeats?: RuletypesRecurrenceDTO | null;
|
||||
repeats?: Recurrence | null;
|
||||
updated_at?: string;
|
||||
updated_by_name?: string;
|
||||
alertOptions?: DefaultOptionType[];
|
||||
@@ -220,10 +218,10 @@ export function CollapseListContent({
|
||||
export function CustomCollapseList(
|
||||
props: DowntimeSchedulesTableData & {
|
||||
setInitialValues: React.Dispatch<
|
||||
React.SetStateAction<Partial<RuletypesPlannedMaintenanceDTO>>
|
||||
React.SetStateAction<Partial<DowntimeSchedules>>
|
||||
>;
|
||||
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleDeleteDowntime: (id: string, name: string) => void;
|
||||
handleDeleteDowntime: (id: number, name: string) => void;
|
||||
setEditMode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
},
|
||||
): JSX.Element {
|
||||
@@ -243,19 +241,12 @@ export function CustomCollapseList(
|
||||
kind,
|
||||
} = props;
|
||||
|
||||
const scheduleTime = schedule?.startTime
|
||||
? dayjs(schedule.startTime).toISOString()
|
||||
: createdAt
|
||||
? dayjs(createdAt).toISOString()
|
||||
: '';
|
||||
const scheduleTime = schedule?.startTime ? schedule.startTime : createdAt;
|
||||
// Combine time and date
|
||||
const formattedDateAndTime = `Start time ⎯ ${formatDateTime(
|
||||
defaultTo(scheduleTime, ''),
|
||||
)} ${schedule?.timezone}`;
|
||||
const endTime = getEndTime({
|
||||
kind,
|
||||
schedule,
|
||||
} as Partial<RuletypesPlannedMaintenanceDTO>);
|
||||
const endTime = getEndTime({ kind, schedule });
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -266,10 +257,7 @@ export function CustomCollapseList(
|
||||
duration={
|
||||
schedule?.recurrence?.duration
|
||||
? (schedule?.recurrence?.duration as string)
|
||||
: getDuration(
|
||||
schedule?.startTime ? dayjs(schedule.startTime).toISOString() : '',
|
||||
schedule?.endTime ? dayjs(schedule.endTime).toISOString() : '',
|
||||
)
|
||||
: getDuration(schedule?.startTime, schedule?.endTime)
|
||||
}
|
||||
name={defaultTo(name, '')}
|
||||
handleEdit={(): void => {
|
||||
@@ -278,23 +266,21 @@ export function CustomCollapseList(
|
||||
setEditMode(true);
|
||||
}}
|
||||
handleDelete={(): void => {
|
||||
handleDeleteDowntime(id ?? '', name || '');
|
||||
handleDeleteDowntime(id, name || '');
|
||||
}}
|
||||
/>
|
||||
}
|
||||
key={id ?? ''}
|
||||
key={id}
|
||||
>
|
||||
<CollapseListContent
|
||||
created_at={createdAt ? dayjs(createdAt).toISOString() : ''}
|
||||
created_at={defaultTo(createdAt, '')}
|
||||
created_by_name={defaultTo(createdBy, '')}
|
||||
timeframe={[
|
||||
schedule?.startTime?.toString(),
|
||||
typeof endTime === 'string' ? endTime : endTime?.toString(),
|
||||
]}
|
||||
repeats={
|
||||
schedule?.recurrence as RuletypesRecurrenceDTO | null | undefined
|
||||
}
|
||||
updated_at={updatedAt ? dayjs(updatedAt).toISOString() : ''}
|
||||
repeats={schedule?.recurrence}
|
||||
updated_at={defaultTo(updatedAt, '')}
|
||||
updated_by_name={defaultTo(updatedBy, '')}
|
||||
alertOptions={alertOptions}
|
||||
timezone={defaultTo(schedule?.timezone, '')}
|
||||
@@ -309,7 +295,7 @@ export function CustomCollapseList(
|
||||
);
|
||||
}
|
||||
|
||||
export type DowntimeSchedulesTableData = RuletypesPlannedMaintenanceDTO & {
|
||||
export type DowntimeSchedulesTableData = DowntimeSchedules & {
|
||||
alertOptions: DefaultOptionType[];
|
||||
};
|
||||
|
||||
@@ -323,15 +309,15 @@ export function PlannedDowntimeList({
|
||||
searchValue,
|
||||
}: {
|
||||
downtimeSchedules: UseQueryResult<
|
||||
ListDowntimeSchedules200,
|
||||
ErrorType<RenderErrorResponseDTO>
|
||||
AxiosResponse<PayloadProps, any>,
|
||||
AxiosError<unknown, any>
|
||||
>;
|
||||
alertOptions: DefaultOptionType[];
|
||||
setInitialValues: React.Dispatch<
|
||||
React.SetStateAction<Partial<RuletypesPlannedMaintenanceDTO>>
|
||||
React.SetStateAction<Partial<DowntimeSchedules>>
|
||||
>;
|
||||
setModalOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
handleDeleteDowntime: (id: string, name: string) => void;
|
||||
handleDeleteDowntime: (id: number, name: string) => void;
|
||||
setEditMode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
searchValue: string | number;
|
||||
}): JSX.Element {
|
||||
@@ -351,19 +337,19 @@ export function PlannedDowntimeList({
|
||||
];
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const tableData = [...(downtimeSchedules.data?.data || [])]
|
||||
const tableData = (downtimeSchedules.data?.data?.data || [])
|
||||
.sort((a, b): number => {
|
||||
if (a?.updatedAt && b?.updatedAt) {
|
||||
return dayjs(b.updatedAt).diff(dayjs(a.updatedAt));
|
||||
return b.updatedAt.localeCompare(a.updatedAt);
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.filter(
|
||||
?.filter(
|
||||
(data) =>
|
||||
data.name.includes(searchValue.toLocaleString()) ||
|
||||
data.id === searchValue.toLocaleString(),
|
||||
data?.name?.includes(searchValue.toLocaleString()) ||
|
||||
data?.id.toLocaleString() === searchValue.toLocaleString(),
|
||||
)
|
||||
.map((data) => {
|
||||
.map?.((data) => {
|
||||
const specificAlertOptions = getAlertOptionsFromIds(
|
||||
data.alertIds || [],
|
||||
alertOptions,
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { UseMutateAsyncFunction } from 'react-query';
|
||||
import type { NotificationInstance } from 'antd/es/notification/interface';
|
||||
import type { DefaultOptionType } from 'antd/es/select';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import type {
|
||||
DeleteDowntimeScheduleByIDPathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesPlannedMaintenanceDTO,
|
||||
RuletypesRecurrenceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { ErrorType } from 'api/generatedAPIInstance';
|
||||
import { AxiosError } from 'axios';
|
||||
import createDowntimeSchedule from 'api/plannedDowntime/createDowntimeSchedule';
|
||||
import { DeleteSchedulePayloadProps } from 'api/plannedDowntime/deleteDowntimeSchedule';
|
||||
import {
|
||||
DowntimeSchedules,
|
||||
Recurrence,
|
||||
} from 'api/plannedDowntime/getAllDowntimeSchedules';
|
||||
import updateDowntimeSchedule, {
|
||||
DowntimeScheduleUpdatePayload,
|
||||
PayloadProps,
|
||||
} from 'api/plannedDowntime/updateDowntimeSchedule';
|
||||
import { showErrorNotification } from 'components/ExplorerCard/utils';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import dayjs from 'dayjs';
|
||||
import { isEmpty, isEqual } from 'lodash-es';
|
||||
import APIError from 'types/api/error';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
type DateTimeString = string | null | undefined;
|
||||
|
||||
@@ -59,21 +61,15 @@ export const getAlertOptionsFromIds = (
|
||||
alertIds?.includes(alert.value as string),
|
||||
);
|
||||
|
||||
export const recurrenceInfo = (
|
||||
recurrence?: RuletypesRecurrenceDTO | null,
|
||||
): string => {
|
||||
export const recurrenceInfo = (recurrence?: Recurrence | null): string => {
|
||||
if (!recurrence) {
|
||||
return 'No';
|
||||
}
|
||||
|
||||
const { startTime, duration, repeatOn, repeatType, endTime } = recurrence;
|
||||
|
||||
const formattedStartTime = startTime
|
||||
? formatDateTime(dayjs(startTime).toISOString())
|
||||
: '';
|
||||
const formattedEndTime = endTime
|
||||
? `to ${formatDateTime(dayjs(endTime).toISOString())}`
|
||||
: '';
|
||||
const formattedStartTime = startTime ? formatDateTime(startTime) : '';
|
||||
const formattedEndTime = endTime ? `to ${formatDateTime(endTime)}` : '';
|
||||
const weeklyRepeatString = repeatOn ? `on ${repeatOn.join(', ')}` : '';
|
||||
const durationString = duration ? `- Duration: ${duration}` : '';
|
||||
|
||||
@@ -81,32 +77,31 @@ export const recurrenceInfo = (
|
||||
};
|
||||
|
||||
export const defautlInitialValues: Partial<
|
||||
RuletypesPlannedMaintenanceDTO & { editMode: boolean }
|
||||
DowntimeSchedules & { editMode: boolean }
|
||||
> = {
|
||||
name: '',
|
||||
description: '',
|
||||
schedule: {
|
||||
timezone: '',
|
||||
endTime: undefined,
|
||||
recurrence: undefined,
|
||||
startTime: undefined,
|
||||
endTime: '',
|
||||
recurrence: null,
|
||||
startTime: '',
|
||||
},
|
||||
alertIds: [],
|
||||
createdAt: undefined,
|
||||
createdBy: undefined,
|
||||
createdAt: '',
|
||||
createdBy: '',
|
||||
editMode: false,
|
||||
};
|
||||
|
||||
type DeleteDowntimeScheduleProps = {
|
||||
deleteDowntimeScheduleAsync: UseMutateAsyncFunction<
|
||||
void,
|
||||
ErrorType<RenderErrorResponseDTO>,
|
||||
{ pathParams: DeleteDowntimeScheduleByIDPathParameters }
|
||||
DeleteSchedulePayloadProps,
|
||||
Error,
|
||||
number
|
||||
>;
|
||||
notifications: NotificationInstance;
|
||||
showErrorModal: (error: APIError) => void;
|
||||
refetchAllSchedules: VoidFunction;
|
||||
deleteId?: string;
|
||||
deleteId?: number;
|
||||
hideDeleteDowntimeScheduleModal: () => void;
|
||||
clearSearch: () => void;
|
||||
};
|
||||
@@ -118,33 +113,40 @@ export const deleteDowntimeHandler = ({
|
||||
hideDeleteDowntimeScheduleModal,
|
||||
clearSearch,
|
||||
notifications,
|
||||
showErrorModal,
|
||||
}: DeleteDowntimeScheduleProps): void => {
|
||||
if (!deleteId) {
|
||||
const errorMsg = new Error('Something went wrong');
|
||||
console.error('Unable to delete, please provide correct deleteId');
|
||||
notifications.error({ message: 'Something went wrong' });
|
||||
showErrorNotification(notifications, errorMsg);
|
||||
} else {
|
||||
deleteDowntimeScheduleAsync(
|
||||
{ pathParams: { id: String(deleteId) } },
|
||||
{
|
||||
onSuccess: () => {
|
||||
hideDeleteDowntimeScheduleModal();
|
||||
clearSearch();
|
||||
notifications.success({
|
||||
message: 'Downtime schedule Deleted Successfully',
|
||||
});
|
||||
refetchAllSchedules();
|
||||
},
|
||||
onError: (err) => {
|
||||
showErrorModal(
|
||||
convertToApiError(err as AxiosError<RenderErrorResponseDTO>) as APIError,
|
||||
);
|
||||
},
|
||||
deleteDowntimeScheduleAsync(deleteId, {
|
||||
onSuccess: () => {
|
||||
hideDeleteDowntimeScheduleModal();
|
||||
clearSearch();
|
||||
notifications.success({
|
||||
message: 'Downtime schedule Deleted Successfully',
|
||||
});
|
||||
refetchAllSchedules();
|
||||
},
|
||||
);
|
||||
onError: (err) => {
|
||||
showErrorNotification(notifications, err);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const createEditDowntimeSchedule = async (
|
||||
props: DowntimeScheduleUpdatePayload,
|
||||
): Promise<
|
||||
| SuccessResponse<PayloadProps>
|
||||
| ErrorResponse<{ code: string; message: string } | string>
|
||||
> => {
|
||||
if (props.id) {
|
||||
return updateDowntimeSchedule({ ...props });
|
||||
}
|
||||
return createDowntimeSchedule({ ...props.data });
|
||||
};
|
||||
|
||||
export const recurrenceOptions = {
|
||||
doesNotRepeat: {
|
||||
label: 'Does not repeat',
|
||||
@@ -228,21 +230,19 @@ export const getEndTime = ({
|
||||
kind,
|
||||
schedule,
|
||||
}: Partial<
|
||||
RuletypesPlannedMaintenanceDTO & {
|
||||
DowntimeSchedules & {
|
||||
editMode: boolean;
|
||||
}
|
||||
>): string | dayjs.Dayjs => {
|
||||
if (kind === 'fixed') {
|
||||
return schedule?.endTime ? dayjs(schedule.endTime).toISOString() : '';
|
||||
return schedule?.endTime || '';
|
||||
}
|
||||
|
||||
return schedule?.recurrence?.endTime
|
||||
? dayjs(schedule.recurrence.endTime).toISOString()
|
||||
: '';
|
||||
return schedule?.recurrence?.endTime || '';
|
||||
};
|
||||
|
||||
export const isScheduleRecurring = (
|
||||
schedule?: RuletypesPlannedMaintenanceDTO['schedule'] | null,
|
||||
schedule?: DowntimeSchedules['schedule'],
|
||||
): boolean => (schedule ? !isEmpty(schedule?.recurrence) : false);
|
||||
|
||||
function convertUtcOffsetToTimezoneOffset(offsetMinutes: number): string {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import type {
|
||||
ListDowntimeSchedules200,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { ErrorType } from 'api/generatedAPIInstance';
|
||||
import { PayloadProps } from 'api/plannedDowntime/getAllDowntimeSchedules';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import {
|
||||
mockLocation,
|
||||
mockQueryParams,
|
||||
@@ -25,53 +22,45 @@ const MOCK_DATE_2 = '2024-01-02';
|
||||
const MOCK_DATE_3 = '2024-01-03';
|
||||
|
||||
const MOCK_DOWNTIME_1 = createMockDowntime({
|
||||
id: '1',
|
||||
id: 1,
|
||||
name: MOCK_DOWNTIME_1_NAME,
|
||||
createdAt: new Date(MOCK_DATE_1),
|
||||
updatedAt: new Date(MOCK_DATE_1),
|
||||
schedule: buildSchedule({
|
||||
startTime: new Date(MOCK_DATE_1),
|
||||
timezone: 'UTC',
|
||||
}),
|
||||
createdAt: MOCK_DATE_1,
|
||||
updatedAt: MOCK_DATE_1,
|
||||
schedule: buildSchedule({ startTime: MOCK_DATE_1, timezone: 'UTC' }),
|
||||
alertIds: [],
|
||||
});
|
||||
|
||||
const MOCK_DOWNTIME_2 = createMockDowntime({
|
||||
id: '2',
|
||||
id: 2,
|
||||
name: MOCK_DOWNTIME_2_NAME,
|
||||
createdAt: new Date(MOCK_DATE_2),
|
||||
updatedAt: new Date(MOCK_DATE_2),
|
||||
schedule: buildSchedule({
|
||||
startTime: new Date(MOCK_DATE_2),
|
||||
timezone: 'UTC',
|
||||
}),
|
||||
createdAt: MOCK_DATE_2,
|
||||
updatedAt: MOCK_DATE_2,
|
||||
schedule: buildSchedule({ startTime: MOCK_DATE_2, timezone: 'UTC' }),
|
||||
alertIds: [],
|
||||
});
|
||||
|
||||
const MOCK_DOWNTIME_3 = createMockDowntime({
|
||||
id: '3',
|
||||
id: 3,
|
||||
name: MOCK_DOWNTIME_3_NAME,
|
||||
createdAt: new Date(MOCK_DATE_3),
|
||||
updatedAt: new Date(MOCK_DATE_3),
|
||||
schedule: buildSchedule({
|
||||
startTime: new Date(MOCK_DATE_3),
|
||||
timezone: 'UTC',
|
||||
}),
|
||||
createdAt: MOCK_DATE_3,
|
||||
updatedAt: MOCK_DATE_3,
|
||||
schedule: buildSchedule({ startTime: MOCK_DATE_3, timezone: 'UTC' }),
|
||||
alertIds: [],
|
||||
});
|
||||
|
||||
const MOCK_DOWNTIME_RESPONSE: ListDowntimeSchedules200 = {
|
||||
data: [MOCK_DOWNTIME_1, MOCK_DOWNTIME_2, MOCK_DOWNTIME_3],
|
||||
status: 'success',
|
||||
const MOCK_DOWNTIME_RESPONSE: Partial<AxiosResponse<PayloadProps>> = {
|
||||
data: {
|
||||
data: [MOCK_DOWNTIME_1, MOCK_DOWNTIME_2, MOCK_DOWNTIME_3],
|
||||
},
|
||||
};
|
||||
|
||||
type DowntimeQueryResult = UseQueryResult<
|
||||
ListDowntimeSchedules200,
|
||||
ErrorType<RenderErrorResponseDTO>
|
||||
AxiosResponse<PayloadProps>,
|
||||
AxiosError
|
||||
>;
|
||||
|
||||
const mockDowntimeQueryResult: Partial<DowntimeQueryResult> = {
|
||||
data: MOCK_DOWNTIME_RESPONSE,
|
||||
data: MOCK_DOWNTIME_RESPONSE as AxiosResponse<PayloadProps>,
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
isError: false,
|
||||
@@ -100,27 +89,13 @@ jest.mock('hooks/useSafeNavigate', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('api/generated/services/downtimeschedules', () => ({
|
||||
useListDowntimeSchedules: (): DowntimeQueryResult =>
|
||||
jest.mock('api/plannedDowntime/getAllDowntimeSchedules', () => ({
|
||||
useGetAllDowntimeSchedules: (): DowntimeQueryResult =>
|
||||
mockDowntimeQueryResult as DowntimeQueryResult,
|
||||
useDeleteDowntimeScheduleByID: (): {
|
||||
mutateAsync: jest.Mock;
|
||||
isLoading: false;
|
||||
} => ({
|
||||
mutateAsync: jest.fn(),
|
||||
isLoading: false,
|
||||
}),
|
||||
}));
|
||||
jest.mock('api/generated/services/rules', () => ({
|
||||
useListRules: (): {
|
||||
data: { data: [] };
|
||||
isError: false;
|
||||
isLoading: false;
|
||||
} => ({
|
||||
data: { data: [] },
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
}),
|
||||
jest.mock('api/alerts/getAll', () => ({
|
||||
__esModule: true,
|
||||
default: (): Promise<{ payload: [] }> => Promise.resolve({ payload: [] }),
|
||||
}));
|
||||
|
||||
describe('PlannedDowntime Component', () => {
|
||||
|
||||
@@ -1,37 +1,29 @@
|
||||
import type {
|
||||
RuletypesPlannedMaintenanceDTO,
|
||||
RuletypesScheduleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
RuletypesMaintenanceKindDTO,
|
||||
RuletypesMaintenanceStatusDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { DowntimeSchedules } from 'api/plannedDowntime/getAllDowntimeSchedules';
|
||||
|
||||
export const buildSchedule = (
|
||||
schedule: Partial<RuletypesScheduleDTO>,
|
||||
): RuletypesScheduleDTO => ({
|
||||
timezone: schedule?.timezone ?? '',
|
||||
startTime: schedule?.startTime,
|
||||
endTime: schedule?.endTime,
|
||||
recurrence: schedule?.recurrence,
|
||||
schedule: Partial<DowntimeSchedules['schedule']>,
|
||||
): DowntimeSchedules['schedule'] => ({
|
||||
timezone: schedule?.timezone ?? null,
|
||||
startTime: schedule?.startTime ?? null,
|
||||
endTime: schedule?.endTime ?? null,
|
||||
recurrence: schedule?.recurrence ?? null,
|
||||
});
|
||||
|
||||
export const createMockDowntime = (
|
||||
overrides: Partial<RuletypesPlannedMaintenanceDTO>,
|
||||
): RuletypesPlannedMaintenanceDTO => ({
|
||||
id: overrides.id ?? '0',
|
||||
name: overrides.name ?? '',
|
||||
description: overrides.description ?? '',
|
||||
overrides: Partial<DowntimeSchedules>,
|
||||
): DowntimeSchedules => ({
|
||||
id: overrides.id ?? 0,
|
||||
name: overrides.name ?? null,
|
||||
description: overrides.description ?? null,
|
||||
schedule: buildSchedule({
|
||||
timezone: 'UTC',
|
||||
startTime: new Date('2024-01-01'),
|
||||
startTime: '2024-01-01',
|
||||
...overrides.schedule,
|
||||
}),
|
||||
alertIds: overrides.alertIds ?? [],
|
||||
createdAt: overrides.createdAt,
|
||||
createdBy: overrides.createdBy ?? '',
|
||||
updatedAt: overrides.updatedAt,
|
||||
updatedBy: overrides.updatedBy ?? '',
|
||||
kind: overrides.kind ?? RuletypesMaintenanceKindDTO.recurring,
|
||||
status: overrides.status ?? RuletypesMaintenanceStatusDTO.active,
|
||||
alertIds: overrides.alertIds ?? null,
|
||||
createdAt: overrides.createdAt ?? null,
|
||||
createdBy: overrides.createdBy ?? null,
|
||||
updatedAt: overrides.updatedAt ?? null,
|
||||
updatedBy: overrides.updatedBy ?? null,
|
||||
kind: overrides.kind ?? null,
|
||||
});
|
||||
|
||||
20
frontend/src/hooks/alerts/useCreateAlertRule.ts
Normal file
20
frontend/src/hooks/alerts/useCreateAlertRule.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
import createAlertRule, {
|
||||
CreateAlertRuleResponse,
|
||||
} from 'api/alerts/createAlertRule';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
|
||||
export function useCreateAlertRule(): UseMutationResult<
|
||||
SuccessResponse<CreateAlertRuleResponse> | ErrorResponse,
|
||||
Error,
|
||||
PostableAlertRuleV2
|
||||
> {
|
||||
return useMutation<
|
||||
SuccessResponse<CreateAlertRuleResponse> | ErrorResponse,
|
||||
Error,
|
||||
PostableAlertRuleV2
|
||||
>({
|
||||
mutationFn: (alertData) => createAlertRule(alertData),
|
||||
});
|
||||
}
|
||||
18
frontend/src/hooks/alerts/useTestAlertRule.ts
Normal file
18
frontend/src/hooks/alerts/useTestAlertRule.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
import testAlertRule, { TestAlertRuleResponse } from 'api/alerts/testAlertRule';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
|
||||
export function useTestAlertRule(): UseMutationResult<
|
||||
SuccessResponse<TestAlertRuleResponse> | ErrorResponse,
|
||||
Error,
|
||||
PostableAlertRuleV2
|
||||
> {
|
||||
return useMutation<
|
||||
SuccessResponse<TestAlertRuleResponse> | ErrorResponse,
|
||||
Error,
|
||||
PostableAlertRuleV2
|
||||
>({
|
||||
mutationFn: (alertData) => testAlertRule(alertData),
|
||||
});
|
||||
}
|
||||
22
frontend/src/hooks/alerts/useUpdateAlertRule.ts
Normal file
22
frontend/src/hooks/alerts/useUpdateAlertRule.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
import updateAlertRule, {
|
||||
UpdateAlertRuleResponse,
|
||||
} from 'api/alerts/updateAlertRule';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
|
||||
export function useUpdateAlertRule(
|
||||
id: string,
|
||||
): UseMutationResult<
|
||||
SuccessResponse<UpdateAlertRuleResponse> | ErrorResponse,
|
||||
Error,
|
||||
PostableAlertRuleV2
|
||||
> {
|
||||
return useMutation<
|
||||
SuccessResponse<UpdateAlertRuleResponse> | ErrorResponse,
|
||||
Error,
|
||||
PostableAlertRuleV2
|
||||
>({
|
||||
mutationFn: (alertData) => updateAlertRule(id, alertData),
|
||||
});
|
||||
}
|
||||
@@ -62,10 +62,10 @@ export interface TooltipRenderArgs {
|
||||
|
||||
export interface BaseTooltipProps {
|
||||
showTooltipHeader?: boolean;
|
||||
timezone?: Timezone;
|
||||
yAxisUnit?: string;
|
||||
decimalPrecision?: PrecisionOption;
|
||||
content?: TooltipContentItem[];
|
||||
timezone?: Timezone;
|
||||
}
|
||||
|
||||
export interface TimeSeriesTooltipProps
|
||||
|
||||
@@ -4,6 +4,7 @@ import cx from 'classnames';
|
||||
import { getFocusedSeriesAtPosition } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { syncCursorRegistry } from './syncCursorRegistry';
|
||||
import {
|
||||
createInitialControllerState,
|
||||
createSetCursorHandler,
|
||||
@@ -40,6 +41,7 @@ export default function TooltipPlugin({
|
||||
maxHeight = 600,
|
||||
syncMode = DashboardCursorSync.None,
|
||||
syncKey = '_tooltip_sync_global_',
|
||||
syncMetadata,
|
||||
pinnedTooltipElement,
|
||||
canPinTooltip = false,
|
||||
}: TooltipPluginProps): JSX.Element | null {
|
||||
@@ -100,7 +102,29 @@ export default function TooltipPlugin({
|
||||
// crosshair / tooltip can follow the dashboard-wide cursor.
|
||||
if (syncMode !== DashboardCursorSync.None && config.scales[0]?.props.time) {
|
||||
config.setCursor({
|
||||
sync: { key: syncKey, scales: ['x', null] },
|
||||
sync: { key: syncKey, scales: ['x', 'y'] },
|
||||
});
|
||||
|
||||
// Show the horizontal crosshair only when the receiving panel shares
|
||||
// the same y-axis unit as the source panel. When this panel is the
|
||||
// source (cursor.event != null) the line is always shown and this
|
||||
// panel's metadata is written to the registry so receivers can read it.
|
||||
config.addHook('setCursor', (u: uPlot): void => {
|
||||
const yCursorEl = u.root.querySelector<HTMLElement>('.u-cursor-y');
|
||||
if (!yCursorEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (u.cursor.event != null) {
|
||||
// This panel is the source — publish metadata and always show line.
|
||||
syncCursorRegistry.setMetadata(syncKey, syncMetadata);
|
||||
yCursorEl.style.display = '';
|
||||
} else {
|
||||
// This panel is receiving sync — show only if units match.
|
||||
const sourceMeta = syncCursorRegistry.getMetadata(syncKey);
|
||||
yCursorEl.style.display =
|
||||
sourceMeta?.yAxisUnit === syncMetadata?.yAxisUnit ? '' : 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { TooltipSyncMetadata } from './types';
|
||||
|
||||
/**
|
||||
* Module-level registry that tracks the metadata of the panel currently
|
||||
* acting as the cursor source (the one being hovered) per sync group.
|
||||
*
|
||||
* uPlot fires the source panel's setCursor hook before broadcasting to
|
||||
* receivers, so the registry is always populated before receivers read it.
|
||||
*
|
||||
* Receivers use this to make decisions such as:
|
||||
* - Whether to show the horizontal crosshair line (matching yAxisUnit)
|
||||
* - Future: what to render inside the tooltip (matching groupBy, etc.)
|
||||
*/
|
||||
const metadataBySyncKey = new Map<string, TooltipSyncMetadata | undefined>();
|
||||
|
||||
export const syncCursorRegistry = {
|
||||
setMetadata(syncKey: string, metadata: TooltipSyncMetadata | undefined): void {
|
||||
metadataBySyncKey.set(syncKey, metadata);
|
||||
},
|
||||
|
||||
getMetadata(syncKey: string): TooltipSyncMetadata | undefined {
|
||||
return metadataBySyncKey.get(syncKey);
|
||||
},
|
||||
};
|
||||
@@ -34,11 +34,16 @@ export interface TooltipLayoutInfo {
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface TooltipSyncMetadata {
|
||||
yAxisUnit?: string;
|
||||
}
|
||||
|
||||
export interface TooltipPluginProps {
|
||||
config: UPlotConfigBuilder;
|
||||
canPinTooltip?: boolean;
|
||||
syncMode?: DashboardCursorSync;
|
||||
syncKey?: string;
|
||||
syncMetadata?: TooltipSyncMetadata;
|
||||
render: (args: TooltipRenderArgs) => ReactNode;
|
||||
pinnedTooltipElement?: (clickData: TooltipClickData) => ReactNode;
|
||||
maxWidth?: number;
|
||||
|
||||
@@ -516,7 +516,7 @@ describe('TooltipPlugin', () => {
|
||||
);
|
||||
|
||||
expect(setCursorSpy).toHaveBeenCalledWith({
|
||||
sync: { key: 'dashboard-sync', scales: ['x', null] },
|
||||
sync: { key: 'dashboard-sync', scales: ['x', 'y'] },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Breadcrumb, Button, Divider } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -14,16 +15,39 @@ import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { NEW_ALERT_SCHEMA_VERSION } from 'types/api/alerts/alertTypesV2';
|
||||
import { fromRuleDTOToPostableRuleV2 } from 'types/api/alerts/convert';
|
||||
import {
|
||||
NEW_ALERT_SCHEMA_VERSION,
|
||||
PostableAlertRuleV2,
|
||||
} from 'types/api/alerts/alertTypesV2';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import AlertHeader from './AlertHeader/AlertHeader';
|
||||
import AlertNotFound from './AlertNotFound';
|
||||
import { useGetAlertRuleDetails, useRouteTabUtils } from './hooks';
|
||||
import { AlertDetailsStatusRendererProps } from './types';
|
||||
|
||||
import './AlertDetails.styles.scss';
|
||||
|
||||
function AlertDetailsStatusRenderer({
|
||||
isLoading,
|
||||
isError,
|
||||
isRefetching,
|
||||
data,
|
||||
}: AlertDetailsStatusRendererProps): JSX.Element {
|
||||
const alertRuleDetails = useMemo(() => data?.payload?.data, [data]);
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
if (isLoading || isRefetching) {
|
||||
return <Spinner tip="Loading..." />;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <div>{data?.error || t('something_went_wrong')}</div>;
|
||||
}
|
||||
|
||||
return <AlertHeader alertDetails={alertRuleDetails} />;
|
||||
}
|
||||
|
||||
function BreadCrumbItem({
|
||||
title,
|
||||
isLast,
|
||||
@@ -63,6 +87,7 @@ function AlertDetails(): JSX.Element {
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isError,
|
||||
ruleId,
|
||||
isValidRuleId,
|
||||
@@ -74,7 +99,7 @@ function AlertDetails(): JSX.Element {
|
||||
}, [params]);
|
||||
|
||||
const getDocumentTitle = useMemo(() => {
|
||||
const alertTitle = alertDetailsResponse?.data?.alert;
|
||||
const alertTitle = alertDetailsResponse?.payload?.data?.alert;
|
||||
if (alertTitle) {
|
||||
return alertTitle;
|
||||
}
|
||||
@@ -85,17 +110,14 @@ function AlertDetails(): JSX.Element {
|
||||
return document.title;
|
||||
}
|
||||
return 'Alert Not Found';
|
||||
}, [alertDetailsResponse?.data?.alert, isTestAlert, isLoading]);
|
||||
}, [alertDetailsResponse?.payload?.data?.alert, isTestAlert, isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = getDocumentTitle;
|
||||
}, [getDocumentTitle]);
|
||||
|
||||
const alertRuleDetails = useMemo(
|
||||
() =>
|
||||
alertDetailsResponse?.data
|
||||
? fromRuleDTOToPostableRuleV2(alertDetailsResponse.data)
|
||||
: undefined,
|
||||
() => alertDetailsResponse?.payload?.data as PostableAlertRuleV2 | undefined,
|
||||
[alertDetailsResponse],
|
||||
);
|
||||
|
||||
@@ -104,7 +126,12 @@ function AlertDetails(): JSX.Element {
|
||||
[alertRuleDetails],
|
||||
);
|
||||
|
||||
if (isError || !isValidRuleId || (!isLoading && !alertRuleDetails)) {
|
||||
if (
|
||||
isError ||
|
||||
!isValidRuleId ||
|
||||
(alertDetailsResponse && alertDetailsResponse.statusCode !== 200) ||
|
||||
(!isLoading && !alertRuleDetails)
|
||||
) {
|
||||
return <AlertNotFound isTestAlert={isTestAlert} />;
|
||||
}
|
||||
|
||||
@@ -146,7 +173,9 @@ function AlertDetails(): JSX.Element {
|
||||
/>
|
||||
<Divider className="divider breadcrumb-divider" />
|
||||
|
||||
{alertRuleDetails && <AlertHeader alertDetails={alertRuleDetails} />}
|
||||
<AlertDetailsStatusRenderer
|
||||
{...{ isLoading, isError, isRefetching, data: alertDetailsResponse }}
|
||||
/>
|
||||
<Divider className="divider" />
|
||||
<div className="tabs-and-filters">
|
||||
<RouteTab
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { RuletypesRuleDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import CreateAlertV2Header from 'container/CreateAlertV2/CreateAlertHeader';
|
||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||
import { useAlertRule } from 'providers/Alert';
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
NEW_ALERT_SCHEMA_VERSION,
|
||||
PostableAlertRuleV2,
|
||||
} from 'types/api/alerts/alertTypesV2';
|
||||
import { GettableAlert } from 'types/api/alerts/get';
|
||||
|
||||
import AlertActionButtons from './ActionButtons/ActionButtons';
|
||||
import AlertLabels from './AlertLabels/AlertLabels';
|
||||
@@ -16,7 +16,7 @@ import AlertState from './AlertState/AlertState';
|
||||
import './AlertHeader.styles.scss';
|
||||
|
||||
export type AlertHeaderProps = {
|
||||
alertDetails: RuletypesRuleDTO | PostableAlertRuleV2;
|
||||
alertDetails: GettableAlert | PostableAlertRuleV2;
|
||||
};
|
||||
function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
||||
const { state, alert: alertName, labels } = alertDetails;
|
||||
|
||||
@@ -3,25 +3,15 @@ import { useMutation, useQuery, useQueryClient } from 'react-query';
|
||||
import { generatePath, useLocation } from 'react-router-dom';
|
||||
import { TablePaginationConfig, TableProps } from 'antd';
|
||||
import type { FilterValue, SorterResult } from 'antd/es/table/interface';
|
||||
import { patchRulePartial } from 'api/alerts/patchRulePartial';
|
||||
import deleteAlerts from 'api/alerts/delete';
|
||||
import get from 'api/alerts/get';
|
||||
import getAll from 'api/alerts/getAll';
|
||||
import patchAlert from 'api/alerts/patch';
|
||||
import ruleStats from 'api/alerts/ruleStats';
|
||||
import save from 'api/alerts/save';
|
||||
import timelineGraph from 'api/alerts/timelineGraph';
|
||||
import timelineTable from 'api/alerts/timelineTable';
|
||||
import topContributors from 'api/alerts/topContributors';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
createRule,
|
||||
deleteRuleByID,
|
||||
updateRuleByID,
|
||||
useGetRuleByID,
|
||||
useListRules,
|
||||
} from 'api/generated/services/rules';
|
||||
import type {
|
||||
GetRuleByID200,
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesPostableRuleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { TabRoutes } from 'components/RouteTab/types';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
@@ -31,6 +21,7 @@ import { TIMELINE_TABLE_PAGE_SIZE } from 'container/AlertHistory/constants';
|
||||
import { AlertDetailsTab, TimelineFilter } from 'container/AlertHistory/types';
|
||||
import { urlKey } from 'container/AllError/utils';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
@@ -43,9 +34,7 @@ import { OrderPreferenceItems } from 'pages/Logs/config';
|
||||
import BetaTag from 'periscope/components/BetaTag/BetaTag';
|
||||
import PaginationInfoText from 'periscope/components/PaginationInfoText/PaginationInfoText';
|
||||
import { useAlertRule } from 'providers/Alert';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { toPostableRuleDTOFromAlertDef } from 'types/api/alerts/convert';
|
||||
import {
|
||||
AlertDef,
|
||||
AlertRuleStatsPayload,
|
||||
@@ -54,7 +43,7 @@ import {
|
||||
AlertRuleTimelineTableResponsePayload,
|
||||
AlertRuleTopContributorsPayload,
|
||||
} from 'types/api/alerts/def';
|
||||
import APIError from 'types/api/error';
|
||||
import { PayloadProps } from 'types/api/alerts/get';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { nanoToMilli } from 'utils/timeUtils';
|
||||
|
||||
@@ -153,7 +142,10 @@ export const useRouteTabUtils = (): { routes: TabRoutes[] } => {
|
||||
type Props = {
|
||||
ruleId: string | null;
|
||||
isValidRuleId: boolean;
|
||||
alertDetailsResponse: GetRuleByID200 | undefined;
|
||||
alertDetailsResponse:
|
||||
| SuccessResponse<PayloadProps, unknown>
|
||||
| ErrorResponse
|
||||
| undefined;
|
||||
isLoading: boolean;
|
||||
isRefetching: boolean;
|
||||
isError: boolean;
|
||||
@@ -169,15 +161,14 @@ export const useGetAlertRuleDetails = (): Props => {
|
||||
data: alertDetailsResponse,
|
||||
isRefetching,
|
||||
isError,
|
||||
} = useGetRuleByID(
|
||||
{ id: ruleId || '' },
|
||||
{
|
||||
query: {
|
||||
enabled: isValidRuleId,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
} = useQuery([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId], {
|
||||
queryFn: () =>
|
||||
get({
|
||||
id: ruleId || '',
|
||||
}),
|
||||
enabled: isValidRuleId,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
return {
|
||||
ruleId,
|
||||
@@ -396,27 +387,24 @@ export const useAlertRuleStatusToggle = ({
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const handleError = useAxiosError();
|
||||
|
||||
const { mutate: toggleAlertState } = useMutation(
|
||||
[REACT_QUERY_KEY.TOGGLE_ALERT_STATE, ruleId],
|
||||
(args: { id: string; data: Partial<RuletypesPostableRuleDTO> }) =>
|
||||
patchRulePartial(args.id, args.data),
|
||||
patchAlert,
|
||||
{
|
||||
onSuccess: (data) => {
|
||||
setAlertRuleState(data.data.state);
|
||||
setAlertRuleState(data?.payload?.state);
|
||||
queryClient.refetchQueries([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId]);
|
||||
notifications.success({
|
||||
message: `Alert has been ${
|
||||
data.data.state === 'disabled' ? 'disabled' : 'enabled'
|
||||
data?.payload?.state === 'disabled' ? 'disabled' : 'enabled'
|
||||
}.`,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
queryClient.refetchQueries([REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId]);
|
||||
showErrorModal(
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
|
||||
);
|
||||
handleError(error);
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -443,14 +431,14 @@ export const useAlertRuleDuplicate = ({
|
||||
|
||||
const params = useUrlQuery();
|
||||
|
||||
const { refetch } = useListRules({
|
||||
query: { cacheTime: 0 },
|
||||
const { refetch } = useQuery(REACT_QUERY_KEY.GET_ALL_ALLERTS, {
|
||||
queryFn: getAll,
|
||||
cacheTime: 0,
|
||||
});
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const handleError = useAxiosError();
|
||||
const { mutate: duplicateAlert } = useMutation(
|
||||
[REACT_QUERY_KEY.DUPLICATE_ALERT_RULE],
|
||||
(args: { data: AlertDef }) =>
|
||||
createRule(toPostableRuleDTOFromAlertDef(args.data)),
|
||||
save,
|
||||
{
|
||||
onSuccess: async () => {
|
||||
notifications.success({
|
||||
@@ -459,17 +447,18 @@ export const useAlertRuleDuplicate = ({
|
||||
|
||||
const { data: allAlertsData } = await refetch();
|
||||
|
||||
const rules = allAlertsData?.data;
|
||||
if (rules && rules.length > 0) {
|
||||
const clonedAlert = rules[rules.length - 1];
|
||||
if (
|
||||
allAlertsData &&
|
||||
allAlertsData.payload &&
|
||||
allAlertsData.payload.length > 0
|
||||
) {
|
||||
const clonedAlert =
|
||||
allAlertsData.payload[allAlertsData.payload.length - 1];
|
||||
params.set(QueryParams.ruleId, String(clonedAlert.id));
|
||||
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
||||
}
|
||||
},
|
||||
onError: (error) =>
|
||||
showErrorModal(
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
|
||||
),
|
||||
onError: handleError,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -495,21 +484,18 @@ export const useAlertRuleUpdate = ({
|
||||
isLoading: boolean;
|
||||
} => {
|
||||
const { notifications } = useNotifications();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const handleError = useAxiosError();
|
||||
|
||||
const { mutate: updateAlertRule, isLoading } = useMutation(
|
||||
[REACT_QUERY_KEY.UPDATE_ALERT_RULE, alertDetails.id],
|
||||
(args: { data: AlertDef; id: string }) =>
|
||||
updateRuleByID({ id: args.id }, toPostableRuleDTOFromAlertDef(args.data)),
|
||||
save,
|
||||
{
|
||||
onMutate: () => setUpdatedName(intermediateName),
|
||||
onSuccess: () =>
|
||||
notifications.success({ message: 'Alert renamed successfully' }),
|
||||
onError: (error) => {
|
||||
setUpdatedName(alertDetails.alert);
|
||||
showErrorModal(
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
|
||||
);
|
||||
handleError(error);
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -517,7 +503,7 @@ export const useAlertRuleUpdate = ({
|
||||
const handleAlertUpdate = (): void => {
|
||||
updateAlertRule({
|
||||
data: { ...alertDetails, alert: intermediateName },
|
||||
id: alertDetails.id || '',
|
||||
id: alertDetails.id,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -532,11 +518,11 @@ export const useAlertRuleDelete = ({
|
||||
handleAlertDelete: () => void;
|
||||
} => {
|
||||
const { notifications } = useNotifications();
|
||||
const { showErrorModal } = useErrorModal();
|
||||
const handleError = useAxiosError();
|
||||
|
||||
const { mutate: deleteAlert } = useMutation(
|
||||
[REACT_QUERY_KEY.REMOVE_ALERT_RULE, ruleId],
|
||||
(args: { id: string }) => deleteRuleByID({ id: args.id }),
|
||||
deleteAlerts,
|
||||
{
|
||||
onSuccess: async () => {
|
||||
notifications.success({
|
||||
@@ -545,11 +531,7 @@ export const useAlertRuleDelete = ({
|
||||
|
||||
history.push(ROUTES.LIST_ALL_ALERT);
|
||||
},
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
onError: (error) =>
|
||||
showErrorModal(
|
||||
convertToApiError(error as AxiosError<RenderErrorResponseDTO>) as APIError,
|
||||
),
|
||||
onError: handleError,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
6
frontend/src/pages/AlertDetails/types.ts
Normal file
6
frontend/src/pages/AlertDetails/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type AlertDetailsStatusRendererProps = {
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
isRefetching: boolean;
|
||||
data: any;
|
||||
};
|
||||
6
frontend/src/pages/EditRules/constants.ts
Normal file
6
frontend/src/pages/EditRules/constants.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export const returnToAlertsPage = 'Return to Alerts Page';
|
||||
|
||||
export const errorMessageReceivedFromBackend = 'sql: no rows in result set';
|
||||
|
||||
export const improvedErrorMessage =
|
||||
'The Alert that you are trying to access does not exist.';
|
||||
@@ -1,15 +1,11 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Button, Card } from 'antd';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { useGetRuleByID } from 'api/generated/services/rules';
|
||||
import type {
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesRuleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import get from 'api/alerts/get';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import EditRulesContainer from 'container/EditRules';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@@ -20,10 +16,12 @@ import {
|
||||
NEW_ALERT_SCHEMA_VERSION,
|
||||
PostableAlertRuleV2,
|
||||
} from 'types/api/alerts/alertTypesV2';
|
||||
|
||||
import {
|
||||
fromRuleDTOToAlertDef,
|
||||
fromRuleDTOToPostableRuleV2,
|
||||
} from 'types/api/alerts/convert';
|
||||
errorMessageReceivedFromBackend,
|
||||
improvedErrorMessage,
|
||||
returnToAlertsPage,
|
||||
} from './constants';
|
||||
|
||||
import './EditRules.styles.scss';
|
||||
|
||||
@@ -35,14 +33,16 @@ function EditRules(): JSX.Element {
|
||||
|
||||
const isValidRuleId = ruleId !== null && String(ruleId).length !== 0;
|
||||
|
||||
const { isLoading, data, isRefetching, isError, error } = useGetRuleByID(
|
||||
{ id: ruleId || '' },
|
||||
const { isLoading, data, isRefetching, isError } = useQuery(
|
||||
[REACT_QUERY_KEY.ALERT_RULE_DETAILS, ruleId],
|
||||
{
|
||||
query: {
|
||||
enabled: isValidRuleId,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
queryFn: () =>
|
||||
get({
|
||||
id: ruleId || '',
|
||||
}),
|
||||
enabled: isValidRuleId,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -65,26 +65,22 @@ function EditRules(): JSX.Element {
|
||||
}
|
||||
}, [isValidRuleId, ruleId, notifications, safeNavigate]);
|
||||
|
||||
const ruleData: RuletypesRuleDTO | undefined = data?.data;
|
||||
|
||||
const apiError = useMemo(
|
||||
() => convertToApiError(error as AxiosError<RenderErrorResponseDTO> | null),
|
||||
[error],
|
||||
);
|
||||
|
||||
if (
|
||||
(isError && !isValidRuleId) ||
|
||||
ruleId == null ||
|
||||
(ruleData === undefined && !isLoading)
|
||||
(data?.payload?.data === undefined && !isLoading)
|
||||
) {
|
||||
const errorMsg = apiError?.getErrorMessage() || '';
|
||||
return (
|
||||
<div className="edit-rules-container edit-rules-container--error">
|
||||
<Card size="small" className="edit-rules-card">
|
||||
<p className="content">{errorMsg || t('something_went_wrong')}</p>
|
||||
<p className="content">
|
||||
{data?.message === errorMessageReceivedFromBackend
|
||||
? improvedErrorMessage
|
||||
: data?.error || t('something_went_wrong')}
|
||||
</p>
|
||||
<div className="btn-container">
|
||||
<Button type="default" size="large" onClick={clickHandler}>
|
||||
Return to Alerts Page
|
||||
{returnToAlertsPage}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -92,20 +88,20 @@ function EditRules(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading || isRefetching || !ruleData) {
|
||||
if (isLoading || isRefetching || !data?.payload) {
|
||||
return <Spinner tip="Loading Rules..." />;
|
||||
}
|
||||
|
||||
let initialV2AlertValue: PostableAlertRuleV2 | null = null;
|
||||
if (ruleData.schemaVersion === NEW_ALERT_SCHEMA_VERSION) {
|
||||
initialV2AlertValue = fromRuleDTOToPostableRuleV2(ruleData);
|
||||
if (data.payload.data.schemaVersion === NEW_ALERT_SCHEMA_VERSION) {
|
||||
initialV2AlertValue = data.payload.data as PostableAlertRuleV2;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="edit-rules-container">
|
||||
<EditRulesContainer
|
||||
ruleId={ruleId || ''}
|
||||
initialValue={fromRuleDTOToAlertDef(ruleData)}
|
||||
initialValue={data.payload.data}
|
||||
initialV2AlertValue={initialV2AlertValue}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
import type {
|
||||
RuletypesAlertCompositeQueryDTO,
|
||||
RuletypesPostableRuleDTO,
|
||||
RuletypesPostableRuleDTOLabels,
|
||||
RuletypesRuleDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
RuletypesAlertTypeDTO,
|
||||
RuletypesPanelTypeDTO,
|
||||
RuletypesQueryTypeDTO,
|
||||
RuletypesRuleTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { AlertTypes } from './alertTypes';
|
||||
import { PostableAlertRuleV2 } from './alertTypesV2';
|
||||
import { ICompositeMetricQuery } from './compositeQuery';
|
||||
import { AlertDef, Labels } from './def';
|
||||
|
||||
function toRuleTypeDTO(ruleType: string | undefined): RuletypesRuleTypeDTO {
|
||||
switch (ruleType) {
|
||||
case RuletypesRuleTypeDTO.promql_rule:
|
||||
return RuletypesRuleTypeDTO.promql_rule;
|
||||
case RuletypesRuleTypeDTO.anomaly_rule:
|
||||
return RuletypesRuleTypeDTO.anomaly_rule;
|
||||
default:
|
||||
return RuletypesRuleTypeDTO.threshold_rule;
|
||||
}
|
||||
}
|
||||
|
||||
function toAlertTypeDTO(
|
||||
alertType: AlertTypes | string | undefined,
|
||||
): RuletypesAlertTypeDTO | undefined {
|
||||
switch (alertType) {
|
||||
case AlertTypes.METRICS_BASED_ALERT:
|
||||
return RuletypesAlertTypeDTO.METRIC_BASED_ALERT;
|
||||
case AlertTypes.LOGS_BASED_ALERT:
|
||||
return RuletypesAlertTypeDTO.LOGS_BASED_ALERT;
|
||||
case AlertTypes.TRACES_BASED_ALERT:
|
||||
return RuletypesAlertTypeDTO.TRACES_BASED_ALERT;
|
||||
case AlertTypes.EXCEPTIONS_BASED_ALERT:
|
||||
return RuletypesAlertTypeDTO.EXCEPTIONS_BASED_ALERT;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function stripUndefinedLabels(
|
||||
labels: Labels | undefined,
|
||||
): RuletypesPostableRuleDTOLabels | undefined {
|
||||
if (!labels) {
|
||||
return undefined;
|
||||
}
|
||||
const out: RuletypesPostableRuleDTOLabels = {};
|
||||
Object.entries(labels).forEach(([key, value]) => {
|
||||
if (typeof value === 'string') {
|
||||
out[key] = value;
|
||||
}
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
// why: local PostableAlertRuleV2/AlertDef diverge from RuletypesPostableRuleDTO
|
||||
// in several spots that match by string value but not by nominal TS type —
|
||||
// condition.{op,matchType}, evaluation.kind, notificationSettings.renotify.alertStates.
|
||||
// The backend accepts the local runtime shape, so one boundary cast encapsulates
|
||||
// the type-surface gap rather than leaking it to call sites.
|
||||
export function toPostableRuleDTO(
|
||||
local: PostableAlertRuleV2,
|
||||
): RuletypesPostableRuleDTO {
|
||||
const payload = {
|
||||
alert: local.alert,
|
||||
alertType: toAlertTypeDTO(local.alertType),
|
||||
ruleType: toRuleTypeDTO(local.ruleType),
|
||||
condition: local.condition,
|
||||
annotations: local.annotations,
|
||||
labels: stripUndefinedLabels(local.labels),
|
||||
notificationSettings: local.notificationSettings,
|
||||
evaluation: local.evaluation,
|
||||
schemaVersion: local.schemaVersion,
|
||||
source: local.source,
|
||||
version: local.version,
|
||||
disabled: local.disabled,
|
||||
};
|
||||
return (payload as unknown) as RuletypesPostableRuleDTO;
|
||||
}
|
||||
|
||||
export function toPostableRuleDTOFromAlertDef(
|
||||
local: AlertDef,
|
||||
): RuletypesPostableRuleDTO {
|
||||
const payload = {
|
||||
alert: local.alert,
|
||||
alertType: toAlertTypeDTO(local.alertType),
|
||||
ruleType: toRuleTypeDTO(local.ruleType),
|
||||
condition: local.condition,
|
||||
annotations: local.annotations,
|
||||
labels: stripUndefinedLabels(local.labels),
|
||||
evalWindow: local.evalWindow,
|
||||
frequency: local.frequency,
|
||||
preferredChannels: local.preferredChannels,
|
||||
source: local.source,
|
||||
version: local.version,
|
||||
disabled: local.disabled,
|
||||
};
|
||||
return (payload as unknown) as RuletypesPostableRuleDTO;
|
||||
}
|
||||
|
||||
export function fromRuleDTOToPostableRuleV2(
|
||||
dto: RuletypesRuleDTO,
|
||||
): PostableAlertRuleV2 {
|
||||
return (dto as unknown) as PostableAlertRuleV2;
|
||||
}
|
||||
|
||||
export function fromRuleDTOToAlertDef(dto: RuletypesRuleDTO): AlertDef {
|
||||
return (dto as unknown) as AlertDef;
|
||||
}
|
||||
|
||||
function toEQueryType(queryType: RuletypesQueryTypeDTO): EQueryType {
|
||||
switch (queryType) {
|
||||
case RuletypesQueryTypeDTO.builder:
|
||||
return EQueryType.QUERY_BUILDER;
|
||||
case RuletypesQueryTypeDTO.clickhouse_sql:
|
||||
return EQueryType.CLICKHOUSE;
|
||||
case RuletypesQueryTypeDTO.promql:
|
||||
return EQueryType.PROM;
|
||||
default:
|
||||
return EQueryType.QUERY_BUILDER;
|
||||
}
|
||||
}
|
||||
|
||||
function toPanelType(panelType: RuletypesPanelTypeDTO): PANEL_TYPES {
|
||||
switch (panelType) {
|
||||
case RuletypesPanelTypeDTO.value:
|
||||
return PANEL_TYPES.VALUE;
|
||||
case RuletypesPanelTypeDTO.table:
|
||||
return PANEL_TYPES.TABLE;
|
||||
case RuletypesPanelTypeDTO.graph:
|
||||
default:
|
||||
return PANEL_TYPES.TIME_SERIES;
|
||||
}
|
||||
}
|
||||
|
||||
export function toCompositeMetricQuery(
|
||||
dto: RuletypesAlertCompositeQueryDTO,
|
||||
): ICompositeMetricQuery {
|
||||
return {
|
||||
queryType: toEQueryType(dto.queryType),
|
||||
panelType: toPanelType(dto.panelType),
|
||||
unit: dto.unit,
|
||||
queries: (dto.queries ?? undefined) as ICompositeMetricQuery['queries'],
|
||||
};
|
||||
}
|
||||
21
frontend/src/types/api/alerts/get.ts
Normal file
21
frontend/src/types/api/alerts/get.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { AlertDef } from './def';
|
||||
|
||||
export interface Props {
|
||||
id: AlertDef['id'];
|
||||
}
|
||||
|
||||
export interface GettableAlert extends AlertDef {
|
||||
id: string;
|
||||
alert: string;
|
||||
state: string;
|
||||
disabled: boolean;
|
||||
createAt: string;
|
||||
createBy: string;
|
||||
updateAt: string;
|
||||
updateBy: string;
|
||||
schemaVersion: string;
|
||||
}
|
||||
|
||||
export type PayloadProps = {
|
||||
data: GettableAlert;
|
||||
};
|
||||
3
frontend/src/types/api/alerts/getAll.ts
Normal file
3
frontend/src/types/api/alerts/getAll.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { GettableAlert } from './get';
|
||||
|
||||
export type PayloadProps = GettableAlert[];
|
||||
12
frontend/src/types/api/alerts/patch.ts
Normal file
12
frontend/src/types/api/alerts/patch.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { GettableAlert } from './get';
|
||||
|
||||
export type PayloadProps = GettableAlert;
|
||||
|
||||
export interface PatchProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
id?: string;
|
||||
data: PatchProps;
|
||||
}
|
||||
@@ -44,6 +44,7 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
|
||||
Description: "This endpoint creates a new alert rule",
|
||||
Request: new(ruletypes.PostableRule),
|
||||
RequestContentType: "application/json",
|
||||
RequestExamples: postableRuleExamples(),
|
||||
Response: new(ruletypes.Rule),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
@@ -54,27 +55,28 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authZ.EditAccess(provider.rulerHandler.UpdateRuleByID), handler.OpenAPIDef{
|
||||
ID: "UpdateRuleByID",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Update alert rule",
|
||||
Description: "This endpoint updates an alert rule by ID",
|
||||
Request: new(ruletypes.PostableRule),
|
||||
RequestContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
ID: "UpdateRuleByID",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Update alert rule",
|
||||
Description: "This endpoint updates an alert rule by ID",
|
||||
Request: new(ruletypes.PostableRule),
|
||||
RequestContentType: "application/json",
|
||||
RequestExamples: postableRuleExamples(),
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/rules/{id}", handler.New(provider.authZ.EditAccess(provider.rulerHandler.DeleteRuleByID), handler.OpenAPIDef{
|
||||
ID: "DeleteRuleByID",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Delete alert rule",
|
||||
Description: "This endpoint deletes an alert rule by ID",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
ID: "DeleteRuleByID",
|
||||
Tags: []string{"rules"},
|
||||
Summary: "Delete alert rule",
|
||||
Description: "This endpoint deletes an alert rule by ID",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -86,6 +88,7 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
|
||||
Description: "This endpoint applies a partial update to an alert rule by ID",
|
||||
Request: new(ruletypes.PostableRule),
|
||||
RequestContentType: "application/json",
|
||||
RequestExamples: postableRuleExamples(),
|
||||
Response: new(ruletypes.Rule),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
@@ -102,6 +105,7 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
|
||||
Description: "This endpoint fires a test notification for the given rule definition",
|
||||
Request: new(ruletypes.PostableRule),
|
||||
RequestContentType: "application/json",
|
||||
RequestExamples: postableRuleExamples(),
|
||||
Response: new(ruletypes.GettableTestRule),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
@@ -156,27 +160,27 @@ func (provider *provider) addRulerRoutes(router *mux.Router) error {
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(provider.authZ.EditAccess(provider.rulerHandler.UpdateDowntimeScheduleByID), handler.OpenAPIDef{
|
||||
ID: "UpdateDowntimeScheduleByID",
|
||||
Tags: []string{"downtimeschedules"},
|
||||
Summary: "Update downtime schedule",
|
||||
Description: "This endpoint updates a downtime schedule by ID",
|
||||
Request: new(ruletypes.PostablePlannedMaintenance),
|
||||
RequestContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
ID: "UpdateDowntimeScheduleByID",
|
||||
Tags: []string{"downtimeschedules"},
|
||||
Summary: "Update downtime schedule",
|
||||
Description: "This endpoint updates a downtime schedule by ID",
|
||||
Request: new(ruletypes.PostablePlannedMaintenance),
|
||||
RequestContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/downtime_schedules/{id}", handler.New(provider.authZ.EditAccess(provider.rulerHandler.DeleteDowntimeScheduleByID), handler.OpenAPIDef{
|
||||
ID: "DeleteDowntimeScheduleByID",
|
||||
Tags: []string{"downtimeschedules"},
|
||||
Summary: "Delete downtime schedule",
|
||||
Description: "This endpoint deletes a downtime schedule by ID",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
ID: "DeleteDowntimeScheduleByID",
|
||||
Tags: []string{"downtimeschedules"},
|
||||
Summary: "Delete downtime schedule",
|
||||
Description: "This endpoint deletes a downtime schedule by ID",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleEditor),
|
||||
})).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
733
pkg/apiserver/signozapiserver/ruler_examples.go
Normal file
733
pkg/apiserver/signozapiserver/ruler_examples.go
Normal file
@@ -0,0 +1,733 @@
|
||||
package signozapiserver
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/http/handler"
|
||||
|
||||
// postableRuleExamples returns example payloads attached to every rule-write
|
||||
// endpoint. They cover each alert type, rule type, and composite-query shape.
|
||||
func postableRuleExamples() []handler.OpenAPIExample {
|
||||
rolling := func(evalWindow, frequency string) map[string]any {
|
||||
return map[string]any{
|
||||
"kind": "rolling",
|
||||
"spec": map[string]any{"evalWindow": evalWindow, "frequency": frequency},
|
||||
}
|
||||
}
|
||||
renotify := func(interval string, states ...string) map[string]any {
|
||||
s := make([]any, 0, len(states))
|
||||
for _, v := range states {
|
||||
s = append(s, v)
|
||||
}
|
||||
return map[string]any{
|
||||
"enabled": true,
|
||||
"interval": interval,
|
||||
"alertStates": s,
|
||||
}
|
||||
}
|
||||
|
||||
return []handler.OpenAPIExample{
|
||||
{
|
||||
Name: "metric_threshold_single",
|
||||
Summary: "Metric threshold single builder query",
|
||||
Description: "Fires when a pod consumes more than 80% of its requested CPU for the whole evaluation window. Uses `k8s.pod.cpu_request_utilization`.",
|
||||
Value: map[string]any{
|
||||
"alert": "Pod CPU above 80% of request",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"description": "CPU usage for api-service pods exceeds 80% of the requested CPU",
|
||||
"ruleType": "threshold_rule",
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": map[string]any{
|
||||
"compositeQuery": map[string]any{
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"unit": "percentunit",
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"stepInterval": 60,
|
||||
"aggregations": []any{map[string]any{"metricName": "k8s.pod.cpu_request_utilization", "timeAggregation": "avg", "spaceAggregation": "max"}},
|
||||
"filter": map[string]any{"expression": "k8s.deployment.name = 'api-service'"},
|
||||
"groupBy": []any{
|
||||
map[string]any{"name": "k8s.pod.name", "fieldContext": "resource", "fieldDataType": "string"},
|
||||
map[string]any{"name": "deployment.environment", "fieldContext": "resource", "fieldDataType": "string"},
|
||||
},
|
||||
"legend": "{{k8s.pod.name}} ({{deployment.environment}})",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"selectedQueryName": "A",
|
||||
"thresholds": map[string]any{
|
||||
"kind": "basic",
|
||||
"spec": []any{
|
||||
map[string]any{
|
||||
"name": "critical",
|
||||
"op": "above",
|
||||
"matchType": "all_the_times",
|
||||
"target": 0.8,
|
||||
"channels": []any{"slack-platform", "pagerduty-oncall"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"evaluation": rolling("15m", "1m"),
|
||||
"notificationSettings": map[string]any{
|
||||
"groupBy": []any{"k8s.pod.name", "deployment.environment"},
|
||||
"renotify": renotify("4h", "firing"),
|
||||
},
|
||||
"labels": map[string]any{"severity": "critical", "team": "platform"},
|
||||
"annotations": map[string]any{
|
||||
"description": "Pod {{$k8s.pod.name}} CPU is at {{$value}} of request in {{$deployment.environment}}.",
|
||||
"summary": "Pod CPU above {{$threshold}} of request",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "metric_threshold_formula",
|
||||
Summary: "Metric threshold multi-query formula",
|
||||
Description: "Computes disk utilization as (1 - available/capacity) * 100 by combining two disabled base queries with a builder_formula. The formula emits 0–100, so compositeQuery.unit is set to \"percent\" and the target is a bare number.",
|
||||
Value: map[string]any{
|
||||
"alert": "PersistentVolume above 80% utilization",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"description": "Disk utilization for a persistent volume is above 80%",
|
||||
"ruleType": "threshold_rule",
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": map[string]any{
|
||||
"compositeQuery": map[string]any{
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"unit": "percent",
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"stepInterval": 60,
|
||||
"disabled": true,
|
||||
"aggregations": []any{map[string]any{"metricName": "k8s.volume.available", "timeAggregation": "max", "spaceAggregation": "max"}},
|
||||
"filter": map[string]any{"expression": "k8s.volume.type = 'persistentVolumeClaim'"},
|
||||
"groupBy": []any{
|
||||
map[string]any{"name": "k8s.persistentvolumeclaim.name", "fieldContext": "resource", "fieldDataType": "string"},
|
||||
map[string]any{"name": "k8s.namespace.name", "fieldContext": "resource", "fieldDataType": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "B",
|
||||
"signal": "metrics",
|
||||
"stepInterval": 60,
|
||||
"disabled": true,
|
||||
"aggregations": []any{map[string]any{"metricName": "k8s.volume.capacity", "timeAggregation": "max", "spaceAggregation": "max"}},
|
||||
"filter": map[string]any{"expression": "k8s.volume.type = 'persistentVolumeClaim'"},
|
||||
"groupBy": []any{
|
||||
map[string]any{"name": "k8s.persistentvolumeclaim.name", "fieldContext": "resource", "fieldDataType": "string"},
|
||||
map[string]any{"name": "k8s.namespace.name", "fieldContext": "resource", "fieldDataType": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "builder_formula",
|
||||
"spec": map[string]any{
|
||||
"name": "F1",
|
||||
"expression": "(1 - A/B) * 100",
|
||||
"legend": "{{k8s.persistentvolumeclaim.name}} ({{k8s.namespace.name}})",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"selectedQueryName": "F1",
|
||||
"thresholds": map[string]any{
|
||||
"kind": "basic",
|
||||
"spec": []any{
|
||||
map[string]any{
|
||||
"name": "critical",
|
||||
"op": "above",
|
||||
"matchType": "at_least_once",
|
||||
"target": 80,
|
||||
"channels": []any{"slack-storage"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"evaluation": rolling("30m", "5m"),
|
||||
"notificationSettings": map[string]any{
|
||||
"groupBy": []any{"k8s.namespace.name", "k8s.persistentvolumeclaim.name"},
|
||||
"renotify": renotify("2h", "firing"),
|
||||
},
|
||||
"labels": map[string]any{"severity": "critical"},
|
||||
"annotations": map[string]any{
|
||||
"description": "Volume {{$k8s.persistentvolumeclaim.name}} in {{$k8s.namespace.name}} is {{$value}}% full.",
|
||||
"summary": "Disk utilization above {{$threshold}}%",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "metric_promql",
|
||||
Summary: "Metric threshold PromQL rule",
|
||||
Description: "PromQL expression instead of the builder. Dotted OTEL resource attributes are quoted (\"deployment.environment\"). Useful for queries that combine series with group_right or other Prom operators.",
|
||||
Value: map[string]any{
|
||||
"alert": "Kafka consumer group lag above 1000",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"description": "Consumer group lag computed via PromQL",
|
||||
"ruleType": "promql_rule",
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": map[string]any{
|
||||
"compositeQuery": map[string]any{
|
||||
"queryType": "promql",
|
||||
"panelType": "graph",
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "promql",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"query": "(max by(topic, partition, \"deployment.environment\")(kafka_log_end_offset) - on(topic, partition, \"deployment.environment\") group_right max by(group, topic, partition, \"deployment.environment\")(kafka_consumer_committed_offset)) > 0",
|
||||
"legend": "{{topic}}/{{partition}} ({{group}})",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"selectedQueryName": "A",
|
||||
"thresholds": map[string]any{
|
||||
"kind": "basic",
|
||||
"spec": []any{
|
||||
map[string]any{
|
||||
"name": "critical",
|
||||
"op": "above",
|
||||
"matchType": "all_the_times",
|
||||
"target": 1000,
|
||||
"channels": []any{"slack-data-platform", "pagerduty-data"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"evaluation": rolling("10m", "1m"),
|
||||
"notificationSettings": map[string]any{
|
||||
"groupBy": []any{"group", "topic"},
|
||||
"renotify": renotify("1h", "firing"),
|
||||
},
|
||||
"labels": map[string]any{"severity": "critical"},
|
||||
"annotations": map[string]any{
|
||||
"description": "Consumer group {{$group}} is {{$value}} messages behind on {{$topic}}/{{$partition}}.",
|
||||
"summary": "Kafka consumer lag high",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "metric_anomaly",
|
||||
Summary: "Metric anomaly rule (v1 only)",
|
||||
Description: "Anomaly rules are not yet supported under schemaVersion v2alpha1, so this example uses the v1 shape. Wraps a builder query in the `anomaly` function with daily seasonality SigNoz compares each point against the forecast for that time of day. Fires when the anomaly score stays below the threshold for the entire window; `requireMinPoints` guards against noisy intervals.",
|
||||
Value: map[string]any{
|
||||
"alert": "Anomalous drop in ingested spans",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"description": "Detect an abrupt drop in span ingestion using a z-score anomaly function",
|
||||
"ruleType": "anomaly_rule",
|
||||
"version": "v5",
|
||||
"evalWindow": "24h",
|
||||
"frequency": "3h",
|
||||
"condition": map[string]any{
|
||||
"compositeQuery": map[string]any{
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"stepInterval": 21600,
|
||||
"aggregations": []any{map[string]any{"metricName": "otelcol_receiver_accepted_spans", "timeAggregation": "rate", "spaceAggregation": "sum"}},
|
||||
"filter": map[string]any{"expression": "tenant_tier = 'premium'"},
|
||||
"groupBy": []any{map[string]any{"name": "tenant_id", "fieldContext": "attribute", "fieldDataType": "string"}},
|
||||
"functions": []any{
|
||||
map[string]any{
|
||||
"name": "anomaly",
|
||||
"args": []any{map[string]any{"name": "z_score_threshold", "value": 2}},
|
||||
},
|
||||
},
|
||||
"legend": "{{tenant_id}}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"op": "below",
|
||||
"matchType": "all_the_times",
|
||||
"target": 2,
|
||||
"algorithm": "standard",
|
||||
"seasonality": "daily",
|
||||
"selectedQueryName": "A",
|
||||
"requireMinPoints": true,
|
||||
"requiredNumPoints": 3,
|
||||
},
|
||||
"labels": map[string]any{"severity": "warning"},
|
||||
"preferredChannels": []any{"slack-ingestion"},
|
||||
"annotations": map[string]any{
|
||||
"description": "Ingestion rate for tenant {{$tenant_id}} is anomalously low (z-score {{$value}}).",
|
||||
"summary": "Span ingestion anomaly",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "logs_threshold",
|
||||
Summary: "Logs threshold count() over filter",
|
||||
Description: "Counts matching log records (ERROR severity + body contains) over a rolling window. Fires at least once per evaluation when the count exceeds zero.",
|
||||
Value: map[string]any{
|
||||
"alert": "Payments service panic logs",
|
||||
"alertType": "LOGS_BASED_ALERT",
|
||||
"description": "Any panic log line emitted by the payments service",
|
||||
"ruleType": "threshold_rule",
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": map[string]any{
|
||||
"compositeQuery": map[string]any{
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"stepInterval": 60,
|
||||
"aggregations": []any{map[string]any{"expression": "count()"}},
|
||||
"filter": map[string]any{"expression": "service.name = 'payments-api' AND severity_text = 'ERROR' AND body CONTAINS 'panic'"},
|
||||
"groupBy": []any{
|
||||
map[string]any{"name": "k8s.pod.name", "fieldContext": "resource", "fieldDataType": "string"},
|
||||
map[string]any{"name": "deployment.environment", "fieldContext": "resource", "fieldDataType": "string"},
|
||||
},
|
||||
"legend": "{{k8s.pod.name}} ({{deployment.environment}})",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"selectedQueryName": "A",
|
||||
"thresholds": map[string]any{
|
||||
"kind": "basic",
|
||||
"spec": []any{
|
||||
map[string]any{
|
||||
"name": "critical",
|
||||
"op": "above",
|
||||
"matchType": "at_least_once",
|
||||
"target": 0,
|
||||
"channels": []any{"slack-payments", "pagerduty-payments"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"evaluation": rolling("5m", "1m"),
|
||||
"notificationSettings": map[string]any{
|
||||
"groupBy": []any{"k8s.pod.name", "deployment.environment"},
|
||||
"renotify": renotify("15m", "firing"),
|
||||
},
|
||||
"labels": map[string]any{"severity": "critical", "team": "payments"},
|
||||
"annotations": map[string]any{
|
||||
"description": "{{$k8s.pod.name}} emitted {{$value}} panic log(s) in {{$deployment.environment}}.",
|
||||
"summary": "Payments service panic",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "logs_error_rate_formula",
|
||||
Summary: "Logs error rate error count / total count × 100",
|
||||
Description: "Two disabled log count queries (A = errors, B = total) combined via a builder_formula into a percentage. Classic service-level error-rate alert pattern for log-based signals.",
|
||||
Value: map[string]any{
|
||||
"alert": "Payments-api error log rate above 1%",
|
||||
"alertType": "LOGS_BASED_ALERT",
|
||||
"description": "Error log ratio as a percentage of total logs for payments-api",
|
||||
"ruleType": "threshold_rule",
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": map[string]any{
|
||||
"compositeQuery": map[string]any{
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"unit": "percent",
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "logs",
|
||||
"stepInterval": 60,
|
||||
"disabled": true,
|
||||
"aggregations": []any{map[string]any{"expression": "count()"}},
|
||||
"filter": map[string]any{"expression": "service.name = 'payments-api' AND severity_text IN ['ERROR', 'FATAL']"},
|
||||
"groupBy": []any{map[string]any{"name": "deployment.environment", "fieldContext": "resource", "fieldDataType": "string"}},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "B",
|
||||
"signal": "logs",
|
||||
"stepInterval": 60,
|
||||
"disabled": true,
|
||||
"aggregations": []any{map[string]any{"expression": "count()"}},
|
||||
"filter": map[string]any{"expression": "service.name = 'payments-api'"},
|
||||
"groupBy": []any{map[string]any{"name": "deployment.environment", "fieldContext": "resource", "fieldDataType": "string"}},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "builder_formula",
|
||||
"spec": map[string]any{
|
||||
"name": "F1",
|
||||
"expression": "(A / B) * 100",
|
||||
"legend": "{{deployment.environment}}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"selectedQueryName": "F1",
|
||||
"thresholds": map[string]any{
|
||||
"kind": "basic",
|
||||
"spec": []any{
|
||||
map[string]any{
|
||||
"name": "critical",
|
||||
"op": "above",
|
||||
"matchType": "at_least_once",
|
||||
"target": 1,
|
||||
"channels": []any{"slack-payments"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"evaluation": rolling("5m", "1m"),
|
||||
"notificationSettings": map[string]any{
|
||||
"groupBy": []any{"deployment.environment"},
|
||||
"renotify": renotify("30m", "firing"),
|
||||
},
|
||||
"labels": map[string]any{"severity": "critical", "team": "payments"},
|
||||
"annotations": map[string]any{
|
||||
"description": "Error log rate in {{$deployment.environment}} is {{$value}}%",
|
||||
"summary": "Payments-api error rate above {{$threshold}}%",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "traces_threshold_latency",
|
||||
Summary: "Traces threshold p99 latency (ns → s conversion)",
|
||||
Description: "Builder query against the traces signal with p99(duration_nano). The series unit is ns (compositeQuery.unit), the target is in seconds (threshold.targetUnit) SigNoz converts before comparing. Canonical shape when series and target live in different units.",
|
||||
Value: map[string]any{
|
||||
"alert": "Search API p99 latency above 5s",
|
||||
"alertType": "TRACES_BASED_ALERT",
|
||||
"description": "p99 duration of the search endpoint exceeds 5s",
|
||||
"ruleType": "threshold_rule",
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": map[string]any{
|
||||
"compositeQuery": map[string]any{
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"unit": "ns",
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"stepInterval": 60,
|
||||
"aggregations": []any{map[string]any{"expression": "p99(duration_nano)"}},
|
||||
"filter": map[string]any{"expression": "service.name = 'search-api' AND name = 'GET /api/v1/search'"},
|
||||
"groupBy": []any{
|
||||
map[string]any{"name": "service.name", "fieldContext": "resource", "fieldDataType": "string"},
|
||||
map[string]any{"name": "http.route", "fieldContext": "attribute", "fieldDataType": "string"},
|
||||
},
|
||||
"legend": "{{service.name}} {{http.route}}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"selectedQueryName": "A",
|
||||
"thresholds": map[string]any{
|
||||
"kind": "basic",
|
||||
"spec": []any{
|
||||
map[string]any{
|
||||
"name": "warning",
|
||||
"op": "above",
|
||||
"matchType": "at_least_once",
|
||||
"target": 5,
|
||||
"targetUnit": "s",
|
||||
"channels": []any{"slack-search"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"evaluation": rolling("5m", "1m"),
|
||||
"notificationSettings": map[string]any{
|
||||
"groupBy": []any{"service.name", "http.route"},
|
||||
"renotify": renotify("30m", "firing"),
|
||||
},
|
||||
"labels": map[string]any{"severity": "warning", "team": "search"},
|
||||
"annotations": map[string]any{
|
||||
"description": "p99 latency for {{$service.name}} on {{$http.route}} crossed {{$threshold}}s.",
|
||||
"summary": "Search-api latency degraded",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "traces_error_rate_formula",
|
||||
Summary: "Traces error rate error spans / total spans × 100",
|
||||
Description: "Two disabled trace count queries (A = error spans where hasError=true, B = total spans) combined via a builder_formula into a percentage. Mirrors the common request-error-rate dashboard shape.",
|
||||
Value: map[string]any{
|
||||
"alert": "Search-api error rate above 5%",
|
||||
"alertType": "TRACES_BASED_ALERT",
|
||||
"description": "Request error rate for search-api, grouped by route",
|
||||
"ruleType": "threshold_rule",
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": map[string]any{
|
||||
"compositeQuery": map[string]any{
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"unit": "percent",
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"stepInterval": 60,
|
||||
"disabled": true,
|
||||
"aggregations": []any{map[string]any{"expression": "count()"}},
|
||||
"filter": map[string]any{"expression": "service.name = 'search-api' AND hasError = true"},
|
||||
"groupBy": []any{
|
||||
map[string]any{"name": "service.name", "fieldContext": "resource", "fieldDataType": "string"},
|
||||
map[string]any{"name": "http.route", "fieldContext": "attribute", "fieldDataType": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "B",
|
||||
"signal": "traces",
|
||||
"stepInterval": 60,
|
||||
"disabled": true,
|
||||
"aggregations": []any{map[string]any{"expression": "count()"}},
|
||||
"filter": map[string]any{"expression": "service.name = 'search-api'"},
|
||||
"groupBy": []any{
|
||||
map[string]any{"name": "service.name", "fieldContext": "resource", "fieldDataType": "string"},
|
||||
map[string]any{"name": "http.route", "fieldContext": "attribute", "fieldDataType": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "builder_formula",
|
||||
"spec": map[string]any{
|
||||
"name": "F1",
|
||||
"expression": "(A / B) * 100",
|
||||
"legend": "{{service.name}} {{http.route}}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"selectedQueryName": "F1",
|
||||
"thresholds": map[string]any{
|
||||
"kind": "basic",
|
||||
"spec": []any{
|
||||
map[string]any{
|
||||
"name": "critical",
|
||||
"op": "above",
|
||||
"matchType": "at_least_once",
|
||||
"target": 5,
|
||||
"channels": []any{"slack-search", "pagerduty-search"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"evaluation": rolling("5m", "1m"),
|
||||
"notificationSettings": map[string]any{
|
||||
"groupBy": []any{"service.name", "http.route"},
|
||||
"renotify": renotify("15m", "firing"),
|
||||
},
|
||||
"labels": map[string]any{"severity": "critical", "team": "search"},
|
||||
"annotations": map[string]any{
|
||||
"description": "Error rate on {{$service.name}} {{$http.route}} is {{$value}}%",
|
||||
"summary": "Search-api error rate above {{$threshold}}%",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "tiered_thresholds",
|
||||
Summary: "Tiered thresholds with per-tier channels",
|
||||
Description: "Two tiers (warning and critical) in a single rule, each with its own target, op, matchType, and channels so warnings and pages route to different receivers. `alertOnAbsent` + `absentFor` fires a no-data alert when the query returns no series for 15 consecutive evaluations.",
|
||||
Value: map[string]any{
|
||||
"alert": "Kafka consumer lag warn / critical",
|
||||
"alertType": "METRIC_BASED_ALERT",
|
||||
"description": "Warn at lag ≥ 50 and page at ≥ 200, tiered via thresholds.spec.",
|
||||
"ruleType": "threshold_rule",
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": map[string]any{
|
||||
"compositeQuery": map[string]any{
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "metrics",
|
||||
"stepInterval": 60,
|
||||
"disabled": true,
|
||||
"aggregations": []any{map[string]any{"metricName": "kafka_log_end_offset", "timeAggregation": "max", "spaceAggregation": "max"}},
|
||||
"filter": map[string]any{"expression": "topic != '__consumer_offsets'"},
|
||||
"groupBy": []any{
|
||||
map[string]any{"name": "topic", "fieldContext": "attribute", "fieldDataType": "string"},
|
||||
map[string]any{"name": "partition", "fieldContext": "attribute", "fieldDataType": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "B",
|
||||
"signal": "metrics",
|
||||
"stepInterval": 60,
|
||||
"disabled": true,
|
||||
"aggregations": []any{map[string]any{"metricName": "kafka_consumer_committed_offset", "timeAggregation": "max", "spaceAggregation": "max"}},
|
||||
"filter": map[string]any{"expression": "topic != '__consumer_offsets'"},
|
||||
"groupBy": []any{
|
||||
map[string]any{"name": "topic", "fieldContext": "attribute", "fieldDataType": "string"},
|
||||
map[string]any{"name": "partition", "fieldContext": "attribute", "fieldDataType": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "builder_formula",
|
||||
"spec": map[string]any{
|
||||
"name": "F1",
|
||||
"expression": "A - B",
|
||||
"legend": "{{topic}}/{{partition}}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"alertOnAbsent": true,
|
||||
"absentFor": 15,
|
||||
"selectedQueryName": "F1",
|
||||
"thresholds": map[string]any{
|
||||
"kind": "basic",
|
||||
"spec": []any{
|
||||
map[string]any{
|
||||
"name": "warning",
|
||||
"op": "above",
|
||||
"matchType": "all_the_times",
|
||||
"target": 50,
|
||||
"channels": []any{"slack-kafka-info"},
|
||||
},
|
||||
map[string]any{
|
||||
"name": "critical",
|
||||
"op": "above",
|
||||
"matchType": "all_the_times",
|
||||
"target": 200,
|
||||
"channels": []any{"slack-kafka-alerts", "pagerduty-kafka"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"evaluation": rolling("5m", "1m"),
|
||||
"notificationSettings": map[string]any{
|
||||
"groupBy": []any{"topic"},
|
||||
"renotify": renotify("15m", "firing"),
|
||||
},
|
||||
"labels": map[string]any{"team": "data-platform"},
|
||||
"annotations": map[string]any{
|
||||
"description": "Consumer lag for {{$topic}} partition {{$partition}} is {{$value}}.",
|
||||
"summary": "Kafka consumer lag",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "notification_settings",
|
||||
Summary: "Full notification settings (grouping, nodata renotify, grace period)",
|
||||
Description: "Demonstrates the full notificationSettings surface: `groupBy` merges alerts across labels to cut noise, `newGroupEvalDelay` gives newly-appearing series a grace period before firing, `renotify` re-alerts every 30m while firing OR while the alert is in nodata (missing data is treated as actionable), and `usePolicy: false` means channels come from the threshold entries rather than global routing policies. Set `usePolicy: true` to skip per-threshold channels and route via the org-level notification policy instead.",
|
||||
Value: map[string]any{
|
||||
"alert": "API 5xx error rate above 1%",
|
||||
"alertType": "TRACES_BASED_ALERT",
|
||||
"description": "Noise-controlled 5xx error rate alert with renotify on gaps",
|
||||
"ruleType": "threshold_rule",
|
||||
"version": "v5",
|
||||
"schemaVersion": "v2alpha1",
|
||||
"condition": map[string]any{
|
||||
"compositeQuery": map[string]any{
|
||||
"queryType": "builder",
|
||||
"panelType": "graph",
|
||||
"unit": "percent",
|
||||
"queries": []any{
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "A",
|
||||
"signal": "traces",
|
||||
"stepInterval": 60,
|
||||
"disabled": true,
|
||||
"aggregations": []any{map[string]any{"expression": "count()"}},
|
||||
"filter": map[string]any{"expression": "service.name CONTAINS 'api' AND http.status_code >= 500"},
|
||||
"groupBy": []any{
|
||||
map[string]any{"name": "service.name", "fieldContext": "resource", "fieldDataType": "string"},
|
||||
map[string]any{"name": "deployment.environment", "fieldContext": "resource", "fieldDataType": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "builder_query",
|
||||
"spec": map[string]any{
|
||||
"name": "B",
|
||||
"signal": "traces",
|
||||
"stepInterval": 60,
|
||||
"disabled": true,
|
||||
"aggregations": []any{map[string]any{"expression": "count()"}},
|
||||
"filter": map[string]any{"expression": "service.name CONTAINS 'api'"},
|
||||
"groupBy": []any{
|
||||
map[string]any{"name": "service.name", "fieldContext": "resource", "fieldDataType": "string"},
|
||||
map[string]any{"name": "deployment.environment", "fieldContext": "resource", "fieldDataType": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "builder_formula",
|
||||
"spec": map[string]any{
|
||||
"name": "F1",
|
||||
"expression": "(A / B) * 100",
|
||||
"legend": "{{service.name}} ({{deployment.environment}})",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"selectedQueryName": "F1",
|
||||
"thresholds": map[string]any{
|
||||
"kind": "basic",
|
||||
"spec": []any{
|
||||
map[string]any{
|
||||
"name": "critical",
|
||||
"op": "above",
|
||||
"matchType": "at_least_once",
|
||||
"target": 1,
|
||||
"channels": []any{"slack-api-alerts", "pagerduty-oncall"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"evaluation": rolling("5m", "1m"),
|
||||
"notificationSettings": map[string]any{
|
||||
"groupBy": []any{"service.name", "deployment.environment"},
|
||||
"newGroupEvalDelay": "2m",
|
||||
"usePolicy": false,
|
||||
"renotify": renotify("30m", "firing", "nodata"),
|
||||
},
|
||||
"labels": map[string]any{"team": "platform"},
|
||||
"annotations": map[string]any{
|
||||
"description": "{{$service.name}} 5xx rate in {{$deployment.environment}} is {{$value}}%.",
|
||||
"summary": "API service error rate elevated",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
32
pkg/apiserver/signozapiserver/ruler_examples_test.go
Normal file
32
pkg/apiserver/signozapiserver/ruler_examples_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/ruletypes"
|
||||
)
|
||||
|
||||
// TestPostableRuleExamplesValidate verifies every example payload returned by
|
||||
// postableRuleExamples() round-trips through PostableRule.UnmarshalJSON and
|
||||
// passes Validate(). If an example drifts from the runtime contract this
|
||||
// breaks loudly so the spec doesn't ship invalid payloads to users.
|
||||
func TestPostableRuleExamplesValidate(t *testing.T) {
|
||||
for _, example := range postableRuleExamples() {
|
||||
t.Run(example.Name, func(t *testing.T) {
|
||||
raw, err := json.Marshal(example.Value)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal example: %v", err)
|
||||
}
|
||||
|
||||
var rule ruletypes.PostableRule
|
||||
if err := json.Unmarshal(raw, &rule); err != nil {
|
||||
t.Fatalf("unmarshal: %v\npayload: %s", err, raw)
|
||||
}
|
||||
|
||||
if err := rule.Validate(); err != nil {
|
||||
t.Fatalf("Validate: %v\npayload: %s", err, raw)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -114,11 +114,11 @@ type AlertCompositeQuery struct {
|
||||
|
||||
type RuleCondition struct {
|
||||
CompositeQuery *AlertCompositeQuery `json:"compositeQuery" required:"true"`
|
||||
CompareOperator CompareOperator `json:"op" required:"true"`
|
||||
CompareOperator CompareOperator `json:"op,omitzero"`
|
||||
Target *float64 `json:"target,omitempty"`
|
||||
AlertOnAbsent bool `json:"alertOnAbsent,omitempty"`
|
||||
AbsentFor uint64 `json:"absentFor,omitempty"`
|
||||
MatchType MatchType `json:"matchType" required:"true"`
|
||||
MatchType MatchType `json:"matchType,omitzero"`
|
||||
TargetUnit string `json:"targetUnit,omitempty"`
|
||||
Algorithm string `json:"algorithm,omitempty"`
|
||||
Seasonality Seasonality `json:"seasonality,omitzero"`
|
||||
|
||||
@@ -50,13 +50,13 @@ const (
|
||||
// PostableRule is used to create alerting rule from HTTP api.
|
||||
type PostableRule struct {
|
||||
AlertName string `json:"alert" required:"true"`
|
||||
AlertType AlertType `json:"alertType,omitempty"`
|
||||
AlertType AlertType `json:"alertType" required:"true"`
|
||||
Description string `json:"description,omitempty"`
|
||||
RuleType RuleType `json:"ruleType,omitzero" required:"true"`
|
||||
RuleType RuleType `json:"ruleType" required:"true"`
|
||||
EvalWindow valuer.TextDuration `json:"evalWindow,omitzero"`
|
||||
Frequency valuer.TextDuration `json:"frequency,omitzero"`
|
||||
|
||||
RuleCondition *RuleCondition `json:"condition,omitempty" required:"true"`
|
||||
RuleCondition *RuleCondition `json:"condition" required:"true"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
|
||||
@@ -67,9 +67,9 @@ type PostableRule struct {
|
||||
|
||||
PreferredChannels []string `json:"preferredChannels,omitempty"`
|
||||
|
||||
Version string `json:"version,omitempty"`
|
||||
Version string `json:"version"`
|
||||
|
||||
Evaluation *EvaluationEnvelope `yaml:"evaluation,omitempty" json:"evaluation,omitempty"`
|
||||
Evaluation *EvaluationEnvelope `json:"evaluation,omitempty"`
|
||||
SchemaVersion string `json:"schemaVersion,omitempty"`
|
||||
|
||||
NotificationSettings *NotificationSettings `json:"notificationSettings,omitempty"`
|
||||
|
||||
Reference in New Issue
Block a user