mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-07 18:32:12 +00:00
Compare commits
2 Commits
playwright
...
thirdparty
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f1bd624e6 | ||
|
|
a45f427533 |
@@ -26,6 +26,10 @@ type resources
|
||||
define create: [user, role#assignee]
|
||||
define list: [user, role#assignee]
|
||||
|
||||
define read: [user, role#assignee]
|
||||
define update: [user, role#assignee]
|
||||
define delete: [user, role#assignee]
|
||||
|
||||
type resource
|
||||
relations
|
||||
define read: [user, anonymous, role#assignee]
|
||||
|
||||
@@ -107,7 +107,7 @@ func (middleware *AuthZ) OpenAccess(next http.HandlerFunc) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// Check middleware accepts the relation, typeable, parentTypeable (for direct access + group relations) and a callback function to derive selector and parentSelectors on per request basis.
|
||||
func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relation, translation authtypes.Relation, typeable authtypes.Typeable, cb authtypes.SelectorCallbackFn) http.HandlerFunc {
|
||||
func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relation, translation authtypes.Relation, typeable authtypes.Typeable, parentTypeable authtypes.Typeable, cb authtypes.SelectorCallbackFn) http.HandlerFunc {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
claims, err := authtypes.ClaimsFromContext(req.Context())
|
||||
if err != nil {
|
||||
@@ -115,13 +115,13 @@ func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relatio
|
||||
return
|
||||
}
|
||||
|
||||
selector, err := cb(req.Context(), claims)
|
||||
selector, parentSelectors, err := cb(req)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = middleware.authzService.CheckWithTupleCreation(req.Context(), claims, relation, typeable, selector)
|
||||
err = middleware.authzService.CheckWithTupleCreation(req.Context(), claims, relation, typeable, selector, parentTypeable, parentSelectors...)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
|
||||
@@ -251,7 +251,7 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, t
|
||||
continue
|
||||
}
|
||||
}
|
||||
results, err := r.Threshold.ShouldAlert(*series, r.Unit())
|
||||
results, err := r.Threshold.ShouldAlert(*series)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -301,7 +301,7 @@ func (r *AnomalyRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUID,
|
||||
continue
|
||||
}
|
||||
}
|
||||
results, err := r.Threshold.ShouldAlert(*series, r.Unit())
|
||||
results, err := r.Threshold.ShouldAlert(*series)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -336,19 +336,14 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
||||
resultFPs := map[uint64]struct{}{}
|
||||
var alerts = make(map[uint64]*ruletypes.Alert, len(res))
|
||||
|
||||
ruleReceivers := r.Threshold.GetRuleReceivers()
|
||||
ruleReceiverMap := make(map[string][]string)
|
||||
for _, value := range ruleReceivers {
|
||||
ruleReceiverMap[value.Name] = value.Channels
|
||||
}
|
||||
|
||||
for _, smpl := range res {
|
||||
l := make(map[string]string, len(smpl.Metric))
|
||||
for _, lbl := range smpl.Metric {
|
||||
l[lbl.Name] = lbl.Value
|
||||
}
|
||||
|
||||
value := valueFormatter.Format(smpl.V, r.Unit())
|
||||
threshold := valueFormatter.Format(smpl.Target, smpl.TargetUnit)
|
||||
threshold := valueFormatter.Format(r.TargetVal(), r.Unit())
|
||||
r.logger.DebugContext(ctx, "Alert template data for rule", "rule_name", r.Name(), "formatter", valueFormatter.Name(), "value", value, "threshold", threshold)
|
||||
|
||||
tmplData := ruletypes.AlertTemplateData(l, value, threshold)
|
||||
@@ -413,12 +408,13 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
||||
State: model.StatePending,
|
||||
Value: smpl.V,
|
||||
GeneratorURL: r.GeneratorURL(),
|
||||
Receivers: ruleReceiverMap[lbs.Map()[ruletypes.LabelThresholdName]],
|
||||
Receivers: r.PreferredChannels(),
|
||||
Missing: smpl.IsMissing,
|
||||
}
|
||||
}
|
||||
|
||||
r.logger.InfoContext(ctx, "number of alerts found", "rule_name", r.Name(), "alerts_count", len(alerts))
|
||||
|
||||
// alerts[h] is ready, add or update active list now
|
||||
for h, a := range alerts {
|
||||
// Check whether we already have alerting state for the identifying label set.
|
||||
@@ -427,9 +423,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro
|
||||
|
||||
alert.Value = a.Value
|
||||
alert.Annotations = a.Annotations
|
||||
if v, ok := alert.Labels.Map()[ruletypes.LabelThresholdName]; ok {
|
||||
alert.Receivers = ruleReceiverMap[v]
|
||||
}
|
||||
alert.Receivers = r.PreferredChannels()
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ func TestNotification(opts baserules.PrepareTestRuleOptions) (int, *basemodel.Ap
|
||||
if parsedRule.RuleType == ruletypes.RuleTypeThreshold {
|
||||
|
||||
// add special labels for test alerts
|
||||
parsedRule.Annotations[labels.AlertSummaryLabel] = fmt.Sprintf("The rule threshold is set to %.4f, and the observed metric value is {{$value}}.", *parsedRule.RuleCondition.Target)
|
||||
parsedRule.Labels[labels.RuleSourceLabel] = ""
|
||||
parsedRule.Labels[labels.AlertRuleIdLabel] = ""
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
// Read credentials from environment variables
|
||||
const username = process.env.SIGNOZ_E2E_USERNAME;
|
||||
const password = process.env.SIGNOZ_E2E_PASSWORD;
|
||||
const username = process.env.LOGIN_USERNAME;
|
||||
const password = process.env.LOGIN_PASSWORD;
|
||||
const baseURL = process.env.BASE_URL;
|
||||
|
||||
/**
|
||||
* Ensures the user is logged in. If not, performs the login steps.
|
||||
@@ -10,17 +11,17 @@ const password = process.env.SIGNOZ_E2E_PASSWORD;
|
||||
*/
|
||||
export async function ensureLoggedIn(page: Page): Promise<void> {
|
||||
// if already in home page, return
|
||||
if (page.url().includes('/home')) {
|
||||
if (await page.url().includes('/home')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!username || !password) {
|
||||
throw new Error(
|
||||
'SIGNOZ_E2E_USERNAME and SIGNOZ_E2E_PASSWORD environment variables must be set.',
|
||||
'E2E_EMAIL and E2E_PASSWORD environment variables must be set.',
|
||||
);
|
||||
}
|
||||
|
||||
await page.goto('/login');
|
||||
await page.goto(`${baseURL}/login`);
|
||||
await page.getByTestId('email').click();
|
||||
await page.getByTestId('email').fill(username);
|
||||
await page.getByTestId('initiate_login').click();
|
||||
|
||||
@@ -4,9 +4,4 @@ FRONTEND_API_ENDPOINT="http://localhost:8080/"
|
||||
PYLON_APP_ID="pylon-app-id"
|
||||
APPCUES_APP_ID="appcess-app-id"
|
||||
|
||||
CI="1"
|
||||
|
||||
# Playwright E2E Test Configuration
|
||||
SIGNOZ_E2E_BASE_URL="your-dev-environment-url"
|
||||
SIGNOZ_E2E_USERNAME="your-email@example.com"
|
||||
SIGNOZ_E2E_PASSWORD="your-password"
|
||||
CI="1"
|
||||
@@ -18,12 +18,7 @@
|
||||
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
|
||||
"commitlint": "commitlint --edit $1",
|
||||
"test": "jest",
|
||||
"test:changedsince": "jest --changedSince=main --coverage --silent",
|
||||
"e2e": "playwright test",
|
||||
"e2e:ui": "playwright test --ui",
|
||||
"e2e:headed": "playwright test --headed",
|
||||
"e2e:debug": "playwright test --debug",
|
||||
"e2e:report": "playwright show-report"
|
||||
"test:changedsince": "jest --changedSince=main --coverage --silent"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.15.0"
|
||||
|
||||
@@ -45,7 +45,14 @@ export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
use: {
|
||||
launchOptions: { args: ['--start-maximized'] },
|
||||
viewport: null,
|
||||
colorScheme: 'dark',
|
||||
locale: 'en-US',
|
||||
baseURL: 'https://app.us.staging.signoz.cloud',
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
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;
|
||||
@@ -1,28 +0,0 @@
|
||||
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;
|
||||
@@ -1,26 +0,0 @@
|
||||
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;
|
||||
@@ -6,7 +6,9 @@ import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
export interface CreateRoutingPolicyBody {
|
||||
name: string;
|
||||
expression: string;
|
||||
channels: string[];
|
||||
actions: {
|
||||
channels: string[];
|
||||
};
|
||||
description?: string;
|
||||
}
|
||||
|
||||
@@ -21,7 +23,7 @@ const createRoutingPolicy = async (
|
||||
SuccessResponseV2<CreateRoutingPolicyResponse> | ErrorResponseV2
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.post(`/route_policies`, props);
|
||||
const response = await axios.post(`/notification-policy`, props);
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
|
||||
@@ -14,7 +14,9 @@ const deleteRoutingPolicy = async (
|
||||
SuccessResponseV2<DeleteRoutingPolicyResponse> | ErrorResponseV2
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.delete(`/route_policies/${routingPolicyId}`);
|
||||
const response = await axios.delete(
|
||||
`/notification-policy/${routingPolicyId}`,
|
||||
);
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
|
||||
@@ -25,7 +25,7 @@ export const getRoutingPolicies = async (
|
||||
headers?: Record<string, string>,
|
||||
): Promise<SuccessResponseV2<GetRoutingPoliciesResponse> | ErrorResponseV2> => {
|
||||
try {
|
||||
const response = await axios.get('/route_policies', {
|
||||
const response = await axios.get('/notification-policy', {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
|
||||
@@ -6,7 +6,9 @@ import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
export interface UpdateRoutingPolicyBody {
|
||||
name: string;
|
||||
expression: string;
|
||||
channels: string[];
|
||||
actions: {
|
||||
channels: string[];
|
||||
};
|
||||
description: string;
|
||||
}
|
||||
|
||||
@@ -22,7 +24,7 @@ const updateRoutingPolicy = async (
|
||||
SuccessResponseV2<UpdateRoutingPolicyResponse> | ErrorResponseV2
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.put(`/route_policies/${id}`, {
|
||||
const response = await axios.put(`/notification-policy/${id}`, {
|
||||
...props,
|
||||
});
|
||||
|
||||
|
||||
@@ -634,260 +634,4 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('builds payload for builder queries with filters array but no filter expression', () => {
|
||||
const props: GetQueryResultsProps = {
|
||||
query: {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
id: 'q8',
|
||||
unit: undefined,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: {
|
||||
queryData: [
|
||||
baseBuilderQuery({
|
||||
dataSource: DataSource.LOGS,
|
||||
filter: { expression: '' },
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { key: 'service.name', type: 'string' },
|
||||
op: '=',
|
||||
value: 'payment-service',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
key: { key: 'http.status_code', type: 'number' },
|
||||
op: '>=',
|
||||
value: 400,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
key: { key: 'message', type: 'string' },
|
||||
op: 'contains',
|
||||
value: 'error',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
}),
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
},
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
start,
|
||||
end,
|
||||
};
|
||||
|
||||
const result = prepareQueryRangePayloadV5(props);
|
||||
|
||||
expect(result.legendMap).toEqual({ A: 'Legend A' });
|
||||
expect(result.queryPayload.compositeQuery.queries).toHaveLength(1);
|
||||
|
||||
const builderQuery = result.queryPayload.compositeQuery.queries.find(
|
||||
(q) => q.type === 'builder_query',
|
||||
) as QueryEnvelope;
|
||||
const logSpec = builderQuery.spec as LogBuilderQuery;
|
||||
|
||||
expect(logSpec.name).toBe('A');
|
||||
expect(logSpec.signal).toBe('logs');
|
||||
expect(logSpec.filter).toEqual({
|
||||
expression:
|
||||
"service.name = 'payment-service' AND http.status_code >= 400 AND message contains 'error'",
|
||||
});
|
||||
});
|
||||
|
||||
it('uses filter.expression when only expression is provided', () => {
|
||||
const props: GetQueryResultsProps = {
|
||||
query: {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
id: 'q9',
|
||||
unit: undefined,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: {
|
||||
queryData: [
|
||||
baseBuilderQuery({
|
||||
dataSource: DataSource.LOGS,
|
||||
filter: { expression: 'http.status_code >= 500' },
|
||||
filters: (undefined as unknown) as IBuilderQuery['filters'],
|
||||
}),
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
},
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
start,
|
||||
end,
|
||||
};
|
||||
|
||||
const result = prepareQueryRangePayloadV5(props);
|
||||
const builderQuery = result.queryPayload.compositeQuery.queries.find(
|
||||
(q) => q.type === 'builder_query',
|
||||
) as QueryEnvelope;
|
||||
const logSpec = builderQuery.spec as LogBuilderQuery;
|
||||
expect(logSpec.filter).toEqual({ expression: 'http.status_code >= 500' });
|
||||
});
|
||||
|
||||
it('derives expression from filters when filter is undefined', () => {
|
||||
const props: GetQueryResultsProps = {
|
||||
query: {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
id: 'q10',
|
||||
unit: undefined,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: {
|
||||
queryData: [
|
||||
baseBuilderQuery({
|
||||
dataSource: DataSource.LOGS,
|
||||
filter: (undefined as unknown) as IBuilderQuery['filter'],
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { key: 'service.name', type: 'string' },
|
||||
op: '=',
|
||||
value: 'checkout',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
}),
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
},
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
start,
|
||||
end,
|
||||
};
|
||||
|
||||
const result = prepareQueryRangePayloadV5(props);
|
||||
const builderQuery = result.queryPayload.compositeQuery.queries.find(
|
||||
(q) => q.type === 'builder_query',
|
||||
) as QueryEnvelope;
|
||||
const logSpec = builderQuery.spec as LogBuilderQuery;
|
||||
expect(logSpec.filter).toEqual({ expression: "service.name = 'checkout'" });
|
||||
});
|
||||
|
||||
it('prefers filter.expression over filters when both are present', () => {
|
||||
const props: GetQueryResultsProps = {
|
||||
query: {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
id: 'q11',
|
||||
unit: undefined,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: {
|
||||
queryData: [
|
||||
baseBuilderQuery({
|
||||
dataSource: DataSource.LOGS,
|
||||
filter: { expression: "service.name = 'frontend'" },
|
||||
filters: {
|
||||
items: [
|
||||
{
|
||||
id: '1',
|
||||
key: { key: 'service.name', type: 'string' },
|
||||
op: '=',
|
||||
value: 'backend',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
}),
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
},
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
start,
|
||||
end,
|
||||
};
|
||||
|
||||
const result = prepareQueryRangePayloadV5(props);
|
||||
const builderQuery = result.queryPayload.compositeQuery.queries.find(
|
||||
(q) => q.type === 'builder_query',
|
||||
) as QueryEnvelope;
|
||||
const logSpec = builderQuery.spec as LogBuilderQuery;
|
||||
expect(logSpec.filter).toEqual({ expression: "service.name = 'frontend'" });
|
||||
});
|
||||
|
||||
it('returns empty expression when neither filter nor filters provided', () => {
|
||||
const props: GetQueryResultsProps = {
|
||||
query: {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
id: 'q12',
|
||||
unit: undefined,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: {
|
||||
queryData: [
|
||||
baseBuilderQuery({
|
||||
dataSource: DataSource.LOGS,
|
||||
filter: (undefined as unknown) as IBuilderQuery['filter'],
|
||||
filters: (undefined as unknown) as IBuilderQuery['filters'],
|
||||
}),
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
},
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
start,
|
||||
end,
|
||||
};
|
||||
|
||||
const result = prepareQueryRangePayloadV5(props);
|
||||
const builderQuery = result.queryPayload.compositeQuery.queries.find(
|
||||
(q) => q.type === 'builder_query',
|
||||
) as QueryEnvelope;
|
||||
const logSpec = builderQuery.spec as LogBuilderQuery;
|
||||
expect(logSpec.filter).toEqual({ expression: '' });
|
||||
});
|
||||
|
||||
it('returns empty expression when filters provided with empty items', () => {
|
||||
const props: GetQueryResultsProps = {
|
||||
query: {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
id: 'q13',
|
||||
unit: undefined,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: {
|
||||
queryData: [
|
||||
baseBuilderQuery({
|
||||
dataSource: DataSource.LOGS,
|
||||
filter: { expression: '' },
|
||||
filters: { items: [], op: 'AND' },
|
||||
}),
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
},
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
start,
|
||||
end,
|
||||
};
|
||||
|
||||
const result = prepareQueryRangePayloadV5(props);
|
||||
const builderQuery = result.queryPayload.compositeQuery.queries.find(
|
||||
(q) => q.type === 'builder_query',
|
||||
) as QueryEnvelope;
|
||||
const logSpec = builderQuery.spec as LogBuilderQuery;
|
||||
expect(logSpec.filter).toEqual({ expression: '' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
@@ -15,7 +14,6 @@ import {
|
||||
BaseBuilderQuery,
|
||||
FieldContext,
|
||||
FieldDataType,
|
||||
Filter,
|
||||
FunctionName,
|
||||
GroupByKey,
|
||||
Having,
|
||||
@@ -113,23 +111,6 @@ function isDeprecatedField(fieldName: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function getFilter(queryData: IBuilderQuery): Filter {
|
||||
const { filter } = queryData;
|
||||
if (filter?.expression) {
|
||||
return {
|
||||
expression: filter.expression,
|
||||
};
|
||||
}
|
||||
|
||||
if (queryData.filters && queryData.filters?.items?.length > 0) {
|
||||
return convertFiltersToExpression(queryData.filters);
|
||||
}
|
||||
|
||||
return {
|
||||
expression: '',
|
||||
};
|
||||
}
|
||||
|
||||
function createBaseSpec(
|
||||
queryData: IBuilderQuery,
|
||||
requestType: RequestType,
|
||||
@@ -143,7 +124,7 @@ function createBaseSpec(
|
||||
return {
|
||||
stepInterval: queryData?.stepInterval || null,
|
||||
disabled: queryData.disabled,
|
||||
filter: getFilter(queryData),
|
||||
filter: queryData?.filter?.expression ? queryData.filter : undefined,
|
||||
groupBy:
|
||||
queryData.groupBy?.length > 0
|
||||
? queryData.groupBy.map(
|
||||
|
||||
@@ -42,31 +42,18 @@ export function useNavigateToExplorer(): (
|
||||
builder: {
|
||||
...widgetQuery.builder,
|
||||
queryData: widgetQuery.builder.queryData
|
||||
.map((item) => {
|
||||
// filter out filters with unique ids
|
||||
const seen = new Set();
|
||||
const filterItems = [
|
||||
...(item.filters?.items || []),
|
||||
...selectedFilters,
|
||||
].filter((item) => {
|
||||
if (seen.has(item.id)) return false;
|
||||
seen.add(item.id);
|
||||
return true;
|
||||
});
|
||||
|
||||
return {
|
||||
...item,
|
||||
dataSource,
|
||||
aggregateOperator: MetricAggregateOperator.NOOP,
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: filterItems,
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
groupBy: [],
|
||||
disabled: false,
|
||||
};
|
||||
})
|
||||
.map((item) => ({
|
||||
...item,
|
||||
dataSource,
|
||||
aggregateOperator: MetricAggregateOperator.NOOP,
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: [...(item.filters?.items || []), ...selectedFilters],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
groupBy: [],
|
||||
disabled: false,
|
||||
}))
|
||||
.slice(0, 1),
|
||||
queryFormulas: [],
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Form, Row } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import CreateAlertV2 from 'container/CreateAlertV2';
|
||||
import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
@@ -126,16 +125,6 @@ function CreateRules(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
const showNewCreateAlertsPageFlag =
|
||||
queryParams.get('showNewCreateAlertsPage') === 'true';
|
||||
|
||||
if (
|
||||
showNewCreateAlertsPageFlag &&
|
||||
alertType !== AlertTypes.ANOMALY_BASED_ALERT
|
||||
) {
|
||||
return <CreateAlertV2 alertType={alertType} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormAlertRules
|
||||
alertType={alertType}
|
||||
|
||||
@@ -1,34 +1,21 @@
|
||||
import './styles.scss';
|
||||
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import getAllChannels from 'api/channels/getAll';
|
||||
import classNames from 'classnames';
|
||||
import { ChartLine } from 'lucide-react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { Activity, ChartLine } from 'lucide-react';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import AdvancedOptions from '../EvaluationSettings/AdvancedOptions';
|
||||
import Stepper from '../Stepper';
|
||||
import { showCondensedLayout } from '../utils';
|
||||
import AlertThreshold from './AlertThreshold';
|
||||
import AnomalyThreshold from './AnomalyThreshold';
|
||||
import { ANOMALY_TAB_TOOLTIP, THRESHOLD_TAB_TOOLTIP } from './constants';
|
||||
|
||||
function AlertCondition(): JSX.Element {
|
||||
const { alertType, setAlertType } = useCreateAlertState();
|
||||
|
||||
const {
|
||||
data,
|
||||
isLoading: isLoadingChannels,
|
||||
isError: isErrorChannels,
|
||||
refetch: refreshChannels,
|
||||
} = useQuery<SuccessResponseV2<Channels[]>, APIError>(['getChannels'], {
|
||||
queryFn: () => getAllChannels(),
|
||||
});
|
||||
const channels = data?.data || [];
|
||||
const showCondensedLayoutFlag = showCondensedLayout();
|
||||
|
||||
const showMultipleTabs =
|
||||
alertType === AlertTypes.ANOMALY_BASED_ALERT ||
|
||||
@@ -40,16 +27,15 @@ function AlertCondition(): JSX.Element {
|
||||
icon: <ChartLine size={14} data-testid="threshold-view" />,
|
||||
value: AlertTypes.METRICS_BASED_ALERT,
|
||||
},
|
||||
// Hide anomaly tab for now
|
||||
// ...(showMultipleTabs
|
||||
// ? [
|
||||
// {
|
||||
// label: 'Anomaly',
|
||||
// icon: <Activity size={14} data-testid="anomaly-view" />,
|
||||
// value: AlertTypes.ANOMALY_BASED_ALERT,
|
||||
// },
|
||||
// ]
|
||||
// : []),
|
||||
...(showMultipleTabs
|
||||
? [
|
||||
{
|
||||
label: 'Anomaly',
|
||||
icon: <Activity size={14} data-testid="anomaly-view" />,
|
||||
value: AlertTypes.ANOMALY_BASED_ALERT,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const handleAlertTypeChange = (value: AlertTypes): void => {
|
||||
@@ -90,25 +76,13 @@ function AlertCondition(): JSX.Element {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{alertType !== AlertTypes.ANOMALY_BASED_ALERT && (
|
||||
<AlertThreshold
|
||||
channels={channels}
|
||||
isLoadingChannels={isLoadingChannels}
|
||||
isErrorChannels={isErrorChannels}
|
||||
refreshChannels={refreshChannels}
|
||||
/>
|
||||
)}
|
||||
{alertType === AlertTypes.ANOMALY_BASED_ALERT && (
|
||||
<AnomalyThreshold
|
||||
channels={channels}
|
||||
isLoadingChannels={isLoadingChannels}
|
||||
isErrorChannels={isErrorChannels}
|
||||
refreshChannels={refreshChannels}
|
||||
/>
|
||||
)}
|
||||
<div className="condensed-advanced-options-container">
|
||||
<AdvancedOptions />
|
||||
</div>
|
||||
{alertType !== AlertTypes.ANOMALY_BASED_ALERT && <AlertThreshold />}
|
||||
{alertType === AlertTypes.ANOMALY_BASED_ALERT && <AnomalyThreshold />}
|
||||
{showCondensedLayoutFlag ? (
|
||||
<div className="condensed-advanced-options-container">
|
||||
<AdvancedOptions />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import './styles.scss';
|
||||
import '../EvaluationSettings/styles.scss';
|
||||
|
||||
import { Button, Select, Tooltip, Typography } from 'antd';
|
||||
import { Button, Select, Typography } from 'antd';
|
||||
import getAllChannels from 'api/channels/getAll';
|
||||
import classNames from 'classnames';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import getRandomColor from 'lib/getRandomColor';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
import { v4 } from 'uuid';
|
||||
import { useQuery } from 'react-query';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import {
|
||||
@@ -18,47 +19,34 @@ import {
|
||||
THRESHOLD_OPERATOR_OPTIONS,
|
||||
} from '../context/constants';
|
||||
import EvaluationSettings from '../EvaluationSettings/EvaluationSettings';
|
||||
import { showCondensedLayout } from '../utils';
|
||||
import ThresholdItem from './ThresholdItem';
|
||||
import { AnomalyAndThresholdProps, UpdateThreshold } from './types';
|
||||
import { UpdateThreshold } from './types';
|
||||
import {
|
||||
getCategoryByOptionId,
|
||||
getCategorySelectOptionByName,
|
||||
getMatchTypeTooltip,
|
||||
getQueryNames,
|
||||
RoutingPolicyBanner,
|
||||
} from './utils';
|
||||
|
||||
function AlertThreshold({
|
||||
channels,
|
||||
isLoadingChannels,
|
||||
isErrorChannels,
|
||||
refreshChannels,
|
||||
}: AnomalyAndThresholdProps): JSX.Element {
|
||||
function AlertThreshold(): JSX.Element {
|
||||
const {
|
||||
alertState,
|
||||
thresholdState,
|
||||
setThresholdState,
|
||||
notificationSettings,
|
||||
setNotificationSettings,
|
||||
} = useCreateAlertState();
|
||||
const { data, isLoading: isLoadingChannels } = useQuery<
|
||||
SuccessResponseV2<Channels[]>,
|
||||
APIError
|
||||
>(['getChannels'], {
|
||||
queryFn: () => getAllChannels(),
|
||||
});
|
||||
const showCondensedLayoutFlag = showCondensedLayout();
|
||||
const channels = data?.data || [];
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const queryNames = getQueryNames(currentQuery);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
queryNames.length > 0 &&
|
||||
!queryNames.some((query) => query.value === thresholdState.selectedQuery)
|
||||
) {
|
||||
setThresholdState({
|
||||
type: 'SET_SELECTED_QUERY',
|
||||
payload: queryNames[0].value,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [queryNames, thresholdState.selectedQuery]);
|
||||
|
||||
const selectedCategory = getCategoryByOptionId(alertState.yAxisUnit || '');
|
||||
const categorySelectOptions = getCategorySelectOptionByName(
|
||||
selectedCategory || '',
|
||||
@@ -67,15 +55,11 @@ function AlertThreshold({
|
||||
const addThreshold = (): void => {
|
||||
let newThreshold;
|
||||
if (thresholdState.thresholds.length === 1) {
|
||||
newThreshold = { ...INITIAL_WARNING_THRESHOLD, id: v4() };
|
||||
newThreshold = INITIAL_WARNING_THRESHOLD;
|
||||
} else if (thresholdState.thresholds.length === 2) {
|
||||
newThreshold = { ...INITIAL_INFO_THRESHOLD, id: v4() };
|
||||
newThreshold = INITIAL_INFO_THRESHOLD;
|
||||
} else {
|
||||
newThreshold = {
|
||||
...INITIAL_RANDOM_THRESHOLD,
|
||||
id: v4(),
|
||||
color: getRandomColor(),
|
||||
};
|
||||
newThreshold = INITIAL_RANDOM_THRESHOLD;
|
||||
}
|
||||
setThresholdState({
|
||||
type: 'SET_THRESHOLDS',
|
||||
@@ -101,71 +85,17 @@ function AlertThreshold({
|
||||
});
|
||||
};
|
||||
|
||||
const onTooltipOpenChange = (open: boolean): void => {
|
||||
// Stop propagation of click events on tooltip text to dropdown
|
||||
if (open) {
|
||||
setTimeout(() => {
|
||||
const tooltipElement = document.querySelector(
|
||||
'.copyable-tooltip .ant-tooltip-inner',
|
||||
);
|
||||
if (tooltipElement) {
|
||||
tooltipElement.addEventListener(
|
||||
'click',
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
true,
|
||||
);
|
||||
tooltipElement.addEventListener(
|
||||
'mousedown',
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
true,
|
||||
);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const matchTypeOptionsWithTooltips = THRESHOLD_MATCH_TYPE_OPTIONS.map(
|
||||
(option) => ({
|
||||
...option,
|
||||
label: (
|
||||
<Tooltip
|
||||
title={getMatchTypeTooltip(option.value, thresholdState.operator)}
|
||||
placement="left"
|
||||
overlayClassName="copyable-tooltip"
|
||||
overlayStyle={{
|
||||
maxWidth: '450px',
|
||||
minWidth: '400px',
|
||||
}}
|
||||
overlayInnerStyle={{
|
||||
padding: '12px 16px',
|
||||
userSelect: 'text',
|
||||
WebkitUserSelect: 'text',
|
||||
MozUserSelect: 'text',
|
||||
msUserSelect: 'text',
|
||||
}}
|
||||
mouseEnterDelay={0.2}
|
||||
trigger={['hover', 'click']}
|
||||
destroyTooltipOnHide={false}
|
||||
onOpenChange={onTooltipOpenChange}
|
||||
>
|
||||
<span style={{ display: 'block', width: '100%' }}>{option.label}</span>
|
||||
</Tooltip>
|
||||
),
|
||||
}),
|
||||
const evaluationWindowContext = showCondensedLayoutFlag ? (
|
||||
<EvaluationSettings />
|
||||
) : (
|
||||
<strong>Evaluation Window.</strong>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'alert-threshold-container',
|
||||
'condensed-alert-threshold-container',
|
||||
)}
|
||||
className={classNames('alert-threshold-container', {
|
||||
'condensed-alert-threshold-container': showCondensedLayoutFlag,
|
||||
})}
|
||||
>
|
||||
{/* Main condition sentence */}
|
||||
<div className="alert-condition-sentences">
|
||||
@@ -184,7 +114,8 @@ function AlertThreshold({
|
||||
style={{ width: 80 }}
|
||||
options={queryNames}
|
||||
/>
|
||||
<Typography.Text className="sentence-text">is</Typography.Text>
|
||||
</div>
|
||||
<div className="alert-condition-sentence">
|
||||
<Select
|
||||
value={thresholdState.operator}
|
||||
onChange={(value): void => {
|
||||
@@ -193,7 +124,7 @@ function AlertThreshold({
|
||||
payload: value,
|
||||
});
|
||||
}}
|
||||
style={{ width: 180 }}
|
||||
style={{ width: 120 }}
|
||||
options={THRESHOLD_OPERATOR_OPTIONS}
|
||||
/>
|
||||
<Typography.Text className="sentence-text">
|
||||
@@ -207,11 +138,11 @@ function AlertThreshold({
|
||||
payload: value,
|
||||
});
|
||||
}}
|
||||
style={{ width: 180 }}
|
||||
options={matchTypeOptionsWithTooltips}
|
||||
style={{ width: 140 }}
|
||||
options={THRESHOLD_MATCH_TYPE_OPTIONS}
|
||||
/>
|
||||
<Typography.Text className="sentence-text">
|
||||
during the <EvaluationSettings />
|
||||
during the {evaluationWindowContext}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
@@ -227,8 +158,6 @@ function AlertThreshold({
|
||||
channels={channels}
|
||||
isLoadingChannels={isLoadingChannels}
|
||||
units={categorySelectOptions}
|
||||
isErrorChannels={isErrorChannels}
|
||||
refreshChannels={refreshChannels}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
@@ -240,11 +169,6 @@ function AlertThreshold({
|
||||
Add Threshold
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<RoutingPolicyBanner
|
||||
notificationSettings={notificationSettings}
|
||||
setNotificationSettings={setNotificationSettings}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Select, Typography } from 'antd';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
@@ -11,26 +10,10 @@ import {
|
||||
ANOMALY_THRESHOLD_OPERATOR_OPTIONS,
|
||||
ANOMALY_TIME_DURATION_OPTIONS,
|
||||
} from '../context/constants';
|
||||
import { AnomalyAndThresholdProps } from './types';
|
||||
import {
|
||||
getQueryNames,
|
||||
NotificationChannelsNotFoundContent,
|
||||
RoutingPolicyBanner,
|
||||
} from './utils';
|
||||
import { getQueryNames } from './utils';
|
||||
|
||||
function AnomalyThreshold({
|
||||
channels,
|
||||
isLoadingChannels,
|
||||
isErrorChannels,
|
||||
refreshChannels,
|
||||
}: AnomalyAndThresholdProps): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const {
|
||||
thresholdState,
|
||||
setThresholdState,
|
||||
notificationSettings,
|
||||
setNotificationSettings,
|
||||
} = useCreateAlertState();
|
||||
function AnomalyThreshold(): JSX.Element {
|
||||
const { thresholdState, setThresholdState } = useCreateAlertState();
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
@@ -44,11 +27,7 @@ function AnomalyThreshold({
|
||||
return options;
|
||||
}, []);
|
||||
|
||||
const updateThreshold = (
|
||||
id: string,
|
||||
field: string,
|
||||
value: string | string[],
|
||||
): void => {
|
||||
const updateThreshold = (id: string, field: string, value: string): void => {
|
||||
setThresholdState({
|
||||
type: 'SET_THRESHOLDS',
|
||||
payload: thresholdState.thresholds.map((t) =>
|
||||
@@ -74,6 +53,7 @@ function AnomalyThreshold({
|
||||
payload: value,
|
||||
});
|
||||
}}
|
||||
style={{ width: 80 }}
|
||||
options={queryNames}
|
||||
/>
|
||||
<Typography.Text
|
||||
@@ -91,11 +71,12 @@ function AnomalyThreshold({
|
||||
payload: value,
|
||||
});
|
||||
}}
|
||||
style={{ width: 80 }}
|
||||
options={ANOMALY_TIME_DURATION_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
{/* Sentence 2 */}
|
||||
<div className="alert-condition-sentence">
|
||||
{/* Sentence 2 */}
|
||||
<Typography.Text data-testid="threshold-text" className="sentence-text">
|
||||
is
|
||||
</Typography.Text>
|
||||
@@ -109,6 +90,7 @@ function AnomalyThreshold({
|
||||
value.toString(),
|
||||
);
|
||||
}}
|
||||
style={{ width: 80 }}
|
||||
options={deviationOptions}
|
||||
/>
|
||||
<Typography.Text data-testid="deviations-text" className="sentence-text">
|
||||
@@ -123,6 +105,7 @@ function AnomalyThreshold({
|
||||
payload: value,
|
||||
});
|
||||
}}
|
||||
style={{ width: 80 }}
|
||||
options={ANOMALY_THRESHOLD_OPERATOR_OPTIONS}
|
||||
/>
|
||||
<Typography.Text
|
||||
@@ -140,6 +123,7 @@ function AnomalyThreshold({
|
||||
payload: value,
|
||||
});
|
||||
}}
|
||||
style={{ width: 80 }}
|
||||
options={ANOMALY_THRESHOLD_MATCH_TYPE_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
@@ -157,6 +141,7 @@ function AnomalyThreshold({
|
||||
payload: value,
|
||||
});
|
||||
}}
|
||||
style={{ width: 80 }}
|
||||
options={ANOMALY_ALGORITHM_OPTIONS}
|
||||
/>
|
||||
<Typography.Text
|
||||
@@ -174,58 +159,14 @@ function AnomalyThreshold({
|
||||
payload: value,
|
||||
});
|
||||
}}
|
||||
style={{ width: 80 }}
|
||||
options={ANOMALY_SEASONALITY_OPTIONS}
|
||||
/>
|
||||
{notificationSettings.routingPolicies ? (
|
||||
<>
|
||||
<Typography.Text
|
||||
data-testid="seasonality-text"
|
||||
className="sentence-text"
|
||||
>
|
||||
seasonality to
|
||||
</Typography.Text>
|
||||
<Select
|
||||
value={thresholdState.thresholds[0].channels}
|
||||
onChange={(value): void =>
|
||||
updateThreshold(thresholdState.thresholds[0].id, 'channels', value)
|
||||
}
|
||||
style={{ width: 350 }}
|
||||
options={channels.map((channel) => ({
|
||||
value: channel.id,
|
||||
label: channel.name,
|
||||
}))}
|
||||
mode="multiple"
|
||||
placeholder="Select notification channels"
|
||||
showSearch
|
||||
maxTagCount={2}
|
||||
maxTagPlaceholder={(omittedValues): string =>
|
||||
`+${omittedValues.length} more`
|
||||
}
|
||||
maxTagTextLength={10}
|
||||
filterOption={(input, option): boolean =>
|
||||
option?.label?.toLowerCase().includes(input.toLowerCase()) || false
|
||||
}
|
||||
status={isErrorChannels ? 'error' : undefined}
|
||||
disabled={isLoadingChannels}
|
||||
notFoundContent={
|
||||
<NotificationChannelsNotFoundContent
|
||||
user={user}
|
||||
refreshChannels={refreshChannels}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Typography.Text data-testid="seasonality-text" className="sentence-text">
|
||||
seasonality
|
||||
</Typography.Text>
|
||||
)}
|
||||
<Typography.Text data-testid="seasonality-text" className="sentence-text">
|
||||
seasonality
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<RoutingPolicyBanner
|
||||
notificationSettings={notificationSettings}
|
||||
setNotificationSettings={setNotificationSettings}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { Button, Input, Select, Tooltip, Typography } from 'antd';
|
||||
import { CircleX, Trash } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { Button, Input, Select, Space, Tooltip, Typography } from 'antd';
|
||||
import { ChartLine, CircleX } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import { AlertThresholdOperator } from '../context/types';
|
||||
import { ThresholdItemProps } from './types';
|
||||
import { NotificationChannelsNotFoundContent } from './utils';
|
||||
|
||||
function ThresholdItem({
|
||||
threshold,
|
||||
@@ -15,12 +11,7 @@ function ThresholdItem({
|
||||
showRemoveButton,
|
||||
channels,
|
||||
units,
|
||||
isErrorChannels,
|
||||
refreshChannels,
|
||||
isLoadingChannels,
|
||||
}: ThresholdItemProps): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const { thresholdState, notificationSettings } = useCreateAlertState();
|
||||
const [showRecoveryThreshold, setShowRecoveryThreshold] = useState(false);
|
||||
|
||||
const yAxisUnitSelect = useMemo(() => {
|
||||
@@ -54,31 +45,6 @@ function ThresholdItem({
|
||||
return component;
|
||||
}, [units, threshold.unit, updateThreshold, threshold.id]);
|
||||
|
||||
const getOperatorSymbol = (): string => {
|
||||
switch (thresholdState.operator) {
|
||||
case AlertThresholdOperator.IS_ABOVE:
|
||||
return '>';
|
||||
case AlertThresholdOperator.IS_BELOW:
|
||||
return '<';
|
||||
case AlertThresholdOperator.IS_EQUAL_TO:
|
||||
return '=';
|
||||
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
|
||||
return '!=';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// const addRecoveryThreshold = (): void => {
|
||||
// setShowRecoveryThreshold(true);
|
||||
// updateThreshold(threshold.id, 'recoveryThresholdValue', 0);
|
||||
// };
|
||||
|
||||
const removeRecoveryThreshold = (): void => {
|
||||
setShowRecoveryThreshold(false);
|
||||
updateThreshold(threshold.id, 'recoveryThresholdValue', null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={threshold.id} className="threshold-item">
|
||||
<div className="threshold-row">
|
||||
@@ -88,111 +54,80 @@ function ThresholdItem({
|
||||
style={{ backgroundColor: threshold.color }}
|
||||
/>
|
||||
</div>
|
||||
<div className="threshold-controls">
|
||||
<Input
|
||||
placeholder="Enter threshold name"
|
||||
value={threshold.label}
|
||||
onChange={(e): void =>
|
||||
updateThreshold(threshold.id, 'label', e.target.value)
|
||||
}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
<Typography.Text className="sentence-text">on value</Typography.Text>
|
||||
<Typography.Text className="sentence-text highlighted-text">
|
||||
{getOperatorSymbol()}
|
||||
</Typography.Text>
|
||||
<Input
|
||||
placeholder="Enter threshold value"
|
||||
value={threshold.thresholdValue}
|
||||
onChange={(e): void =>
|
||||
updateThreshold(threshold.id, 'thresholdValue', e.target.value)
|
||||
}
|
||||
style={{ width: 100 }}
|
||||
type="number"
|
||||
/>
|
||||
{yAxisUnitSelect}
|
||||
{!notificationSettings.routingPolicies && (
|
||||
<>
|
||||
<Typography.Text className="sentence-text">send to</Typography.Text>
|
||||
<Select
|
||||
value={threshold.channels}
|
||||
onChange={(value): void =>
|
||||
updateThreshold(threshold.id, 'channels', value)
|
||||
}
|
||||
style={{ width: 350 }}
|
||||
options={channels.map((channel) => ({
|
||||
value: channel.name,
|
||||
label: channel.name,
|
||||
}))}
|
||||
mode="multiple"
|
||||
placeholder="Select notification channels"
|
||||
showSearch
|
||||
maxTagCount={2}
|
||||
maxTagPlaceholder={(omittedValues): string =>
|
||||
`+${omittedValues.length} more`
|
||||
}
|
||||
maxTagTextLength={10}
|
||||
filterOption={(input, option): boolean =>
|
||||
option?.label?.toLowerCase().includes(input.toLowerCase()) || false
|
||||
}
|
||||
status={isErrorChannels ? 'error' : undefined}
|
||||
disabled={isLoadingChannels}
|
||||
notFoundContent={
|
||||
<NotificationChannelsNotFoundContent
|
||||
user={user}
|
||||
refreshChannels={refreshChannels}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{showRecoveryThreshold && (
|
||||
<>
|
||||
<Typography.Text className="sentence-text">recover on</Typography.Text>
|
||||
<Space className="threshold-controls">
|
||||
<div className="threshold-inputs">
|
||||
<Input.Group>
|
||||
<Input
|
||||
placeholder="Enter recovery threshold value"
|
||||
value={threshold.recoveryThresholdValue ?? ''}
|
||||
placeholder="Enter threshold name"
|
||||
value={threshold.label}
|
||||
onChange={(e): void =>
|
||||
updateThreshold(threshold.id, 'recoveryThresholdValue', e.target.value)
|
||||
updateThreshold(threshold.id, 'label', e.target.value)
|
||||
}
|
||||
style={{ width: 100 }}
|
||||
type="number"
|
||||
style={{ width: 260 }}
|
||||
/>
|
||||
<Tooltip title="Remove recovery threshold">
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Trash size={16} />}
|
||||
onClick={removeRecoveryThreshold}
|
||||
className="icon-btn"
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
<Input
|
||||
placeholder="Enter threshold value"
|
||||
value={threshold.thresholdValue}
|
||||
onChange={(e): void =>
|
||||
updateThreshold(threshold.id, 'thresholdValue', e.target.value)
|
||||
}
|
||||
style={{ width: 210 }}
|
||||
/>
|
||||
{yAxisUnitSelect}
|
||||
</Input.Group>
|
||||
</div>
|
||||
<Typography.Text className="sentence-text">to</Typography.Text>
|
||||
<Select
|
||||
value={threshold.channels}
|
||||
onChange={(value): void =>
|
||||
updateThreshold(threshold.id, 'channels', value)
|
||||
}
|
||||
style={{ width: 260 }}
|
||||
options={channels.map((channel) => ({
|
||||
value: channel.id,
|
||||
label: channel.name,
|
||||
}))}
|
||||
mode="multiple"
|
||||
placeholder="Select notification channels"
|
||||
/>
|
||||
<Button.Group>
|
||||
{/* TODO: Add recovery threshold back once the functionality is implemented */}
|
||||
{/* {!showRecoveryThreshold && (
|
||||
<Tooltip title="Add recovery threshold">
|
||||
<Button
|
||||
type="default"
|
||||
icon={<ChartLine size={16} />}
|
||||
className="icon-btn"
|
||||
onClick={addRecoveryThreshold}
|
||||
/>
|
||||
</Tooltip>
|
||||
)} */}
|
||||
{!showRecoveryThreshold && (
|
||||
<Button
|
||||
type="default"
|
||||
icon={<ChartLine size={16} />}
|
||||
className="icon-btn"
|
||||
onClick={(): void => setShowRecoveryThreshold(true)}
|
||||
/>
|
||||
)}
|
||||
{showRemoveButton && (
|
||||
<Tooltip title="Remove threshold">
|
||||
<Button
|
||||
type="default"
|
||||
icon={<CircleX size={16} />}
|
||||
onClick={(): void => removeThreshold(threshold.id)}
|
||||
className="icon-btn"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<CircleX size={16} />}
|
||||
onClick={(): void => removeThreshold(threshold.id)}
|
||||
className="icon-btn"
|
||||
/>
|
||||
)}
|
||||
</Button.Group>
|
||||
</div>
|
||||
</Space>
|
||||
</div>
|
||||
{showRecoveryThreshold && (
|
||||
<Input.Group className="recovery-threshold-input-group">
|
||||
<Input
|
||||
placeholder="Recovery threshold"
|
||||
disabled
|
||||
style={{ width: 260 }}
|
||||
className="recovery-threshold-label"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Enter recovery threshold value"
|
||||
value={threshold.recoveryThresholdValue}
|
||||
onChange={(e): void =>
|
||||
updateThreshold(threshold.id, 'recoveryThresholdValue', e.target.value)
|
||||
}
|
||||
style={{ width: 210 }}
|
||||
/>
|
||||
</Input.Group>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import { CreateAlertProvider } from '../../context';
|
||||
import AlertCondition from '../AlertCondition';
|
||||
@@ -106,7 +105,7 @@ const renderAlertCondition = (
|
||||
return render(
|
||||
<MemoryRouter initialEntries={initialEntries}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||
<CreateAlertProvider>
|
||||
<AlertCondition />
|
||||
</CreateAlertProvider>
|
||||
</QueryClientProvider>
|
||||
@@ -127,10 +126,9 @@ describe('AlertCondition', () => {
|
||||
|
||||
// Verify default alertType is METRICS_BASED_ALERT (shows AlertThreshold component)
|
||||
expect(screen.getByTestId(ALERT_THRESHOLD_TEST_ID)).toBeInTheDocument();
|
||||
// TODO: uncomment this when anomaly tab is implemented
|
||||
// expect(
|
||||
// screen.queryByTestId(ANOMALY_THRESHOLD_TEST_ID),
|
||||
// ).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId(ANOMALY_THRESHOLD_TEST_ID),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Verify threshold tab is active by default
|
||||
const thresholdTab = screen.getByText(THRESHOLD_TAB_TEXT);
|
||||
@@ -138,8 +136,7 @@ describe('AlertCondition', () => {
|
||||
|
||||
// Verify both tabs are visible (METRICS_BASED_ALERT supports multiple tabs)
|
||||
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
|
||||
// TODO: uncomment this when anomaly tab is implemented
|
||||
// expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders threshold tab by default', () => {
|
||||
@@ -154,8 +151,7 @@ describe('AlertCondition', () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// TODO: Unskip this when anomaly tab is implemented
|
||||
it.skip('renders anomaly tab when alert type supports multiple tabs', () => {
|
||||
it('renders anomaly tab when alert type supports multiple tabs', () => {
|
||||
renderAlertCondition();
|
||||
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
@@ -169,8 +165,7 @@ describe('AlertCondition', () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// TODO: Unskip this when anomaly tab is implemented
|
||||
it.skip('shows AnomalyThreshold component when alert type is anomaly based', () => {
|
||||
it('shows AnomalyThreshold component when alert type is anomaly based', () => {
|
||||
renderAlertCondition();
|
||||
|
||||
// Click on anomaly tab to switch to anomaly-based alert
|
||||
@@ -181,8 +176,7 @@ describe('AlertCondition', () => {
|
||||
expect(screen.queryByTestId(ALERT_THRESHOLD_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// TODO: Unskip this when anomaly tab is implemented
|
||||
it.skip('switches between threshold and anomaly tabs', () => {
|
||||
it('switches between threshold and anomaly tabs', () => {
|
||||
renderAlertCondition();
|
||||
|
||||
// Initially shows threshold component
|
||||
@@ -207,8 +201,7 @@ describe('AlertCondition', () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// TODO: Unskip this when anomaly tab is implemented
|
||||
it.skip('applies active tab styling correctly', () => {
|
||||
it('applies active tab styling correctly', () => {
|
||||
renderAlertCondition();
|
||||
|
||||
const thresholdTab = screen.getByText(THRESHOLD_TAB_TEXT);
|
||||
@@ -229,21 +222,21 @@ describe('AlertCondition', () => {
|
||||
it('shows multiple tabs for METRICS_BASED_ALERT', () => {
|
||||
renderAlertCondition('METRIC_BASED_ALERT');
|
||||
|
||||
// TODO: uncomment this when anomaly tab is implemented
|
||||
// Both tabs should be visible
|
||||
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
|
||||
// expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(THRESHOLD_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
// expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows multiple tabs for ANOMALY_BASED_ALERT', () => {
|
||||
renderAlertCondition('ANOMALY_BASED_ALERT');
|
||||
|
||||
// Both tabs should be visible
|
||||
expect(screen.getByText(THRESHOLD_TAB_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(THRESHOLD_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
// TODO: uncomment this when anomaly tab is implemented
|
||||
// expect(screen.getByText(ANOMALY_TAB_TEXT)).toBeInTheDocument();
|
||||
// expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
expect(screen.getByTestId(ANOMALY_VIEW_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows only threshold tab for LOGS_BASED_ALERT', () => {
|
||||
|
||||
@@ -3,23 +3,11 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
|
||||
import { CreateAlertProvider } from '../../context';
|
||||
import AlertThreshold from '../AlertThreshold';
|
||||
|
||||
const mockChannels: Channels[] = [];
|
||||
const mockRefreshChannels = jest.fn();
|
||||
const mockIsLoadingChannels = false;
|
||||
const mockIsErrorChannels = false;
|
||||
const mockProps = {
|
||||
channels: mockChannels,
|
||||
isLoadingChannels: mockIsLoadingChannels,
|
||||
isErrorChannels: mockIsErrorChannels,
|
||||
refreshChannels: mockRefreshChannels,
|
||||
};
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
@@ -108,14 +96,10 @@ jest.mock('container/NewWidget/RightContainer/alertFomatCategories', () => ({
|
||||
]),
|
||||
}));
|
||||
|
||||
jest.mock('container/CreateAlertV2/utils', () => ({
|
||||
...jest.requireActual('container/CreateAlertV2/utils'),
|
||||
}));
|
||||
|
||||
const TEST_STRINGS = {
|
||||
ADD_THRESHOLD: 'Add Threshold',
|
||||
AT_LEAST_ONCE: 'AT LEAST ONCE',
|
||||
IS_ABOVE: 'ABOVE',
|
||||
IS_ABOVE: 'IS ABOVE',
|
||||
} as const;
|
||||
|
||||
const createTestQueryClient = (): QueryClient =>
|
||||
@@ -132,8 +116,8 @@ const renderAlertThreshold = (): ReturnType<typeof render> => {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||
<AlertThreshold {...mockProps} />
|
||||
<CreateAlertProvider>
|
||||
<AlertThreshold />
|
||||
</CreateAlertProvider>
|
||||
</QueryClientProvider>
|
||||
</MemoryRouter>,
|
||||
@@ -141,10 +125,7 @@ const renderAlertThreshold = (): ReturnType<typeof render> => {
|
||||
};
|
||||
|
||||
const verifySelectRenders = (title: string): void => {
|
||||
let select = screen.queryByTitle(title);
|
||||
if (!select) {
|
||||
select = screen.getByText(title);
|
||||
}
|
||||
const select = screen.getByTitle(title);
|
||||
expect(select).toBeInTheDocument();
|
||||
};
|
||||
|
||||
@@ -158,9 +139,7 @@ describe('AlertThreshold', () => {
|
||||
expect(screen.getByText('Send a notification when')).toBeInTheDocument();
|
||||
expect(screen.getByText('the threshold(s)')).toBeInTheDocument();
|
||||
expect(screen.getByText('during the')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId('condensed-evaluation-settings-container'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Evaluation Window.')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders query selection dropdown', async () => {
|
||||
@@ -210,11 +189,11 @@ describe('AlertThreshold', () => {
|
||||
|
||||
// First addition should add WARNING threshold
|
||||
fireEvent.click(addButton);
|
||||
expect(screen.getByText('warning')).toBeInTheDocument();
|
||||
expect(screen.getByText('WARNING')).toBeInTheDocument();
|
||||
|
||||
// Second addition should add INFO threshold
|
||||
fireEvent.click(addButton);
|
||||
expect(screen.getByText('info')).toBeInTheDocument();
|
||||
expect(screen.getByText('INFO')).toBeInTheDocument();
|
||||
|
||||
// Third addition should add random threshold
|
||||
fireEvent.click(addButton);
|
||||
@@ -286,7 +265,7 @@ describe('AlertThreshold', () => {
|
||||
renderAlertThreshold();
|
||||
|
||||
// Should have initial critical threshold
|
||||
expect(screen.getByText('critical')).toBeInTheDocument();
|
||||
expect(screen.getByText('CRITICAL')).toBeInTheDocument();
|
||||
verifySelectRenders(TEST_STRINGS.IS_ABOVE);
|
||||
verifySelectRenders(TEST_STRINGS.AT_LEAST_ONCE);
|
||||
});
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
|
||||
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
|
||||
import * as appHooks from 'providers/App/App';
|
||||
import {
|
||||
INITIAL_ALERT_STATE,
|
||||
INITIAL_ALERT_THRESHOLD_STATE,
|
||||
} from 'container/CreateAlertV2/context/constants';
|
||||
|
||||
import * as context from '../../context';
|
||||
import AnomalyThreshold from '../AnomalyThreshold';
|
||||
|
||||
jest.spyOn(appHooks, 'useAppContext').mockReturnValue(getAppContextMockState());
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
@@ -24,12 +23,12 @@ jest.mock('uplot', () => {
|
||||
|
||||
const mockSetAlertState = jest.fn();
|
||||
const mockSetThresholdState = jest.fn();
|
||||
jest.spyOn(context, 'useCreateAlertState').mockReturnValue(
|
||||
createMockAlertContextState({
|
||||
setThresholdState: mockSetThresholdState,
|
||||
setAlertState: mockSetAlertState,
|
||||
}),
|
||||
);
|
||||
jest.spyOn(context, 'useCreateAlertState').mockReturnValue({
|
||||
alertState: INITIAL_ALERT_STATE,
|
||||
setAlertState: mockSetAlertState,
|
||||
thresholdState: INITIAL_ALERT_THRESHOLD_STATE,
|
||||
setThresholdState: mockSetThresholdState,
|
||||
} as any);
|
||||
|
||||
// Mock useQueryBuilder hook
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
@@ -55,14 +54,7 @@ jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
}));
|
||||
|
||||
const renderAnomalyThreshold = (): ReturnType<typeof render> =>
|
||||
render(
|
||||
<AnomalyThreshold
|
||||
channels={[]}
|
||||
isLoadingChannels={false}
|
||||
isErrorChannels={false}
|
||||
refreshChannels={jest.fn()}
|
||||
/>,
|
||||
);
|
||||
render(<AnomalyThreshold />);
|
||||
|
||||
describe('AnomalyThreshold', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -2,37 +2,15 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
|
||||
import { getAppContextMockState } from 'container/RoutingPolicies/__tests__/testUtils';
|
||||
import * as appHooks from 'providers/App/App';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
|
||||
import * as context from '../../context';
|
||||
import ThresholdItem from '../ThresholdItem';
|
||||
import { ThresholdItemProps } from '../types';
|
||||
|
||||
jest.spyOn(appHooks, 'useAppContext').mockReturnValue(getAppContextMockState());
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock: any = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
uplotMock.paths = paths;
|
||||
return uplotMock;
|
||||
});
|
||||
|
||||
const mockSetAlertState = jest.fn();
|
||||
const mockSetThresholdState = jest.fn();
|
||||
jest.spyOn(context, 'useCreateAlertState').mockReturnValue(
|
||||
createMockAlertContextState({
|
||||
setThresholdState: mockSetThresholdState,
|
||||
setAlertState: mockSetAlertState,
|
||||
}),
|
||||
);
|
||||
// Mock the enableRecoveryThreshold utility
|
||||
jest.mock('../../utils', () => ({
|
||||
enableRecoveryThreshold: jest.fn(() => true),
|
||||
}));
|
||||
|
||||
const TEST_CONSTANTS = {
|
||||
THRESHOLD_ID: 'test-threshold-1',
|
||||
@@ -43,7 +21,6 @@ const TEST_CONSTANTS = {
|
||||
CHANNEL_2: 'channel-2',
|
||||
CHANNEL_3: 'channel-3',
|
||||
EMAIL_CHANNEL_NAME: 'Email Channel',
|
||||
EMAIL_CHANNEL_TRUNCATED: 'Email Chan...',
|
||||
ENTER_THRESHOLD_NAME: 'Enter threshold name',
|
||||
ENTER_THRESHOLD_VALUE: 'Enter threshold value',
|
||||
ENTER_RECOVERY_THRESHOLD_VALUE: 'Enter recovery threshold value',
|
||||
@@ -82,8 +59,6 @@ const defaultProps: ThresholdItemProps = {
|
||||
channels: mockChannels,
|
||||
isLoadingChannels: false,
|
||||
units: mockUnits,
|
||||
isErrorChannels: false,
|
||||
refreshChannels: jest.fn(),
|
||||
};
|
||||
|
||||
const renderThresholdItem = (
|
||||
@@ -102,11 +77,10 @@ const verifySelectorWidth = (
|
||||
expect(selector.closest('.ant-select')).toHaveStyle(`width: ${expectedWidth}`);
|
||||
};
|
||||
|
||||
// TODO: Unskip this when recovery threshold is implemented
|
||||
// const showRecoveryThreshold = (): void => {
|
||||
// const recoveryButton = screen.getByRole('button', { name: '' });
|
||||
// fireEvent.click(recoveryButton);
|
||||
// };
|
||||
const showRecoveryThreshold = (): void => {
|
||||
const recoveryButton = screen.getByRole('button', { name: '' });
|
||||
fireEvent.click(recoveryButton);
|
||||
};
|
||||
|
||||
const verifyComponentRendersWithLoading = (): void => {
|
||||
expect(
|
||||
@@ -148,7 +122,7 @@ describe('ThresholdItem', () => {
|
||||
const valueInput = screen.getByPlaceholderText(
|
||||
TEST_CONSTANTS.ENTER_THRESHOLD_VALUE,
|
||||
);
|
||||
expect(valueInput).toHaveValue(100);
|
||||
expect(valueInput).toHaveValue('100');
|
||||
});
|
||||
|
||||
it('renders unit selector with correct value', () => {
|
||||
@@ -158,6 +132,15 @@ describe('ThresholdItem', () => {
|
||||
expect(screen.getByText('Bytes')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders channels selector with correct value', () => {
|
||||
renderThresholdItem();
|
||||
|
||||
// Check for the channels selector by looking for the displayed text
|
||||
expect(
|
||||
screen.getByText(TEST_CONSTANTS.EMAIL_CHANNEL_NAME),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('updates threshold label when label input changes', () => {
|
||||
const updateThreshold = jest.fn();
|
||||
renderThresholdItem({ updateThreshold });
|
||||
@@ -229,31 +212,38 @@ describe('ThresholdItem', () => {
|
||||
|
||||
// The remove button is the second button (with circle-x icon)
|
||||
const buttons = screen.getAllByRole('button');
|
||||
expect(buttons).toHaveLength(1); // remove button
|
||||
expect(buttons).toHaveLength(2); // Recovery button + remove button
|
||||
});
|
||||
|
||||
it('does not show remove button when showRemoveButton is false', () => {
|
||||
renderThresholdItem({ showRemoveButton: false });
|
||||
|
||||
// No buttons should be present
|
||||
const buttons = screen.queryAllByRole('button');
|
||||
expect(buttons).toHaveLength(0);
|
||||
// Only the recovery button should be present
|
||||
const buttons = screen.getAllByRole('button');
|
||||
expect(buttons).toHaveLength(1); // Only recovery button
|
||||
});
|
||||
|
||||
it('calls removeThreshold when remove button is clicked', () => {
|
||||
const removeThreshold = jest.fn();
|
||||
renderThresholdItem({ showRemoveButton: true, removeThreshold });
|
||||
|
||||
// The remove button is the first button (with circle-x icon)
|
||||
// The remove button is the second button (with circle-x icon)
|
||||
const buttons = screen.getAllByRole('button');
|
||||
const removeButton = buttons[0];
|
||||
const removeButton = buttons[1]; // Second button is the remove button
|
||||
fireEvent.click(removeButton);
|
||||
|
||||
expect(removeThreshold).toHaveBeenCalledWith(TEST_CONSTANTS.THRESHOLD_ID);
|
||||
});
|
||||
|
||||
// TODO: Unskip this when recovery threshold is implemented
|
||||
it.skip('shows recovery threshold inputs when recovery button is clicked', () => {
|
||||
it('shows recovery threshold button when recovery threshold is enabled', () => {
|
||||
renderThresholdItem();
|
||||
|
||||
// The recovery button is the first button (with chart-line icon)
|
||||
const buttons = screen.getAllByRole('button');
|
||||
expect(buttons).toHaveLength(1); // Recovery button
|
||||
});
|
||||
|
||||
it('shows recovery threshold inputs when recovery button is clicked', () => {
|
||||
renderThresholdItem();
|
||||
|
||||
// The recovery button is the first button (with chart-line icon)
|
||||
@@ -261,16 +251,13 @@ describe('ThresholdItem', () => {
|
||||
const recoveryButton = buttons[0]; // First button is the recovery button
|
||||
fireEvent.click(recoveryButton);
|
||||
|
||||
expect(
|
||||
screen.getByPlaceholderText('Enter recovery threshold value'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Recovery threshold')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByPlaceholderText(TEST_CONSTANTS.ENTER_RECOVERY_THRESHOLD_VALUE),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// TODO: Unskip this when recovery threshold is implemented
|
||||
it.skip('updates recovery threshold value when input changes', () => {
|
||||
it('updates recovery threshold value when input changes', () => {
|
||||
const updateThreshold = jest.fn();
|
||||
renderThresholdItem({ updateThreshold });
|
||||
|
||||
@@ -303,6 +290,22 @@ describe('ThresholdItem', () => {
|
||||
verifyUnitSelectorDisabled();
|
||||
});
|
||||
|
||||
it('renders channels as multiple select options', () => {
|
||||
renderThresholdItem();
|
||||
|
||||
// Check that channels are rendered as multiple select
|
||||
expect(
|
||||
screen.getByText(TEST_CONSTANTS.EMAIL_CHANNEL_NAME),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Should be able to select multiple channels
|
||||
const channelSelectors = screen.getAllByRole('combobox');
|
||||
const channelSelector = channelSelectors[1]; // Second combobox is the channels selector
|
||||
fireEvent.change(channelSelector, {
|
||||
target: { value: [TEST_CONSTANTS.CHANNEL_1, TEST_CONSTANTS.CHANNEL_2] },
|
||||
});
|
||||
});
|
||||
|
||||
it('handles empty threshold values correctly', () => {
|
||||
const emptyThreshold = {
|
||||
...mockThreshold,
|
||||
@@ -315,7 +318,7 @@ describe('ThresholdItem', () => {
|
||||
renderThresholdItem({ threshold: emptyThreshold });
|
||||
|
||||
expect(screen.getByPlaceholderText('Enter threshold name')).toHaveValue('');
|
||||
expect(screen.getByPlaceholderText('Enter threshold value')).toHaveValue(0);
|
||||
expect(screen.getByPlaceholderText('Enter threshold value')).toHaveValue('0');
|
||||
});
|
||||
|
||||
it('renders with correct input widths', () => {
|
||||
@@ -328,13 +331,13 @@ describe('ThresholdItem', () => {
|
||||
TEST_CONSTANTS.ENTER_THRESHOLD_VALUE,
|
||||
);
|
||||
|
||||
expect(labelInput).toHaveStyle('width: 200px');
|
||||
expect(valueInput).toHaveStyle('width: 100px');
|
||||
expect(labelInput).toHaveStyle('width: 260px');
|
||||
expect(valueInput).toHaveStyle('width: 210px');
|
||||
});
|
||||
|
||||
it('renders channels selector with correct width', () => {
|
||||
renderThresholdItem();
|
||||
verifySelectorWidth(1, '350px');
|
||||
verifySelectorWidth(1, '260px');
|
||||
});
|
||||
|
||||
it('renders unit selector with correct width', () => {
|
||||
@@ -347,14 +350,37 @@ describe('ThresholdItem', () => {
|
||||
verifyComponentRendersWithLoading();
|
||||
});
|
||||
|
||||
it.skip('renders recovery threshold with correct initial value', () => {
|
||||
it('renders recovery threshold with correct initial value', () => {
|
||||
renderThresholdItem();
|
||||
// showRecoveryThreshold();
|
||||
showRecoveryThreshold();
|
||||
|
||||
const recoveryValueInput = screen.getByPlaceholderText(
|
||||
TEST_CONSTANTS.ENTER_RECOVERY_THRESHOLD_VALUE,
|
||||
);
|
||||
expect(recoveryValueInput).toHaveValue(80);
|
||||
expect(recoveryValueInput).toHaveValue('80');
|
||||
});
|
||||
|
||||
it('renders recovery threshold label as disabled', () => {
|
||||
renderThresholdItem();
|
||||
showRecoveryThreshold();
|
||||
|
||||
const recoveryLabelInput = screen.getByPlaceholderText('Recovery threshold');
|
||||
expect(recoveryLabelInput).toBeDisabled();
|
||||
});
|
||||
|
||||
it('renders correct channel options', () => {
|
||||
renderThresholdItem();
|
||||
|
||||
// Check that channels are rendered
|
||||
expect(
|
||||
screen.getByText(TEST_CONSTANTS.EMAIL_CHANNEL_NAME),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Should be able to select different channels
|
||||
const channelSelectors = screen.getAllByRole('combobox');
|
||||
const channelSelector = channelSelectors[1]; // Second combobox is the channels selector
|
||||
fireEvent.change(channelSelector, { target: { value: 'channel-2' } });
|
||||
expect(screen.getByText('Slack Channel')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles threshold without channels', () => {
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
padding-right: 72px;
|
||||
background-color: var(--bg-ink-500);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
width: 100%;
|
||||
width: fit-content;
|
||||
|
||||
.alert-condition-sentences {
|
||||
display: flex;
|
||||
@@ -90,7 +90,7 @@
|
||||
}
|
||||
|
||||
.ant-select {
|
||||
width: 240px;
|
||||
width: 240px !important;
|
||||
|
||||
.ant-select-selector {
|
||||
background-color: var(--bg-ink-300);
|
||||
@@ -148,7 +148,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.ant-input {
|
||||
background-color: var(--bg-ink-400);
|
||||
@@ -278,29 +277,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.routing-policies-info-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
background-color: #4568dc1a;
|
||||
border: 1px solid var(--bg-robin-500);
|
||||
padding: 8px 16px;
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-robin-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.anomaly-threshold-container {
|
||||
.ant-select {
|
||||
.ant-select-selector {
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.condensed-alert-threshold-container,
|
||||
@@ -317,8 +293,7 @@
|
||||
.ant-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 240px;
|
||||
width: auto;
|
||||
width: 240px;
|
||||
justify-content: space-between;
|
||||
background-color: var(--bg-ink-300);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
@@ -326,7 +301,6 @@
|
||||
.evaluate-alert-conditions-button-left {
|
||||
color: var(--bg-vanilla-400);
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.evaluate-alert-conditions-button-right {
|
||||
@@ -334,7 +308,6 @@
|
||||
align-items: center;
|
||||
color: var(--bg-vanilla-400);
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.evaluate-alert-conditions-button-right-text {
|
||||
font-size: 12px;
|
||||
@@ -345,235 +318,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.alert-condition-container {
|
||||
.alert-condition {
|
||||
.alert-condition-tabs {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-300);
|
||||
|
||||
.explorer-view-option {
|
||||
border-left: 0.5px solid var(--bg-vanilla-300);
|
||||
border-bottom: 0.5px solid var(--bg-vanilla-300);
|
||||
|
||||
&.active-tab {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert-threshold-container,
|
||||
.anomaly-threshold-container {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
.alert-condition-sentences {
|
||||
.alert-condition-sentence {
|
||||
.sentence-text {
|
||||
color: var(--text-ink-400);
|
||||
}
|
||||
|
||||
.ant-select {
|
||||
.ant-select-selector {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
color: var(--text-ink-400);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-ink-300);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-ink-300);
|
||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.thresholds-section {
|
||||
.threshold-item {
|
||||
.threshold-row {
|
||||
.threshold-controls {
|
||||
.threshold-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
background-color: var(--bg-vanilla-200);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-ink-300);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-ink-300);
|
||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select {
|
||||
.ant-select-selector {
|
||||
background-color: var(--bg-vanilla-200);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-ink-300);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-ink-300);
|
||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
color: var(--bg-ink-400);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recovery-threshold-input-group {
|
||||
.recovery-threshold-btn {
|
||||
color: var(--bg-ink-400);
|
||||
background-color: var(--bg-vanilla-200) !important;
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
background-color: var(--bg-vanilla-200);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-ink-300);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-ink-300);
|
||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-threshold-btn,
|
||||
.ant-btn.add-threshold-btn {
|
||||
border: 1px dashed var(--bg-vanilla-300);
|
||||
color: var(--bg-ink-300);
|
||||
background-color: transparent;
|
||||
|
||||
.ant-typography {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-ink-300);
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.condensed-evaluation-settings-container {
|
||||
.ant-btn {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
min-width: 240px;
|
||||
width: auto;
|
||||
|
||||
.evaluate-alert-conditions-button-left {
|
||||
color: var(--bg-ink-400);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.evaluate-alert-conditions-button-right {
|
||||
color: var(--bg-ink-400);
|
||||
flex-shrink: 0;
|
||||
|
||||
.evaluate-alert-conditions-button-right-text {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.highlighted-text {
|
||||
font-weight: bold;
|
||||
color: var(--bg-robin-400);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
// Tooltip styles
|
||||
.tooltip-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
|
||||
.tooltip-description {
|
||||
margin-bottom: 8px;
|
||||
|
||||
span {
|
||||
font-weight: bold;
|
||||
color: var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-example {
|
||||
margin-bottom: 8px;
|
||||
color: #8b92a0;
|
||||
}
|
||||
|
||||
.tooltip-link {
|
||||
.tooltip-link-text {
|
||||
color: #1890ff;
|
||||
font-size: 11px;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
|
||||
import {
|
||||
NotificationSettingsAction,
|
||||
NotificationSettingsState,
|
||||
Threshold,
|
||||
} from '../context/types';
|
||||
import { Threshold } from '../context/types';
|
||||
|
||||
export type UpdateThreshold = {
|
||||
(thresholdId: string, field: 'channels', value: string[]): void;
|
||||
(
|
||||
thresholdId: string,
|
||||
field: Exclude<keyof Threshold, 'channels'>,
|
||||
value: string | number | null,
|
||||
value: string,
|
||||
): void;
|
||||
};
|
||||
|
||||
@@ -24,20 +20,4 @@ export interface ThresholdItemProps {
|
||||
channels: Channels[];
|
||||
isLoadingChannels: boolean;
|
||||
units: DefaultOptionType[];
|
||||
isErrorChannels: boolean;
|
||||
refreshChannels: () => void;
|
||||
}
|
||||
|
||||
export interface AnomalyAndThresholdProps {
|
||||
channels: Channels[];
|
||||
isLoadingChannels: boolean;
|
||||
isErrorChannels: boolean;
|
||||
refreshChannels: () => void;
|
||||
}
|
||||
|
||||
export interface RoutingPolicyBannerProps {
|
||||
notificationSettings: NotificationSettingsState;
|
||||
setNotificationSettings: (
|
||||
notificationSettings: NotificationSettingsAction,
|
||||
) => void;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
import { Button, Flex, Switch, Typography } from 'antd';
|
||||
import { BaseOptionType, DefaultOptionType, SelectProps } from 'antd/es/select';
|
||||
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
|
||||
import { Y_AXIS_CATEGORIES } from 'components/YAxisUnitSelector/constants';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
AlertThresholdMatchType,
|
||||
AlertThresholdOperator,
|
||||
} from 'container/CreateAlertV2/context/types';
|
||||
import { getSelectedQueryOptions } from 'container/FormAlertRules/utils';
|
||||
import { IUser } from 'providers/App/types';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import { RoutingPolicyBannerProps } from './types';
|
||||
|
||||
export function getQueryNames(currentQuery: Query): BaseOptionType[] {
|
||||
const involvedQueriesInTraceOperator = getInvolvedQueriesInTraceOperator(
|
||||
@@ -54,360 +44,3 @@ export function getCategorySelectOptionByName(
|
||||
) || []
|
||||
);
|
||||
}
|
||||
|
||||
const getOperatorWord = (op: AlertThresholdOperator): string => {
|
||||
switch (op) {
|
||||
case AlertThresholdOperator.IS_ABOVE:
|
||||
return 'exceed';
|
||||
case AlertThresholdOperator.IS_BELOW:
|
||||
return 'fall below';
|
||||
case AlertThresholdOperator.IS_EQUAL_TO:
|
||||
return 'equal';
|
||||
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
|
||||
return 'not equal';
|
||||
default:
|
||||
return 'exceed';
|
||||
}
|
||||
};
|
||||
|
||||
const getThresholdValue = (op: AlertThresholdOperator): number => {
|
||||
switch (op) {
|
||||
case AlertThresholdOperator.IS_ABOVE:
|
||||
return 80;
|
||||
case AlertThresholdOperator.IS_BELOW:
|
||||
return 50;
|
||||
case AlertThresholdOperator.IS_EQUAL_TO:
|
||||
return 100;
|
||||
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
|
||||
return 0;
|
||||
default:
|
||||
return 80;
|
||||
}
|
||||
};
|
||||
|
||||
const getDataPoints = (
|
||||
matchType: AlertThresholdMatchType,
|
||||
op: AlertThresholdOperator,
|
||||
): number[] => {
|
||||
const dataPointMap: Record<
|
||||
AlertThresholdMatchType,
|
||||
Record<AlertThresholdOperator, number[]>
|
||||
> = {
|
||||
[AlertThresholdMatchType.AT_LEAST_ONCE]: {
|
||||
[AlertThresholdOperator.IS_BELOW]: [60, 45, 40, 55, 35],
|
||||
[AlertThresholdOperator.IS_EQUAL_TO]: [95, 100, 105, 90, 100],
|
||||
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [5, 0, 10, 15, 0],
|
||||
[AlertThresholdOperator.IS_ABOVE]: [75, 85, 90, 78, 95],
|
||||
[AlertThresholdOperator.ABOVE_BELOW]: [75, 85, 90, 78, 95],
|
||||
},
|
||||
[AlertThresholdMatchType.ALL_THE_TIME]: {
|
||||
[AlertThresholdOperator.IS_BELOW]: [45, 40, 35, 42, 38],
|
||||
[AlertThresholdOperator.IS_EQUAL_TO]: [100, 100, 100, 100, 100],
|
||||
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [5, 10, 15, 8, 12],
|
||||
[AlertThresholdOperator.IS_ABOVE]: [85, 87, 90, 88, 95],
|
||||
[AlertThresholdOperator.ABOVE_BELOW]: [85, 87, 90, 88, 95],
|
||||
},
|
||||
[AlertThresholdMatchType.ON_AVERAGE]: {
|
||||
[AlertThresholdOperator.IS_BELOW]: [60, 40, 45, 35, 45],
|
||||
[AlertThresholdOperator.IS_EQUAL_TO]: [95, 105, 100, 95, 105],
|
||||
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [5, 10, 15, 8, 12],
|
||||
[AlertThresholdOperator.IS_ABOVE]: [75, 85, 90, 78, 95],
|
||||
[AlertThresholdOperator.ABOVE_BELOW]: [75, 85, 90, 78, 95],
|
||||
},
|
||||
[AlertThresholdMatchType.IN_TOTAL]: {
|
||||
[AlertThresholdOperator.IS_BELOW]: [8, 5, 10, 12, 8],
|
||||
[AlertThresholdOperator.IS_EQUAL_TO]: [20, 20, 20, 20, 20],
|
||||
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [10, 15, 25, 5, 30],
|
||||
[AlertThresholdOperator.IS_ABOVE]: [10, 15, 25, 5, 30],
|
||||
[AlertThresholdOperator.ABOVE_BELOW]: [10, 15, 25, 5, 30],
|
||||
},
|
||||
[AlertThresholdMatchType.LAST]: {
|
||||
[AlertThresholdOperator.IS_BELOW]: [75, 85, 90, 78, 45],
|
||||
[AlertThresholdOperator.IS_EQUAL_TO]: [75, 85, 90, 78, 100],
|
||||
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: [75, 85, 90, 78, 25],
|
||||
[AlertThresholdOperator.IS_ABOVE]: [75, 85, 90, 78, 95],
|
||||
[AlertThresholdOperator.ABOVE_BELOW]: [75, 85, 90, 78, 95],
|
||||
},
|
||||
};
|
||||
|
||||
return dataPointMap[matchType]?.[op] || [75, 85, 90, 78, 95];
|
||||
};
|
||||
|
||||
const getTooltipOperatorSymbol = (op: AlertThresholdOperator): string => {
|
||||
const symbolMap: Record<AlertThresholdOperator, string> = {
|
||||
[AlertThresholdOperator.IS_ABOVE]: '>',
|
||||
[AlertThresholdOperator.IS_BELOW]: '<',
|
||||
[AlertThresholdOperator.IS_EQUAL_TO]: '=',
|
||||
[AlertThresholdOperator.IS_NOT_EQUAL_TO]: '!=',
|
||||
[AlertThresholdOperator.ABOVE_BELOW]: '>',
|
||||
};
|
||||
return symbolMap[op] || '>';
|
||||
};
|
||||
|
||||
const handleTooltipClick = (
|
||||
e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
|
||||
): void => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
function TooltipContent({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleTooltipClick}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleTooltipClick(e);
|
||||
}
|
||||
}}
|
||||
className="tooltip-content"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TooltipExample({
|
||||
children,
|
||||
dataPoints,
|
||||
operatorSymbol,
|
||||
thresholdValue,
|
||||
matchType,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
dataPoints: number[];
|
||||
operatorSymbol: string;
|
||||
thresholdValue: number;
|
||||
matchType: AlertThresholdMatchType;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="tooltip-example">
|
||||
<strong>Example:</strong>
|
||||
<br />
|
||||
Say, For a 5-minute window (configured in Evaluation settings), 1 min
|
||||
aggregation interval (set up in query) → 5{' '}
|
||||
{matchType === AlertThresholdMatchType.IN_TOTAL
|
||||
? 'error counts'
|
||||
: 'data points'}
|
||||
: [{dataPoints.join(', ')}]<br />
|
||||
With threshold {operatorSymbol} {thresholdValue}: {children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TooltipLink(): JSX.Element {
|
||||
return (
|
||||
<div className="tooltip-link">
|
||||
<a
|
||||
href="https://signoz.io/docs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="tooltip-link-text"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const getMatchTypeTooltip = (
|
||||
matchType: AlertThresholdMatchType,
|
||||
operator: AlertThresholdOperator,
|
||||
): React.ReactNode => {
|
||||
const operatorSymbol = getTooltipOperatorSymbol(operator);
|
||||
const operatorWord = getOperatorWord(operator);
|
||||
const thresholdValue = getThresholdValue(operator);
|
||||
const dataPoints = getDataPoints(matchType, operator);
|
||||
const getMatchingPointsCount = (): number =>
|
||||
dataPoints.filter((p) => {
|
||||
switch (operator) {
|
||||
case AlertThresholdOperator.IS_ABOVE:
|
||||
return p > thresholdValue;
|
||||
case AlertThresholdOperator.IS_BELOW:
|
||||
return p < thresholdValue;
|
||||
case AlertThresholdOperator.IS_EQUAL_TO:
|
||||
return p === thresholdValue;
|
||||
case AlertThresholdOperator.IS_NOT_EQUAL_TO:
|
||||
return p !== thresholdValue;
|
||||
default:
|
||||
return p > thresholdValue;
|
||||
}
|
||||
}).length;
|
||||
|
||||
switch (matchType) {
|
||||
case AlertThresholdMatchType.AT_LEAST_ONCE:
|
||||
return (
|
||||
<TooltipContent>
|
||||
<div className="tooltip-description">
|
||||
Data is aggregated at each interval within your evaluation window,
|
||||
creating multiple data points. This option triggers if <span>ANY</span> of
|
||||
those aggregated data points crosses the threshold.
|
||||
</div>
|
||||
<TooltipExample
|
||||
dataPoints={dataPoints}
|
||||
operatorSymbol={operatorSymbol}
|
||||
thresholdValue={thresholdValue}
|
||||
matchType={matchType}
|
||||
>
|
||||
Alert triggers ({getMatchingPointsCount()} points {operatorWord}{' '}
|
||||
{thresholdValue})
|
||||
</TooltipExample>
|
||||
<TooltipLink />
|
||||
</TooltipContent>
|
||||
);
|
||||
|
||||
case AlertThresholdMatchType.ALL_THE_TIME:
|
||||
return (
|
||||
<TooltipContent>
|
||||
<div className="tooltip-description">
|
||||
Data is aggregated at each interval within your evaluation window,
|
||||
creating multiple data points. This option triggers if <span>ALL</span>{' '}
|
||||
aggregated data points cross the threshold.
|
||||
</div>
|
||||
<TooltipExample
|
||||
dataPoints={dataPoints}
|
||||
operatorSymbol={operatorSymbol}
|
||||
thresholdValue={thresholdValue}
|
||||
matchType={matchType}
|
||||
>
|
||||
Alert triggers (all points {operatorWord} {thresholdValue})<br />
|
||||
If any point was {thresholdValue}, no alert would fire
|
||||
</TooltipExample>
|
||||
<TooltipLink />
|
||||
</TooltipContent>
|
||||
);
|
||||
|
||||
case AlertThresholdMatchType.ON_AVERAGE: {
|
||||
const average = (
|
||||
dataPoints.reduce((a, b) => a + b, 0) / dataPoints.length
|
||||
).toFixed(1);
|
||||
return (
|
||||
<TooltipContent>
|
||||
<div className="tooltip-description">
|
||||
Data is aggregated at each interval within your evaluation window,
|
||||
creating multiple data points. This option triggers if the{' '}
|
||||
<span>AVERAGE</span> of all aggregated data points crosses the threshold.
|
||||
</div>
|
||||
<TooltipExample
|
||||
dataPoints={dataPoints}
|
||||
operatorSymbol={operatorSymbol}
|
||||
thresholdValue={thresholdValue}
|
||||
matchType={matchType}
|
||||
>
|
||||
Alert triggers (average = {average})
|
||||
</TooltipExample>
|
||||
<TooltipLink />
|
||||
</TooltipContent>
|
||||
);
|
||||
}
|
||||
|
||||
case AlertThresholdMatchType.IN_TOTAL: {
|
||||
const total = dataPoints.reduce((a, b) => a + b, 0);
|
||||
return (
|
||||
<TooltipContent>
|
||||
<div className="tooltip-description">
|
||||
Data is aggregated at each interval within your evaluation window,
|
||||
creating multiple data points. This option triggers if the{' '}
|
||||
<span>SUM</span> of all aggregated data points crosses the threshold.
|
||||
</div>
|
||||
<TooltipExample
|
||||
dataPoints={dataPoints}
|
||||
operatorSymbol={operatorSymbol}
|
||||
thresholdValue={thresholdValue}
|
||||
matchType={matchType}
|
||||
>
|
||||
Alert triggers (total = {total})
|
||||
</TooltipExample>
|
||||
<TooltipLink />
|
||||
</TooltipContent>
|
||||
);
|
||||
}
|
||||
|
||||
case AlertThresholdMatchType.LAST: {
|
||||
const lastPoint = dataPoints[dataPoints.length - 1];
|
||||
return (
|
||||
<TooltipContent>
|
||||
<div className="tooltip-description">
|
||||
Data is aggregated at each interval within your evaluation window,
|
||||
creating multiple data points. This option triggers based on the{' '}
|
||||
<span>MOST RECENT</span> aggregated data point only.
|
||||
</div>
|
||||
<TooltipExample
|
||||
dataPoints={dataPoints}
|
||||
operatorSymbol={operatorSymbol}
|
||||
thresholdValue={thresholdValue}
|
||||
matchType={matchType}
|
||||
>
|
||||
Alert triggers (last point = {lastPoint})
|
||||
</TooltipExample>
|
||||
<TooltipLink />
|
||||
</TooltipContent>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export function NotificationChannelsNotFoundContent({
|
||||
user,
|
||||
refreshChannels,
|
||||
}: {
|
||||
user: IUser;
|
||||
refreshChannels: () => void;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<Flex justify="space-between">
|
||||
<Flex gap={4} align="center">
|
||||
<Typography.Text>No channels yet.</Typography.Text>
|
||||
{user?.role === USER_ROLES.ADMIN ? (
|
||||
<Typography.Text>
|
||||
Create one
|
||||
<Button
|
||||
style={{ padding: '0 4px' }}
|
||||
type="link"
|
||||
onClick={(): void => {
|
||||
window.open(ROUTES.CHANNELS_NEW, '_blank');
|
||||
}}
|
||||
>
|
||||
here.
|
||||
</Button>
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text>Please ask your admin to create one.</Typography.Text>
|
||||
)}
|
||||
</Flex>
|
||||
<Button type="text" onClick={refreshChannels}>
|
||||
Refresh
|
||||
</Button>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export function RoutingPolicyBanner({
|
||||
notificationSettings,
|
||||
setNotificationSettings,
|
||||
}: RoutingPolicyBannerProps): JSX.Element {
|
||||
return (
|
||||
<div className="routing-policies-info-banner">
|
||||
<Typography.Text>
|
||||
Use <strong>Routing Policies</strong> for dynamic routing
|
||||
</Typography.Text>
|
||||
<Switch
|
||||
checked={notificationSettings.routingPolicies}
|
||||
onChange={(value): void => {
|
||||
setNotificationSettings({
|
||||
type: 'SET_ROUTING_POLICIES',
|
||||
payload: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import './styles.scss';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Labels } from 'types/api/alerts/def';
|
||||
@@ -9,7 +8,7 @@ import { useCreateAlertState } from '../context';
|
||||
import LabelsInput from './LabelsInput';
|
||||
|
||||
function CreateAlertHeader(): JSX.Element {
|
||||
const { alertState, setAlertState, isEditMode } = useCreateAlertState();
|
||||
const { alertState, setAlertState } = useCreateAlertState();
|
||||
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
@@ -35,14 +34,11 @@ function CreateAlertHeader(): JSX.Element {
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('alert-header', { 'edit-alert-header': isEditMode })}
|
||||
>
|
||||
{!isEditMode && (
|
||||
<div className="alert-header__tab-bar">
|
||||
<div className="alert-header__tab">New Alert Rule</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="alert-header">
|
||||
<div className="alert-header__tab-bar">
|
||||
<div className="alert-header__tab">New Alert Rule</div>
|
||||
</div>
|
||||
|
||||
<div className="alert-header__content">
|
||||
<input
|
||||
type="text"
|
||||
@@ -53,6 +49,15 @@ function CreateAlertHeader(): JSX.Element {
|
||||
className="alert-header__input title"
|
||||
placeholder="Enter alert rule name"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={alertState.description}
|
||||
onChange={(e): void =>
|
||||
setAlertState({ type: 'SET_ALERT_DESCRIPTION', payload: e.target.value })
|
||||
}
|
||||
className="alert-header__input description"
|
||||
placeholder="Click to add description..."
|
||||
/>
|
||||
<LabelsInput
|
||||
labels={alertState.labels}
|
||||
onLabelsChange={(labels: Labels): void =>
|
||||
|
||||
@@ -1,28 +1,9 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { defaultPostableAlertRuleV2 } from 'container/CreateAlertV2/constants';
|
||||
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
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';
|
||||
|
||||
jest.spyOn(useCreateAlertRuleHook, 'useCreateAlertRule').mockReturnValue({
|
||||
mutate: jest.fn(),
|
||||
isLoading: false,
|
||||
} as any);
|
||||
jest.spyOn(useTestAlertRuleHook, 'useTestAlertRule').mockReturnValue({
|
||||
mutate: jest.fn(),
|
||||
isLoading: false,
|
||||
} as any);
|
||||
jest.spyOn(useUpdateAlertRuleHook, 'useUpdateAlertRule').mockReturnValue({
|
||||
mutate: jest.fn(),
|
||||
isLoading: false,
|
||||
} as any);
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
@@ -44,11 +25,9 @@ jest.mock('react-router-dom', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const ENTER_ALERT_RULE_NAME_PLACEHOLDER = 'Enter alert rule name';
|
||||
|
||||
const renderCreateAlertHeader = (): ReturnType<typeof render> =>
|
||||
render(
|
||||
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||
<CreateAlertProvider>
|
||||
<CreateAlertHeader />
|
||||
</CreateAlertProvider>,
|
||||
);
|
||||
@@ -61,12 +40,18 @@ describe('CreateAlertHeader', () => {
|
||||
|
||||
it('renders name input with placeholder', () => {
|
||||
renderCreateAlertHeader();
|
||||
const nameInput = screen.getByPlaceholderText(
|
||||
ENTER_ALERT_RULE_NAME_PLACEHOLDER,
|
||||
);
|
||||
const nameInput = screen.getByPlaceholderText('Enter alert rule name');
|
||||
expect(nameInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders description input with placeholder', () => {
|
||||
renderCreateAlertHeader();
|
||||
const descriptionInput = screen.getByPlaceholderText(
|
||||
'Click to add description...',
|
||||
);
|
||||
expect(descriptionInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders LabelsInput component', () => {
|
||||
renderCreateAlertHeader();
|
||||
expect(screen.getByText('+ Add labels')).toBeInTheDocument();
|
||||
@@ -74,30 +59,19 @@ describe('CreateAlertHeader', () => {
|
||||
|
||||
it('updates name when typing in name input', () => {
|
||||
renderCreateAlertHeader();
|
||||
const nameInput = screen.getByPlaceholderText(
|
||||
ENTER_ALERT_RULE_NAME_PLACEHOLDER,
|
||||
);
|
||||
const nameInput = screen.getByPlaceholderText('Enter alert rule name');
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: 'Test Alert' } });
|
||||
|
||||
expect(nameInput).toHaveValue('Test Alert');
|
||||
});
|
||||
|
||||
it('renders the header with title when isEditMode is true', () => {
|
||||
render(
|
||||
<CreateAlertProvider
|
||||
isEditMode
|
||||
initialAlertType={AlertTypes.METRICS_BASED_ALERT}
|
||||
initialAlertState={getCreateAlertLocalStateFromAlertDef(
|
||||
defaultPostableAlertRuleV2,
|
||||
)}
|
||||
>
|
||||
<CreateAlertHeader />
|
||||
</CreateAlertProvider>,
|
||||
it('updates description when typing in description input', () => {
|
||||
renderCreateAlertHeader();
|
||||
const descriptionInput = screen.getByPlaceholderText(
|
||||
'Click to add description...',
|
||||
);
|
||||
expect(screen.queryByText('New Alert Rule')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByPlaceholderText(ENTER_ALERT_RULE_NAME_PLACEHOLDER),
|
||||
).toHaveValue('TEST_ALERT');
|
||||
fireEvent.change(descriptionInput, { target: { value: 'Test Description' } });
|
||||
expect(descriptionInput).toHaveValue('Test Description');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,21 @@
|
||||
font-family: inherit;
|
||||
color: var(--text-vanilla-100);
|
||||
|
||||
/* Top bar with diagonal stripes */
|
||||
&__tab-bar {
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: repeating-linear-gradient(
|
||||
-45deg,
|
||||
#0f0f0f,
|
||||
#0f0f0f 10px,
|
||||
#101010 10px,
|
||||
#101010 20px
|
||||
);
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
/* Tab block visuals */
|
||||
&__tab {
|
||||
display: flex;
|
||||
@@ -29,8 +44,6 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 300px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__input.title {
|
||||
@@ -38,8 +51,6 @@
|
||||
font-weight: 500;
|
||||
background-color: transparent;
|
||||
color: var(--text-vanilla-100);
|
||||
width: 100%;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
&__input:focus,
|
||||
@@ -53,15 +64,6 @@
|
||||
background-color: transparent;
|
||||
color: var(--text-vanilla-300);
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
color: var(--text-vanilla-100);
|
||||
border: 1px solid var(--bg-slate-300);
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.labels-input {
|
||||
@@ -147,74 +149,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.alert-header {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
color: var(--text-ink-100);
|
||||
|
||||
&__tab {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
color: var(--text-ink-100);
|
||||
}
|
||||
|
||||
&__tab::before {
|
||||
color: var(--bg-ink-100);
|
||||
}
|
||||
|
||||
&__content {
|
||||
background: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
&__input.title {
|
||||
color: var(--text-ink-100);
|
||||
}
|
||||
|
||||
&__input.description {
|
||||
color: var(--text-ink-300);
|
||||
}
|
||||
}
|
||||
|
||||
.edit-alert-header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.edit-alert-header .alert-header__content {
|
||||
background: var(--bg-vanilla-200);
|
||||
}
|
||||
|
||||
.labels-input {
|
||||
&__add-button {
|
||||
color: var(--bg-ink-400);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background-color: var(--bg-vanilla-100);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-ink-300);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
|
||||
&__label-pill {
|
||||
background-color: #ad7f581a;
|
||||
color: var(--bg-sienna-400);
|
||||
border: 1px solid var(--bg-sienna-500);
|
||||
}
|
||||
|
||||
&__remove-button {
|
||||
color: var(--bg-sienna-400);
|
||||
|
||||
&:hover {
|
||||
color: var(--text-ink-100);
|
||||
}
|
||||
}
|
||||
|
||||
&__input {
|
||||
color: var(--bg-ink-500);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--bg-ink-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
$top-nav-background-1: #0f0f0f;
|
||||
$top-nav-background-2: #101010;
|
||||
|
||||
.create-alert-v2-container {
|
||||
background-color: var(--bg-ink-500);
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.create-alert-v2-container {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
|
||||
.sticky-page-spinner {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
z-index: 10000;
|
||||
pointer-events: auto;
|
||||
.top-nav-container {
|
||||
background: repeating-linear-gradient(
|
||||
-45deg,
|
||||
$top-nav-background-1,
|
||||
$top-nav-background-1 10px,
|
||||
$top-nav-background-2 10px,
|
||||
$top-nav-background-2 20px
|
||||
);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -2,36 +2,34 @@ import './CreateAlertV2.styles.scss';
|
||||
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import AlertCondition from './AlertCondition';
|
||||
import { CreateAlertProvider } from './context';
|
||||
import { buildInitialAlertDef } from './context/utils';
|
||||
import CreateAlertHeader from './CreateAlertHeader';
|
||||
import Footer from './Footer';
|
||||
import EvaluationSettings from './EvaluationSettings';
|
||||
import NotificationSettings from './NotificationSettings';
|
||||
import QuerySection from './QuerySection';
|
||||
import { CreateAlertV2Props } from './types';
|
||||
import { Spinner } from './utils';
|
||||
import { showCondensedLayout } from './utils';
|
||||
|
||||
function CreateAlertV2({ alertType }: CreateAlertV2Props): JSX.Element {
|
||||
const queryToRedirect = buildInitialAlertDef(alertType);
|
||||
const currentQueryToRedirect = mapQueryDataFromApi(
|
||||
queryToRedirect.condition.compositeQuery,
|
||||
);
|
||||
function CreateAlertV2({
|
||||
initialQuery = initialQueriesMap.metrics,
|
||||
}: {
|
||||
initialQuery?: Query;
|
||||
}): JSX.Element {
|
||||
useShareBuilderUrl({ defaultValue: initialQuery });
|
||||
|
||||
useShareBuilderUrl({ defaultValue: currentQueryToRedirect });
|
||||
const showCondensedLayoutFlag = showCondensedLayout();
|
||||
|
||||
return (
|
||||
<CreateAlertProvider initialAlertType={alertType}>
|
||||
<Spinner />
|
||||
<CreateAlertProvider>
|
||||
<div className="create-alert-v2-container">
|
||||
<CreateAlertHeader />
|
||||
<QuerySection />
|
||||
<AlertCondition />
|
||||
{!showCondensedLayoutFlag ? <EvaluationSettings /> : null}
|
||||
<NotificationSettings />
|
||||
</div>
|
||||
<Footer />
|
||||
</CreateAlertProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import './styles.scss';
|
||||
|
||||
import { Switch, Tooltip, Typography } from 'antd';
|
||||
import { Info } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { IAdvancedOptionItemProps } from '../types';
|
||||
|
||||
@@ -12,14 +12,9 @@ function AdvancedOptionItem({
|
||||
input,
|
||||
tooltipText,
|
||||
onToggle,
|
||||
defaultShowInput,
|
||||
}: IAdvancedOptionItemProps): JSX.Element {
|
||||
const [showInput, setShowInput] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setShowInput(defaultShowInput);
|
||||
}, [defaultShowInput]);
|
||||
|
||||
const handleOnToggle = (): void => {
|
||||
onToggle?.();
|
||||
setShowInput((currentShowInput) => !currentShowInput);
|
||||
@@ -47,7 +42,7 @@ function AdvancedOptionItem({
|
||||
>
|
||||
{input}
|
||||
</div>
|
||||
<Switch onChange={handleOnToggle} checked={showInput} />
|
||||
<Switch onChange={handleOnToggle} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -114,14 +114,6 @@
|
||||
height: 32px;
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
.ant-select-selection-placeholder {
|
||||
font-family: 'Space Mono';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Collapse, Input, Typography } from 'antd';
|
||||
import { Collapse, Input, Select, Typography } from 'antd';
|
||||
import { Y_AXIS_CATEGORIES } from 'components/YAxisUnitSelector/constants';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import AdvancedOptionItem from './AdvancedOptionItem';
|
||||
@@ -7,6 +8,10 @@ import EvaluationCadence from './EvaluationCadence';
|
||||
function AdvancedOptions(): JSX.Element {
|
||||
const { advancedOptions, setAdvancedOptions } = useCreateAlertState();
|
||||
|
||||
const timeOptions = Y_AXIS_CATEGORIES.find(
|
||||
(category) => category.name === 'Time',
|
||||
)?.units.map((unit) => ({ label: unit.name, value: unit.id }));
|
||||
|
||||
return (
|
||||
<div className="advanced-options-container">
|
||||
<Collapse bordered={false}>
|
||||
@@ -33,16 +38,24 @@ function AdvancedOptions(): JSX.Element {
|
||||
}
|
||||
value={advancedOptions.sendNotificationIfDataIsMissing.toleranceLimit}
|
||||
/>
|
||||
<Typography.Text>Minutes</Typography.Text>
|
||||
<Select
|
||||
style={{ width: 120 }}
|
||||
options={timeOptions}
|
||||
placeholder="Select time unit"
|
||||
onChange={(value): void =>
|
||||
setAdvancedOptions({
|
||||
type: 'SET_SEND_NOTIFICATION_IF_DATA_IS_MISSING',
|
||||
payload: {
|
||||
toleranceLimit:
|
||||
advancedOptions.sendNotificationIfDataIsMissing.toleranceLimit,
|
||||
timeUnit: value as string,
|
||||
},
|
||||
})
|
||||
}
|
||||
value={advancedOptions.sendNotificationIfDataIsMissing.timeUnit}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
onToggle={(): void =>
|
||||
setAdvancedOptions({
|
||||
type: 'TOGGLE_SEND_NOTIFICATION_IF_DATA_IS_MISSING',
|
||||
payload: !advancedOptions.sendNotificationIfDataIsMissing.enabled,
|
||||
})
|
||||
}
|
||||
defaultShowInput={advancedOptions.sendNotificationIfDataIsMissing.enabled}
|
||||
/>
|
||||
<AdvancedOptionItem
|
||||
title="Minimum data required"
|
||||
@@ -67,16 +80,8 @@ function AdvancedOptions(): JSX.Element {
|
||||
<Typography.Text>Datapoints</Typography.Text>
|
||||
</div>
|
||||
}
|
||||
onToggle={(): void =>
|
||||
setAdvancedOptions({
|
||||
type: 'TOGGLE_ENFORCE_MINIMUM_DATAPOINTS',
|
||||
payload: !advancedOptions.enforceMinimumDatapoints.enabled,
|
||||
})
|
||||
}
|
||||
defaultShowInput={advancedOptions.enforceMinimumDatapoints.enabled}
|
||||
/>
|
||||
{/* TODO: Add back when the functionality is implemented */}
|
||||
{/* <AdvancedOptionItem
|
||||
<AdvancedOptionItem
|
||||
title="Account for data delay"
|
||||
description="Shift the evaluation window backwards to account for data processing delays."
|
||||
tooltipText="Use when your data takes time to arrive on the platform. For example, if logs typically arrive 5 minutes late, set a 5-minute delay so the alert checks the correct time window."
|
||||
@@ -114,7 +119,7 @@ function AdvancedOptions(): JSX.Element {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/> */}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import './styles.scss';
|
||||
import '../AdvancedOptionItem/styles.scss';
|
||||
|
||||
import { Input, Select, Tooltip, Typography } from 'antd';
|
||||
import { Info } from 'lucide-react';
|
||||
import { Button, Input, Select, Tooltip, Typography } from 'antd';
|
||||
import { Info, Plus } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useCreateAlertState } from '../../context';
|
||||
@@ -36,10 +36,10 @@ function EvaluationCadence(): JSX.Element {
|
||||
);
|
||||
}, [advancedOptions.evaluationCadence.mode]);
|
||||
|
||||
// const showCustomSchedule = (): void => {
|
||||
// setIsEvaluationCadenceDetailsVisible(true);
|
||||
// setIsCustomScheduleButtonVisible(false);
|
||||
// };
|
||||
const showCustomSchedule = (): void => {
|
||||
setIsEvaluationCadenceDetailsVisible(true);
|
||||
setIsCustomScheduleButtonVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="evaluation-cadence-container">
|
||||
@@ -98,14 +98,13 @@ function EvaluationCadence(): JSX.Element {
|
||||
}
|
||||
/>
|
||||
</Input.Group>
|
||||
{/* TODO: Add custom schedule back once the functionality is implemented */}
|
||||
{/* <Button
|
||||
<Button
|
||||
className="advanced-option-item-button"
|
||||
onClick={showCustomSchedule}
|
||||
>
|
||||
<Plus size={12} />
|
||||
<Typography.Text>Add custom schedule</Typography.Text>
|
||||
</Button> */}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -164,14 +164,6 @@
|
||||
background-color: var(--bg-ink-300);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
color: var(--bg-vanilla-100);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,15 +529,6 @@
|
||||
background-color: var(--bg-vanilla-300);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-ink-300);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-ink-300);
|
||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import './styles.scss';
|
||||
|
||||
import { Button, Popover } from 'antd';
|
||||
import { Button, Popover, Typography } from 'antd';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import Stepper from '../Stepper';
|
||||
import { showCondensedLayout } from '../utils';
|
||||
import AdvancedOptions from './AdvancedOptions';
|
||||
import EvaluationWindowPopover from './EvaluationWindowPopover';
|
||||
import { getEvaluationWindowTypeText, getTimeframeText } from './utils';
|
||||
|
||||
function EvaluationSettings(): JSX.Element {
|
||||
const { evaluationWindow, setEvaluationWindow } = useCreateAlertState();
|
||||
const {
|
||||
alertType,
|
||||
evaluationWindow,
|
||||
setEvaluationWindow,
|
||||
} = useCreateAlertState();
|
||||
const [
|
||||
isEvaluationWindowPopoverOpen,
|
||||
setIsEvaluationWindowPopoverOpen,
|
||||
] = useState(false);
|
||||
const showCondensedLayoutFlag = showCondensedLayout();
|
||||
|
||||
const popoverContent = (
|
||||
<Popover
|
||||
@@ -48,12 +57,33 @@ function EvaluationSettings(): JSX.Element {
|
||||
</Popover>
|
||||
);
|
||||
|
||||
// Layout consists of only the evaluation window popover
|
||||
if (showCondensedLayoutFlag) {
|
||||
return (
|
||||
<div
|
||||
className="condensed-evaluation-settings-container"
|
||||
data-testid="condensed-evaluation-settings-container"
|
||||
>
|
||||
{popoverContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Layout consists of
|
||||
// - Stepper header
|
||||
// - Evaluation window popover
|
||||
// - Advanced options
|
||||
return (
|
||||
<div
|
||||
className="condensed-evaluation-settings-container"
|
||||
data-testid="condensed-evaluation-settings-container"
|
||||
>
|
||||
{popoverContent}
|
||||
<div className="evaluation-settings-container">
|
||||
<Stepper stepNumber={3} label="Evaluation settings" />
|
||||
{alertType !== AlertTypes.ANOMALY_BASED_ALERT && (
|
||||
<div className="evaluate-alert-conditions-container">
|
||||
<Typography.Text>Check conditions using data from</Typography.Text>
|
||||
<div className="evaluate-alert-conditions-separator" />
|
||||
{popoverContent}
|
||||
</div>
|
||||
)}
|
||||
<AdvancedOptions />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useMemo } from 'react';
|
||||
|
||||
import { ADVANCED_OPTIONS_TIME_UNIT_OPTIONS } from '../../context/constants';
|
||||
import {
|
||||
getCumulativeWindowDescription,
|
||||
getRollingWindowDescription,
|
||||
CUMULATIVE_WINDOW_DESCRIPTION,
|
||||
ROLLING_WINDOW_DESCRIPTION,
|
||||
TIMEZONE_DATA,
|
||||
} from '../constants';
|
||||
import TimeInput from '../TimeInput';
|
||||
@@ -116,9 +116,7 @@ function EvaluationWindowDetails({
|
||||
if (isCurrentHour) {
|
||||
return (
|
||||
<div className="evaluation-window-details">
|
||||
<Typography.Text>
|
||||
{getCumulativeWindowDescription(evaluationWindow.timeframe)}
|
||||
</Typography.Text>
|
||||
<Typography.Text>{CUMULATIVE_WINDOW_DESCRIPTION}</Typography.Text>
|
||||
<Typography.Text>{displayText}</Typography.Text>
|
||||
<div className="select-group">
|
||||
<Typography.Text>STARTING AT MINUTE</Typography.Text>
|
||||
@@ -136,9 +134,7 @@ function EvaluationWindowDetails({
|
||||
if (isCurrentDay) {
|
||||
return (
|
||||
<div className="evaluation-window-details">
|
||||
<Typography.Text>
|
||||
{getCumulativeWindowDescription(evaluationWindow.timeframe)}
|
||||
</Typography.Text>
|
||||
<Typography.Text>{CUMULATIVE_WINDOW_DESCRIPTION}</Typography.Text>
|
||||
<Typography.Text>{displayText}</Typography.Text>
|
||||
<div className="select-group time-select-group">
|
||||
<Typography.Text>STARTING AT</Typography.Text>
|
||||
@@ -163,9 +159,7 @@ function EvaluationWindowDetails({
|
||||
if (isCurrentMonth) {
|
||||
return (
|
||||
<div className="evaluation-window-details">
|
||||
<Typography.Text>
|
||||
{getCumulativeWindowDescription(evaluationWindow.timeframe)}
|
||||
</Typography.Text>
|
||||
<Typography.Text>{CUMULATIVE_WINDOW_DESCRIPTION}</Typography.Text>
|
||||
<Typography.Text>{displayText}</Typography.Text>
|
||||
<div className="select-group">
|
||||
<Typography.Text>STARTING ON DAY</Typography.Text>
|
||||
@@ -198,9 +192,7 @@ function EvaluationWindowDetails({
|
||||
|
||||
return (
|
||||
<div className="evaluation-window-details">
|
||||
<Typography.Text>
|
||||
{getRollingWindowDescription(evaluationWindow.timeframe)}
|
||||
</Typography.Text>
|
||||
<Typography.Text>{ROLLING_WINDOW_DESCRIPTION}</Typography.Text>
|
||||
<Typography.Text>Specify custom duration</Typography.Text>
|
||||
<Typography.Text>{displayText}</Typography.Text>
|
||||
<div className="select-group">
|
||||
|
||||
@@ -3,10 +3,10 @@ import classNames from 'classnames';
|
||||
import { Check } from 'lucide-react';
|
||||
|
||||
import {
|
||||
CUMULATIVE_WINDOW_DESCRIPTION,
|
||||
EVALUATION_WINDOW_TIMEFRAME,
|
||||
EVALUATION_WINDOW_TYPE,
|
||||
getCumulativeWindowDescription,
|
||||
getRollingWindowDescription,
|
||||
ROLLING_WINDOW_DESCRIPTION,
|
||||
} from '../constants';
|
||||
import {
|
||||
CumulativeWindowTimeframes,
|
||||
@@ -96,9 +96,7 @@ function EvaluationWindowPopover({
|
||||
}
|
||||
return (
|
||||
<div className="selection-content">
|
||||
<Typography.Text>
|
||||
{getRollingWindowDescription(evaluationWindow.timeframe)}
|
||||
</Typography.Text>
|
||||
<Typography.Text>{ROLLING_WINDOW_DESCRIPTION}</Typography.Text>
|
||||
<Button type="link">Read the docs</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -110,9 +108,7 @@ function EvaluationWindowPopover({
|
||||
) {
|
||||
return (
|
||||
<div className="selection-content">
|
||||
<Typography.Text>
|
||||
{getCumulativeWindowDescription(evaluationWindow.timeframe)}
|
||||
</Typography.Text>
|
||||
<Typography.Text>{CUMULATIVE_WINDOW_DESCRIPTION}</Typography.Text>
|
||||
<Button type="link">Read the docs</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -33,7 +33,6 @@ describe('AdvancedOptionItem', () => {
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
defaultShowInput={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -51,7 +50,6 @@ describe('AdvancedOptionItem', () => {
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
defaultShowInput={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -67,7 +65,6 @@ describe('AdvancedOptionItem', () => {
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
defaultShowInput={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -91,7 +88,6 @@ describe('AdvancedOptionItem', () => {
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
defaultShowInput={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -121,7 +117,6 @@ describe('AdvancedOptionItem', () => {
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
defaultShowInput={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -151,7 +146,6 @@ describe('AdvancedOptionItem', () => {
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
defaultShowInput={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -166,24 +160,9 @@ describe('AdvancedOptionItem', () => {
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
tooltipText="mock tooltip text"
|
||||
defaultShowInput={false}
|
||||
/>,
|
||||
);
|
||||
const tooltipIcon = screen.getByTestId('tooltip-icon');
|
||||
expect(tooltipIcon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show input when defaultShowInput is true', () => {
|
||||
render(
|
||||
<AdvancedOptionItem
|
||||
title={defaultProps.title}
|
||||
description={defaultProps.description}
|
||||
input={defaultProps.input}
|
||||
defaultShowInput
|
||||
/>,
|
||||
);
|
||||
const inputElement = screen.getByTestId(TEST_INPUT_TEST_ID);
|
||||
expect(inputElement).toBeInTheDocument();
|
||||
expect(inputElement).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,10 +28,9 @@ describe('AdvancedOptions', () => {
|
||||
expect(
|
||||
screen.queryByText(MINIMUM_DATA_REQUIRED_TEXT),
|
||||
).not.toBeInTheDocument();
|
||||
// TODO: Uncomment this when account for data delay is implemented
|
||||
// expect(
|
||||
// screen.queryByText(ACCOUNT_FOR_DATA_DELAY_TEXT),
|
||||
// ).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(ACCOUNT_FOR_DATA_DELAY_TEXT),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should be able to expand the advanced options', () => {
|
||||
@@ -43,10 +42,9 @@ describe('AdvancedOptions', () => {
|
||||
expect(
|
||||
screen.queryByText(MINIMUM_DATA_REQUIRED_TEXT),
|
||||
).not.toBeInTheDocument();
|
||||
// TODO: Uncomment this when account for data delay is implemented
|
||||
// expect(
|
||||
// screen.queryByText(ACCOUNT_FOR_DATA_DELAY_TEXT),
|
||||
// ).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(ACCOUNT_FOR_DATA_DELAY_TEXT),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const collapse = screen.getByRole('button', { name: /ADVANCED OPTIONS/i });
|
||||
fireEvent.click(collapse);
|
||||
@@ -54,8 +52,7 @@ describe('AdvancedOptions', () => {
|
||||
expect(screen.getByText('How often to check')).toBeInTheDocument();
|
||||
expect(screen.getByText('Alert when data stops coming')).toBeInTheDocument();
|
||||
expect(screen.getByText('Minimum data required')).toBeInTheDocument();
|
||||
// TODO: Uncomment this when account for data delay is implemented
|
||||
// expect(screen.getByText('Account for data delay')).toBeInTheDocument();
|
||||
expect(screen.getByText('Account for data delay')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('"Alert when data stops coming" works as expected', () => {
|
||||
@@ -115,7 +112,7 @@ describe('AdvancedOptions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('"Account for data delay" works as expected', () => {
|
||||
it('"Account for data delay" works as expected', () => {
|
||||
render(<AdvancedOptions />);
|
||||
|
||||
const collapse = screen.getByRole('button', { name: /ADVANCED OPTIONS/i });
|
||||
|
||||
@@ -64,12 +64,10 @@ describe('EvaluationCadence', () => {
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Enter time')).toHaveValue(1);
|
||||
expect(screen.getByText('Minutes')).toBeInTheDocument();
|
||||
// TODO: Uncomment this when add custom schedule button is implemented
|
||||
// expect(screen.getByText(ADD_CUSTOM_SCHEDULE_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(ADD_CUSTOM_SCHEDULE_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// TODO: Unskip this when add custom schedule button is implemented
|
||||
it.skip('should hide the input group when add custom schedule button is clicked', () => {
|
||||
it('should hide the input group when add custom schedule button is clicked', () => {
|
||||
render(<EvaluationCadence />);
|
||||
|
||||
expect(
|
||||
@@ -86,14 +84,12 @@ describe('EvaluationCadence', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// TODO: Unskip this when add custom schedule button is implemented
|
||||
it.skip('should not show the edit custom schedule component in default mode', () => {
|
||||
it('should not show the edit custom schedule component in default mode', () => {
|
||||
render(<EvaluationCadence />);
|
||||
expect(screen.queryByTestId('edit-custom-schedule')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// TODO: Unskip this when add custom schedule button is implemented
|
||||
it.skip('should show the custom schedule text when the mode is custom with selected values', () => {
|
||||
it('should show the custom schedule text when the mode is custom with selected values', () => {
|
||||
jest.spyOn(alertState, 'useCreateAlertState').mockReturnValueOnce(
|
||||
createMockAlertContextState({
|
||||
advancedOptions: {
|
||||
@@ -122,8 +118,7 @@ describe('EvaluationCadence', () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// TODO: Unskip this when add custom schedule button is implemented
|
||||
it.skip('should show evaluation cadence details component when clicked on add custom schedule button', () => {
|
||||
it('should show evaluation cadence details component when clicked on add custom schedule button', () => {
|
||||
render(<EvaluationCadence />);
|
||||
|
||||
expect(
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import * as alertState from 'container/CreateAlertV2/context';
|
||||
import * as utils from 'container/CreateAlertV2/utils';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import EvaluationSettings from '../EvaluationSettings';
|
||||
import { createMockAlertContextState } from './testUtils';
|
||||
|
||||
jest.mock('container/CreateAlertV2/utils', () => ({
|
||||
...jest.requireActual('container/CreateAlertV2/utils'),
|
||||
}));
|
||||
|
||||
const mockSetEvaluationWindow = jest.fn();
|
||||
jest.spyOn(alertState, 'useCreateAlertState').mockReturnValue(
|
||||
createMockAlertContextState({
|
||||
@@ -15,14 +13,52 @@ jest.spyOn(alertState, 'useCreateAlertState').mockReturnValue(
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('../AdvancedOptions', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="advanced-options">AdvancedOptions</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const EVALUATION_SETTINGS_TEXT = 'Evaluation settings';
|
||||
const CHECK_CONDITIONS_USING_DATA_FROM_TEXT =
|
||||
'Check conditions using data from';
|
||||
|
||||
describe('EvaluationSettings', () => {
|
||||
it('should render the condensed evaluation settings layout', () => {
|
||||
it('should render the default evaluation settings layout', () => {
|
||||
render(<EvaluationSettings />);
|
||||
expect(screen.getByText(EVALUATION_SETTINGS_TEXT)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(CHECK_CONDITIONS_USING_DATA_FROM_TEXT),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('advanced-options')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render evaluation window for anomaly based alert', () => {
|
||||
jest.spyOn(alertState, 'useCreateAlertState').mockReturnValueOnce(
|
||||
createMockAlertContextState({
|
||||
alertType: AlertTypes.ANOMALY_BASED_ALERT,
|
||||
}),
|
||||
);
|
||||
render(<EvaluationSettings />);
|
||||
expect(screen.getByText(EVALUATION_SETTINGS_TEXT)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(CHECK_CONDITIONS_USING_DATA_FROM_TEXT),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render the condensed evaluation settings layout', () => {
|
||||
jest.spyOn(utils, 'showCondensedLayout').mockReturnValueOnce(true);
|
||||
render(<EvaluationSettings />);
|
||||
// Header, check conditions using data from and advanced options should be hidden
|
||||
expect(screen.queryByText(EVALUATION_SETTINGS_TEXT)).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(CHECK_CONDITIONS_USING_DATA_FROM_TEXT),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('advanced-options')).not.toBeInTheDocument();
|
||||
// Only evaluation window popover should be visible
|
||||
expect(
|
||||
screen.getByTestId('condensed-evaluation-settings-container'),
|
||||
).toBeInTheDocument();
|
||||
// Verify that default option is selected
|
||||
expect(screen.getByText('Rolling')).toBeInTheDocument();
|
||||
expect(screen.getByText('Last 5 minutes')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,12 +24,9 @@ describe('EvaluationWindowDetails', () => {
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
(_, element) =>
|
||||
element?.textContent?.includes(
|
||||
'Monitors data over a fixed time period that moves forward continuously',
|
||||
) ?? false,
|
||||
)[0],
|
||||
screen.getByText(
|
||||
'A Rolling Window has a fixed size and shifts its starting point over time based on when the rules are evaluated.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Specify custom duration')).toBeInTheDocument();
|
||||
expect(screen.getByText('Last 5 Minutes')).toBeInTheDocument();
|
||||
|
||||
@@ -125,12 +125,9 @@ describe('EvaluationWindowPopover', () => {
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
(_, element) =>
|
||||
element?.textContent?.includes(
|
||||
'Monitors data over a fixed time period that moves forward continuously',
|
||||
) ?? false,
|
||||
)[0],
|
||||
screen.getByText(
|
||||
'A Rolling Window has a fixed size and shifts its starting point over time based on when the rules are evaluated.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId(EVALUATION_WINDOW_DETAILS_TEST_ID),
|
||||
|
||||
@@ -26,14 +26,6 @@ export const createMockAlertContextState = (
|
||||
setEvaluationWindow: jest.fn(),
|
||||
notificationSettings: INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
setNotificationSettings: jest.fn(),
|
||||
discardAlertRule: jest.fn(),
|
||||
testAlertRule: jest.fn(),
|
||||
isCreatingAlertRule: false,
|
||||
isTestingAlertRule: false,
|
||||
createAlertRule: jest.fn(),
|
||||
isUpdatingAlertRule: false,
|
||||
updateAlertRule: jest.fn(),
|
||||
isEditMode: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
||||
@@ -62,87 +62,8 @@ export const TIMEZONE_DATA = generateTimezoneData().map((timezone) => ({
|
||||
value: timezone.value,
|
||||
}));
|
||||
|
||||
export const getCumulativeWindowDescription = (timeframe?: string): string => {
|
||||
let example = '';
|
||||
switch (timeframe) {
|
||||
case 'currentHour':
|
||||
example =
|
||||
'An hourly cumulative window for error count alerts when errors exceed 100. Starting at the top of the hour, it tracks: 20 errors by :15, 55 by :30, 105 by :45 (alert fires).';
|
||||
break;
|
||||
case 'currentDay':
|
||||
example =
|
||||
'A daily cumulative window for sales alerts when total revenue exceeds $10,000. Starting at midnight, it tracks: $2,000 by 9 AM, $5,500 by noon, $11,000 by 3 PM (alert fires).';
|
||||
break;
|
||||
case 'currentMonth':
|
||||
example =
|
||||
'A monthly cumulative window for expense alerts when spending exceeds $50,000. Starting on the 1st, it tracks: $15,000 by the 7th, $32,000 by the 15th, $51,000 by the 22nd (alert fires).';
|
||||
break;
|
||||
default:
|
||||
example = '';
|
||||
}
|
||||
return `Monitors data accumulated since a fixed starting point. The window grows over time, keeping all historical data from the start.\n\nExample: ${example}`;
|
||||
};
|
||||
export const CUMULATIVE_WINDOW_DESCRIPTION =
|
||||
'A Cumulative Window has a fixed starting point and expands over time.';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export const getRollingWindowDescription = (duration?: string): string => {
|
||||
let timeWindow = '5-minute';
|
||||
let examples = '14:01:00-14:06:00, 14:02:00-14:07:00';
|
||||
|
||||
if (duration) {
|
||||
const match = duration.match(/^(\d+)([mhs])/);
|
||||
if (match) {
|
||||
const value = parseInt(match[1], 10);
|
||||
const unit = match[2];
|
||||
|
||||
if (unit === 'm' && !Number.isNaN(value)) {
|
||||
timeWindow = `${value}-minute`;
|
||||
const endMinutes1 = 1 + value;
|
||||
const endMinutes2 = 2 + value;
|
||||
examples = `14:01:00-14:${String(endMinutes1).padStart(
|
||||
2,
|
||||
'0',
|
||||
)}:00, 14:02:00-14:${String(endMinutes2).padStart(2, '0')}:00`;
|
||||
} else if (unit === 'h' && !Number.isNaN(value)) {
|
||||
timeWindow = `${value}-hour`;
|
||||
const endHour1 = 14 + value;
|
||||
const endHour2 = 14 + value;
|
||||
examples = `14:00:00-${String(endHour1).padStart(
|
||||
2,
|
||||
'0',
|
||||
)}:00:00, 14:01:00-${String(endHour2).padStart(2, '0')}:01:00`;
|
||||
} else if (unit === 's' && !Number.isNaN(value)) {
|
||||
timeWindow = `${value}-second`;
|
||||
examples = `14:01:00-14:01:${String(value).padStart(
|
||||
2,
|
||||
'0',
|
||||
)}, 14:01:01-14:01:${String(1 + value).padStart(2, '0')}`;
|
||||
}
|
||||
} else if (duration === 'custom') {
|
||||
timeWindow = '5-minute';
|
||||
examples = '14:01:00-14:06:00, 14:02:00-14:07:00';
|
||||
} else if (duration.includes('h')) {
|
||||
const hours = parseInt(duration, 10);
|
||||
if (!Number.isNaN(hours)) {
|
||||
timeWindow = `${hours}-hour`;
|
||||
const endHour = 14 + hours;
|
||||
examples = `14:00:00-${String(endHour).padStart(
|
||||
2,
|
||||
'0',
|
||||
)}:00:00, 14:01:00-${String(endHour).padStart(2, '0')}:01:00`;
|
||||
}
|
||||
} else if (duration.includes('m')) {
|
||||
const minutes = parseInt(duration, 10);
|
||||
if (!Number.isNaN(minutes)) {
|
||||
timeWindow = `${minutes}-minute`;
|
||||
const endMinutes1 = 1 + minutes;
|
||||
const endMinutes2 = 2 + minutes;
|
||||
examples = `14:01:00-14:${String(endMinutes1).padStart(
|
||||
2,
|
||||
'0',
|
||||
)}:00, 14:02:00-14:${String(endMinutes2).padStart(2, '0')}:00`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return `Monitors data over a fixed time period that moves forward continuously.\n\nExample: A ${timeWindow} rolling window for error rate alerts with 1 minute evaluation cadence. Unlike fixed windows, this checks continuously: ${examples}, etc.`;
|
||||
};
|
||||
export const ROLLING_WINDOW_DESCRIPTION =
|
||||
'A Rolling Window has a fixed size and shifts its starting point over time based on when the rules are evaluated.';
|
||||
|
||||
@@ -209,16 +209,6 @@
|
||||
|
||||
.ant-select {
|
||||
width: 40px;
|
||||
|
||||
.ant-select-selector {
|
||||
&:hover {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,14 +231,6 @@
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
color: var(--bg-vanilla-100);
|
||||
height: 32px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -397,14 +379,6 @@
|
||||
background-color: var(--bg-vanilla-300);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
color: var(--bg-ink-400);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-ink-300);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-ink-300);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -11,7 +11,6 @@ export interface IAdvancedOptionItemProps {
|
||||
input: JSX.Element;
|
||||
tooltipText?: string;
|
||||
onToggle?: () => void;
|
||||
defaultShowInput: boolean;
|
||||
}
|
||||
|
||||
export enum RollingWindowTimeframes {
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
import './styles.scss';
|
||||
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { Check, Send, X } from 'lucide-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import {
|
||||
buildCreateThresholdAlertRulePayload,
|
||||
validateCreateAlertState,
|
||||
} from './utils';
|
||||
|
||||
function Footer(): JSX.Element {
|
||||
const {
|
||||
alertType,
|
||||
alertState: basicAlertState,
|
||||
thresholdState,
|
||||
advancedOptions,
|
||||
evaluationWindow,
|
||||
notificationSettings,
|
||||
discardAlertRule,
|
||||
createAlertRule,
|
||||
isCreatingAlertRule,
|
||||
testAlertRule,
|
||||
isTestingAlertRule,
|
||||
updateAlertRule,
|
||||
isUpdatingAlertRule,
|
||||
isEditMode,
|
||||
} = useCreateAlertState();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const handleDiscard = (): void => {
|
||||
discardAlertRule();
|
||||
safeNavigate('/alerts');
|
||||
};
|
||||
|
||||
const alertValidationMessage = useMemo(
|
||||
() =>
|
||||
validateCreateAlertState({
|
||||
alertType,
|
||||
basicAlertState,
|
||||
thresholdState,
|
||||
advancedOptions,
|
||||
evaluationWindow,
|
||||
notificationSettings,
|
||||
query: currentQuery,
|
||||
}),
|
||||
[
|
||||
alertType,
|
||||
basicAlertState,
|
||||
thresholdState,
|
||||
advancedOptions,
|
||||
evaluationWindow,
|
||||
notificationSettings,
|
||||
currentQuery,
|
||||
],
|
||||
);
|
||||
|
||||
const handleTestNotification = useCallback((): void => {
|
||||
const payload = buildCreateThresholdAlertRulePayload({
|
||||
alertType,
|
||||
basicAlertState,
|
||||
thresholdState,
|
||||
advancedOptions,
|
||||
evaluationWindow,
|
||||
notificationSettings,
|
||||
query: currentQuery,
|
||||
});
|
||||
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,
|
||||
thresholdState,
|
||||
advancedOptions,
|
||||
evaluationWindow,
|
||||
notificationSettings,
|
||||
currentQuery,
|
||||
testAlertRule,
|
||||
]);
|
||||
|
||||
const handleSaveAlert = useCallback((): void => {
|
||||
const payload = buildCreateThresholdAlertRulePayload({
|
||||
alertType,
|
||||
basicAlertState,
|
||||
thresholdState,
|
||||
advancedOptions,
|
||||
evaluationWindow,
|
||||
notificationSettings,
|
||||
query: currentQuery,
|
||||
});
|
||||
if (isEditMode) {
|
||||
updateAlertRule(payload, {
|
||||
onSuccess: () => {
|
||||
toast.success('Alert rule updated successfully');
|
||||
safeNavigate('/alerts');
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
createAlertRule(payload, {
|
||||
onSuccess: () => {
|
||||
toast.success('Alert rule created successfully');
|
||||
safeNavigate('/alerts');
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message);
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [
|
||||
alertType,
|
||||
basicAlertState,
|
||||
thresholdState,
|
||||
advancedOptions,
|
||||
evaluationWindow,
|
||||
notificationSettings,
|
||||
currentQuery,
|
||||
isEditMode,
|
||||
updateAlertRule,
|
||||
createAlertRule,
|
||||
safeNavigate,
|
||||
]);
|
||||
|
||||
const disableButtons =
|
||||
isCreatingAlertRule || isTestingAlertRule || isUpdatingAlertRule;
|
||||
|
||||
const saveAlertButton = useMemo(() => {
|
||||
let button = (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSaveAlert}
|
||||
disabled={disableButtons || Boolean(alertValidationMessage)}
|
||||
>
|
||||
<Check size={14} />
|
||||
<Typography.Text>Save Alert Rule</Typography.Text>
|
||||
</Button>
|
||||
);
|
||||
if (alertValidationMessage) {
|
||||
button = <Tooltip title={alertValidationMessage}>{button}</Tooltip>;
|
||||
}
|
||||
return button;
|
||||
}, [alertValidationMessage, disableButtons, handleSaveAlert]);
|
||||
|
||||
const testAlertButton = useMemo(() => {
|
||||
let button = (
|
||||
<Button
|
||||
type="default"
|
||||
onClick={handleTestNotification}
|
||||
disabled={disableButtons || Boolean(alertValidationMessage)}
|
||||
>
|
||||
<Send size={14} />
|
||||
<Typography.Text>Test Notification</Typography.Text>
|
||||
</Button>
|
||||
);
|
||||
if (alertValidationMessage) {
|
||||
button = <Tooltip title={alertValidationMessage}>{button}</Tooltip>;
|
||||
}
|
||||
return button;
|
||||
}, [alertValidationMessage, disableButtons, handleTestNotification]);
|
||||
|
||||
return (
|
||||
<div className="create-alert-v2-footer">
|
||||
<Button type="default" onClick={handleDiscard} disabled={disableButtons}>
|
||||
<X size={14} /> Discard
|
||||
</Button>
|
||||
<div className="button-group">
|
||||
{testAlertButton}
|
||||
{saveAlertButton}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
@@ -1,248 +0,0 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import {
|
||||
AlertThresholdMatchType,
|
||||
AlertThresholdOperator,
|
||||
} from 'container/CreateAlertV2/context/types';
|
||||
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
|
||||
|
||||
import * as createAlertState from '../../context';
|
||||
import Footer from '../Footer';
|
||||
|
||||
// Mock the hooks used by Footer component
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockCreateAlertRule = jest.fn();
|
||||
const mockTestAlertRule = jest.fn();
|
||||
const mockUpdateAlertRule = jest.fn();
|
||||
const mockDiscardAlertRule = jest.fn();
|
||||
|
||||
// Import the mocked hooks
|
||||
const { useQueryBuilder } = jest.requireMock(
|
||||
'hooks/queryBuilder/useQueryBuilder',
|
||||
);
|
||||
const { useSafeNavigate } = jest.requireMock('hooks/useSafeNavigate');
|
||||
|
||||
const mockAlertContextState = createMockAlertContextState({
|
||||
createAlertRule: mockCreateAlertRule,
|
||||
testAlertRule: mockTestAlertRule,
|
||||
updateAlertRule: mockUpdateAlertRule,
|
||||
discardAlertRule: mockDiscardAlertRule,
|
||||
alertState: {
|
||||
name: 'Test Alert',
|
||||
labels: {},
|
||||
yAxisUnit: undefined,
|
||||
},
|
||||
thresholdState: {
|
||||
selectedQuery: 'A',
|
||||
operator: AlertThresholdOperator.ABOVE_BELOW,
|
||||
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
|
||||
evaluationWindow: '5m0s',
|
||||
algorithm: 'standard',
|
||||
seasonality: 'hourly',
|
||||
thresholds: [
|
||||
{
|
||||
id: '1',
|
||||
label: 'CRITICAL',
|
||||
thresholdValue: 0,
|
||||
recoveryThresholdValue: null,
|
||||
unit: '',
|
||||
channels: ['test-channel'],
|
||||
color: '#ff0000',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(createAlertState, 'useCreateAlertState')
|
||||
.mockReturnValue(mockAlertContextState);
|
||||
|
||||
const SAVE_ALERT_RULE_TEXT = 'Save Alert Rule';
|
||||
const TEST_NOTIFICATION_TEXT = 'Test Notification';
|
||||
const DISCARD_TEXT = 'Discard';
|
||||
|
||||
describe('Footer', () => {
|
||||
beforeEach(() => {
|
||||
useQueryBuilder.mockReturnValue({
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [],
|
||||
queryFormulas: [],
|
||||
},
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
queryType: 'builder',
|
||||
},
|
||||
});
|
||||
|
||||
useSafeNavigate.mockReturnValue({
|
||||
safeNavigate: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the component with 3 buttons', () => {
|
||||
render(<Footer />);
|
||||
expect(screen.getByText(SAVE_ALERT_RULE_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(TEST_NOTIFICATION_TEXT)).toBeInTheDocument();
|
||||
expect(screen.getByText(DISCARD_TEXT)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('discard action works correctly', () => {
|
||||
render(<Footer />);
|
||||
fireEvent.click(screen.getByText(DISCARD_TEXT));
|
||||
expect(mockDiscardAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('save alert rule action works correctly', () => {
|
||||
render(<Footer />);
|
||||
fireEvent.click(screen.getByText(SAVE_ALERT_RULE_TEXT));
|
||||
expect(mockCreateAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('update alert rule action works correctly', () => {
|
||||
jest.spyOn(createAlertState, 'useCreateAlertState').mockReturnValueOnce({
|
||||
...mockAlertContextState,
|
||||
isEditMode: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
fireEvent.click(screen.getByText(SAVE_ALERT_RULE_TEXT));
|
||||
expect(mockUpdateAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('test notification action works correctly', () => {
|
||||
render(<Footer />);
|
||||
fireEvent.click(screen.getByText(TEST_NOTIFICATION_TEXT));
|
||||
expect(mockTestAlertRule).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('all buttons are disabled when creating alert rule', () => {
|
||||
jest.spyOn(createAlertState, 'useCreateAlertState').mockReturnValueOnce({
|
||||
...mockAlertContextState,
|
||||
isCreatingAlertRule: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /test notification/i }),
|
||||
).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: /discard/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('all buttons are disabled when updating alert rule', () => {
|
||||
jest.spyOn(createAlertState, 'useCreateAlertState').mockReturnValueOnce({
|
||||
...mockAlertContextState,
|
||||
isUpdatingAlertRule: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
|
||||
// Target the button elements directly instead of the text spans inside them
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /test notification/i }),
|
||||
).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: /discard/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('all buttons are disabled when testing alert rule', () => {
|
||||
jest.spyOn(createAlertState, 'useCreateAlertState').mockReturnValueOnce({
|
||||
...mockAlertContextState,
|
||||
isTestingAlertRule: true,
|
||||
});
|
||||
render(<Footer />);
|
||||
|
||||
// Target the button elements directly instead of the text spans inside them
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /test notification/i }),
|
||||
).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: /discard/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it('create and test buttons are disabled when alert name is missing', () => {
|
||||
jest.spyOn(createAlertState, 'useCreateAlertState').mockReturnValueOnce({
|
||||
...mockAlertContextState,
|
||||
alertState: {
|
||||
...mockAlertContextState.alertState,
|
||||
name: '',
|
||||
},
|
||||
});
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /test notification/i }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it('create and test buttons are disabled when notifcation channels are missing and routing policies are disabled', () => {
|
||||
jest.spyOn(createAlertState, 'useCreateAlertState').mockReturnValueOnce({
|
||||
...mockAlertContextState,
|
||||
notificationSettings: {
|
||||
...mockAlertContextState.notificationSettings,
|
||||
routingPolicies: false,
|
||||
},
|
||||
thresholdState: {
|
||||
...mockAlertContextState.thresholdState,
|
||||
thresholds: [
|
||||
{
|
||||
...mockAlertContextState.thresholdState.thresholds[0],
|
||||
channels: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
).toBeDisabled();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /test notification/i }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it('buttons are enabled even with no notification channels when routing policies are enabled', () => {
|
||||
jest.spyOn(createAlertState, 'useCreateAlertState').mockReturnValueOnce({
|
||||
...mockAlertContextState,
|
||||
notificationSettings: {
|
||||
...mockAlertContextState.notificationSettings,
|
||||
routingPolicies: true,
|
||||
},
|
||||
thresholdState: {
|
||||
...mockAlertContextState.thresholdState,
|
||||
thresholds: [
|
||||
{
|
||||
...mockAlertContextState.thresholdState.thresholds[0],
|
||||
channels: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
render(<Footer />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: /save alert rule/i }),
|
||||
).toBeEnabled();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /test notification/i }),
|
||||
).toBeEnabled();
|
||||
expect(screen.getByRole('button', { name: /discard/i })).toBeEnabled();
|
||||
});
|
||||
});
|
||||
@@ -1,524 +0,0 @@
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import {
|
||||
INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
INITIAL_ALERT_STATE,
|
||||
INITIAL_ALERT_THRESHOLD_STATE,
|
||||
INITIAL_EVALUATION_WINDOW_STATE,
|
||||
INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
} from 'container/CreateAlertV2/context/constants';
|
||||
import {
|
||||
AdvancedOptionsState,
|
||||
EvaluationWindowState,
|
||||
NotificationSettingsState,
|
||||
} from 'container/CreateAlertV2/context/types';
|
||||
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { BuildCreateAlertRulePayloadArgs } from '../types';
|
||||
import {
|
||||
buildCreateThresholdAlertRulePayload,
|
||||
getAlertOnAbsentProps,
|
||||
getEnforceMinimumDatapointsProps,
|
||||
getEvaluationProps,
|
||||
getFormattedTimeValue,
|
||||
getNotificationSettingsProps,
|
||||
validateCreateAlertState,
|
||||
} from '../utils';
|
||||
|
||||
describe('Footer utils', () => {
|
||||
describe('getFormattedTimeValue', () => {
|
||||
it('for 60 seconds', () => {
|
||||
expect(getFormattedTimeValue(60, UniversalYAxisUnit.SECONDS)).toBe('60s');
|
||||
});
|
||||
it('for 60 minutes', () => {
|
||||
expect(getFormattedTimeValue(60, UniversalYAxisUnit.MINUTES)).toBe('60m');
|
||||
});
|
||||
it('for 60 hours', () => {
|
||||
expect(getFormattedTimeValue(60, UniversalYAxisUnit.HOURS)).toBe('60h');
|
||||
});
|
||||
it('for 60 days', () => {
|
||||
expect(getFormattedTimeValue(60, UniversalYAxisUnit.DAYS)).toBe('60d');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateCreateAlertState', () => {
|
||||
const args: BuildCreateAlertRulePayloadArgs = {
|
||||
alertType: AlertTypes.METRICS_BASED_ALERT,
|
||||
basicAlertState: INITIAL_ALERT_STATE,
|
||||
thresholdState: INITIAL_ALERT_THRESHOLD_STATE,
|
||||
advancedOptions: INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
evaluationWindow: INITIAL_EVALUATION_WINDOW_STATE,
|
||||
notificationSettings: INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
query: initialQueriesMap.metrics,
|
||||
};
|
||||
|
||||
it('when alert name is not provided', () => {
|
||||
expect(validateCreateAlertState(args)).toBeDefined();
|
||||
expect(validateCreateAlertState(args)).toBe('Please enter an alert name');
|
||||
});
|
||||
|
||||
it('when threshold label is not provided', () => {
|
||||
const currentArgs: BuildCreateAlertRulePayloadArgs = {
|
||||
...args,
|
||||
basicAlertState: {
|
||||
...args.basicAlertState,
|
||||
name: 'test name',
|
||||
},
|
||||
thresholdState: {
|
||||
...args.thresholdState,
|
||||
thresholds: [
|
||||
{
|
||||
...args.thresholdState.thresholds[0],
|
||||
label: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(validateCreateAlertState(currentArgs)).toBeDefined();
|
||||
expect(validateCreateAlertState(currentArgs)).toBe(
|
||||
'Please enter a label for each threshold',
|
||||
);
|
||||
});
|
||||
|
||||
it('when threshold channels are not provided', () => {
|
||||
const currentArgs: BuildCreateAlertRulePayloadArgs = {
|
||||
...args,
|
||||
basicAlertState: {
|
||||
...args.basicAlertState,
|
||||
name: 'test name',
|
||||
},
|
||||
};
|
||||
expect(validateCreateAlertState(currentArgs)).toBeDefined();
|
||||
expect(validateCreateAlertState(currentArgs)).toBe(
|
||||
'Please select at least one channel for each threshold or enable routing policies',
|
||||
);
|
||||
});
|
||||
|
||||
it('when threshold channels are not provided but routing policies are enabled', () => {
|
||||
const currentArgs: BuildCreateAlertRulePayloadArgs = {
|
||||
...args,
|
||||
basicAlertState: {
|
||||
...args.basicAlertState,
|
||||
name: 'test name',
|
||||
},
|
||||
notificationSettings: {
|
||||
...args.notificationSettings,
|
||||
routingPolicies: true,
|
||||
},
|
||||
};
|
||||
expect(validateCreateAlertState(currentArgs)).toBeNull();
|
||||
});
|
||||
|
||||
it('when threshold channels are provided', () => {
|
||||
const currentArgs: BuildCreateAlertRulePayloadArgs = {
|
||||
...args,
|
||||
basicAlertState: {
|
||||
...args.basicAlertState,
|
||||
name: 'test name',
|
||||
},
|
||||
thresholdState: {
|
||||
...args.thresholdState,
|
||||
thresholds: [
|
||||
{
|
||||
...args.thresholdState.thresholds[0],
|
||||
channels: ['test channel'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
expect(validateCreateAlertState(currentArgs)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNotificationSettingsProps', () => {
|
||||
it('when initial notification settings are provided', () => {
|
||||
const notificationSettings = INITIAL_NOTIFICATION_SETTINGS_STATE;
|
||||
const props = getNotificationSettingsProps(notificationSettings);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
groupBy: [],
|
||||
renotify: {
|
||||
enabled: false,
|
||||
interval: '30m',
|
||||
alertStates: [],
|
||||
},
|
||||
usePolicy: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renotification is enabled', () => {
|
||||
const notificationSettings: NotificationSettingsState = {
|
||||
...INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
reNotification: {
|
||||
enabled: true,
|
||||
value: 30,
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
conditions: ['firing'],
|
||||
},
|
||||
};
|
||||
const props = getNotificationSettingsProps(notificationSettings);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
groupBy: [],
|
||||
renotify: {
|
||||
enabled: true,
|
||||
interval: '30m',
|
||||
alertStates: ['firing'],
|
||||
},
|
||||
usePolicy: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('routing policies are enabled', () => {
|
||||
const notificationSettings: NotificationSettingsState = {
|
||||
...INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
routingPolicies: true,
|
||||
};
|
||||
const props = getNotificationSettingsProps(notificationSettings);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
groupBy: [],
|
||||
renotify: {
|
||||
enabled: false,
|
||||
interval: '30m',
|
||||
alertStates: [],
|
||||
},
|
||||
usePolicy: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('group by notifications are provided', () => {
|
||||
const notificationSettings: NotificationSettingsState = {
|
||||
...INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
multipleNotifications: ['test group'],
|
||||
};
|
||||
const props = getNotificationSettingsProps(notificationSettings);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
groupBy: ['test group'],
|
||||
renotify: {
|
||||
enabled: false,
|
||||
interval: '30m',
|
||||
alertStates: [],
|
||||
},
|
||||
usePolicy: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAlertOnAbsentProps', () => {
|
||||
it('when alert on absent is disabled', () => {
|
||||
const advancedOptions: AdvancedOptionsState = {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
sendNotificationIfDataIsMissing: {
|
||||
enabled: false,
|
||||
toleranceLimit: 0,
|
||||
timeUnit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
};
|
||||
const props = getAlertOnAbsentProps(advancedOptions);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
alertOnAbsent: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('when alert on absent is enabled', () => {
|
||||
const advancedOptions: AdvancedOptionsState = {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
sendNotificationIfDataIsMissing: {
|
||||
enabled: true,
|
||||
toleranceLimit: 13,
|
||||
timeUnit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
};
|
||||
const props = getAlertOnAbsentProps(advancedOptions);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
alertOnAbsent: true,
|
||||
absentFor: 13,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEnforceMinimumDatapointsProps', () => {
|
||||
it('when enforce minimum datapoints is disabled', () => {
|
||||
const advancedOptions: AdvancedOptionsState = {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
enforceMinimumDatapoints: {
|
||||
enabled: false,
|
||||
minimumDatapoints: 0,
|
||||
},
|
||||
};
|
||||
const props = getEnforceMinimumDatapointsProps(advancedOptions);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
requireMinPoints: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('when enforce minimum datapoints is enabled', () => {
|
||||
const advancedOptions: AdvancedOptionsState = {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
enforceMinimumDatapoints: {
|
||||
enabled: true,
|
||||
minimumDatapoints: 12,
|
||||
},
|
||||
};
|
||||
const props = getEnforceMinimumDatapointsProps(advancedOptions);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
requireMinPoints: true,
|
||||
requiredNumPoints: 12,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEvaluationProps', () => {
|
||||
const advancedOptions: AdvancedOptionsState = {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
evaluationCadence: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE.evaluationCadence,
|
||||
mode: 'default',
|
||||
default: {
|
||||
value: 12,
|
||||
timeUnit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('for rolling window with non-custom timeframe', () => {
|
||||
const evaluationWindow: EvaluationWindowState = {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE,
|
||||
windowType: 'rolling',
|
||||
timeframe: '5m0s',
|
||||
};
|
||||
const props = getEvaluationProps(evaluationWindow, advancedOptions);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
kind: 'rolling',
|
||||
spec: {
|
||||
evalWindow: '5m0s',
|
||||
frequency: '12m',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('for rolling window with custom timeframe', () => {
|
||||
const evaluationWindow: EvaluationWindowState = {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE,
|
||||
windowType: 'rolling',
|
||||
timeframe: 'custom',
|
||||
startingAt: {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE.startingAt,
|
||||
number: '13',
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
};
|
||||
const props = getEvaluationProps(evaluationWindow, advancedOptions);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
kind: 'rolling',
|
||||
spec: {
|
||||
evalWindow: '13m',
|
||||
frequency: '12m',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('for cumulative window with current hour', () => {
|
||||
const evaluationWindow: EvaluationWindowState = {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE,
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'currentHour',
|
||||
startingAt: {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE.startingAt,
|
||||
number: '14',
|
||||
timezone: 'UTC',
|
||||
},
|
||||
};
|
||||
const props = getEvaluationProps(evaluationWindow, advancedOptions);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
kind: 'cumulative',
|
||||
spec: {
|
||||
schedule: { type: 'hourly', minute: 14 },
|
||||
frequency: '12m',
|
||||
timezone: 'UTC',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('for cumulative window with current day', () => {
|
||||
const evaluationWindow: EvaluationWindowState = {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE,
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'currentDay',
|
||||
startingAt: {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE.startingAt,
|
||||
time: '15:43:00',
|
||||
timezone: 'UTC',
|
||||
},
|
||||
};
|
||||
const props = getEvaluationProps(evaluationWindow, advancedOptions);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
kind: 'cumulative',
|
||||
spec: {
|
||||
schedule: { type: 'daily', hour: 15, minute: 43 },
|
||||
frequency: '12m',
|
||||
timezone: 'UTC',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('for cumulative window with current month', () => {
|
||||
const evaluationWindow: EvaluationWindowState = {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE,
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'currentMonth',
|
||||
startingAt: {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE.startingAt,
|
||||
number: '17',
|
||||
timezone: 'UTC',
|
||||
time: '16:34:00',
|
||||
},
|
||||
};
|
||||
const props = getEvaluationProps(evaluationWindow, advancedOptions);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
kind: 'cumulative',
|
||||
spec: {
|
||||
schedule: { type: 'monthly', day: 17, hour: 16, minute: 34 },
|
||||
frequency: '12m',
|
||||
timezone: 'UTC',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildCreateThresholdAlertRulePayload', () => {
|
||||
const mockCreateAlertContextState = createMockAlertContextState();
|
||||
const INITIAL_BUILD_CREATE_ALERT_RULE_PAYLOAD_ARGS: BuildCreateAlertRulePayloadArgs = {
|
||||
basicAlertState: mockCreateAlertContextState.alertState,
|
||||
thresholdState: mockCreateAlertContextState.thresholdState,
|
||||
advancedOptions: mockCreateAlertContextState.advancedOptions,
|
||||
evaluationWindow: mockCreateAlertContextState.evaluationWindow,
|
||||
notificationSettings: mockCreateAlertContextState.notificationSettings,
|
||||
query: initialQueriesMap.metrics,
|
||||
alertType: mockCreateAlertContextState.alertType,
|
||||
};
|
||||
|
||||
it('verify buildCreateThresholdAlertRulePayload', () => {
|
||||
const props = buildCreateThresholdAlertRulePayload(
|
||||
INITIAL_BUILD_CREATE_ALERT_RULE_PAYLOAD_ARGS,
|
||||
);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toStrictEqual({
|
||||
alert: '',
|
||||
alertType: 'METRIC_BASED_ALERT',
|
||||
annotations: {
|
||||
description:
|
||||
'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})',
|
||||
summary:
|
||||
'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})',
|
||||
},
|
||||
condition: {
|
||||
alertOnAbsent: false,
|
||||
compositeQuery: {
|
||||
builderQueries: undefined,
|
||||
chQueries: undefined,
|
||||
panelType: 'graph',
|
||||
promQueries: undefined,
|
||||
queries: [
|
||||
{
|
||||
spec: {
|
||||
aggregations: [
|
||||
{
|
||||
metricName: '',
|
||||
reduceTo: undefined,
|
||||
spaceAggregation: 'sum',
|
||||
temporality: undefined,
|
||||
timeAggregation: 'count',
|
||||
},
|
||||
],
|
||||
disabled: false,
|
||||
filter: {
|
||||
expression: '',
|
||||
},
|
||||
functions: undefined,
|
||||
groupBy: undefined,
|
||||
having: undefined,
|
||||
legend: undefined,
|
||||
limit: undefined,
|
||||
name: 'A',
|
||||
offset: undefined,
|
||||
order: undefined,
|
||||
selectFields: undefined,
|
||||
signal: 'metrics',
|
||||
source: '',
|
||||
stepInterval: null,
|
||||
},
|
||||
type: 'builder_query',
|
||||
},
|
||||
],
|
||||
queryType: 'builder',
|
||||
unit: undefined,
|
||||
},
|
||||
requireMinPoints: false,
|
||||
selectedQueryName: 'A',
|
||||
thresholds: {
|
||||
kind: 'basic',
|
||||
spec: [
|
||||
{
|
||||
channels: [],
|
||||
matchType: '1',
|
||||
name: 'critical',
|
||||
op: '1',
|
||||
target: 0,
|
||||
targetUnit: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
evaluation: {
|
||||
kind: 'rolling',
|
||||
spec: {
|
||||
evalWindow: '5m0s',
|
||||
frequency: '1m',
|
||||
},
|
||||
},
|
||||
labels: {},
|
||||
notificationSettings: {
|
||||
groupBy: [],
|
||||
renotify: {
|
||||
enabled: false,
|
||||
interval: '30m',
|
||||
alertStates: [],
|
||||
},
|
||||
usePolicy: false,
|
||||
},
|
||||
ruleType: 'threshold_rule',
|
||||
schemaVersion: 'v2alpha1',
|
||||
source: 'http://localhost/',
|
||||
version: 'v5',
|
||||
});
|
||||
});
|
||||
|
||||
it('verify for promql query type', () => {
|
||||
const currentArgs: BuildCreateAlertRulePayloadArgs = {
|
||||
...INITIAL_BUILD_CREATE_ALERT_RULE_PAYLOAD_ARGS,
|
||||
query: {
|
||||
...INITIAL_BUILD_CREATE_ALERT_RULE_PAYLOAD_ARGS.query,
|
||||
queryType: EQueryType.PROM,
|
||||
},
|
||||
};
|
||||
const props = buildCreateThresholdAlertRulePayload(currentArgs);
|
||||
expect(props).toBeDefined();
|
||||
expect(props.condition.compositeQuery.queryType).toBe('promql');
|
||||
expect(props.ruleType).toBe('promql_rule');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
import Footer from './Footer';
|
||||
|
||||
export default Footer;
|
||||
@@ -1,51 +0,0 @@
|
||||
.create-alert-v2-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 63px;
|
||||
right: 0;
|
||||
background-color: var(--bg-ink-500);
|
||||
height: 70px;
|
||||
border-top: 1px solid var(--bg-slate-500);
|
||||
padding: 16px 24px;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ant-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ant-btn-default {
|
||||
background-color: var(--bg-slate-500);
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
color: var(--bg-vanilla-400);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.create-alert-v2-footer {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
border-top: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
.ant-btn-default {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
border: 1px solid var(--bg-vanilla-400);
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
|
||||
.ant-btn-primary {
|
||||
.ant-typography {
|
||||
color: var(--bg-vanilla-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
AdvancedOptionsState,
|
||||
AlertState,
|
||||
AlertThresholdState,
|
||||
EvaluationWindowState,
|
||||
NotificationSettingsState,
|
||||
} from '../context/types';
|
||||
|
||||
export interface BuildCreateAlertRulePayloadArgs {
|
||||
alertType: AlertTypes;
|
||||
basicAlertState: AlertState;
|
||||
thresholdState: AlertThresholdState;
|
||||
advancedOptions: AdvancedOptionsState;
|
||||
evaluationWindow: EvaluationWindowState;
|
||||
notificationSettings: NotificationSettingsState;
|
||||
query: Query;
|
||||
}
|
||||
@@ -1,345 +0,0 @@
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||
import {
|
||||
BasicThreshold,
|
||||
PostableAlertRuleV2,
|
||||
} from 'types/api/alerts/alertTypesV2';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { compositeQueryToQueryEnvelope } from 'utils/compositeQueryToQueryEnvelope';
|
||||
|
||||
import {
|
||||
AdvancedOptionsState,
|
||||
EvaluationWindowState,
|
||||
NotificationSettingsState,
|
||||
} from '../context/types';
|
||||
import { BuildCreateAlertRulePayloadArgs } from './types';
|
||||
|
||||
// Get formatted time/unit pairs for create alert api payload
|
||||
export function getFormattedTimeValue(timeValue: number, unit: string): string {
|
||||
const unitMap: Record<string, string> = {
|
||||
[UniversalYAxisUnit.SECONDS]: 's',
|
||||
[UniversalYAxisUnit.MINUTES]: 'm',
|
||||
[UniversalYAxisUnit.HOURS]: 'h',
|
||||
[UniversalYAxisUnit.DAYS]: 'd',
|
||||
};
|
||||
return `${timeValue}${unitMap[unit]}`;
|
||||
}
|
||||
|
||||
// Validate create alert api payload
|
||||
export function validateCreateAlertState(
|
||||
args: BuildCreateAlertRulePayloadArgs,
|
||||
): string | null {
|
||||
const { basicAlertState, thresholdState, notificationSettings } = args;
|
||||
|
||||
// Validate alert name
|
||||
if (!basicAlertState.name) {
|
||||
return 'Please enter an alert name';
|
||||
}
|
||||
|
||||
// Validate threshold state if routing policies is not enabled
|
||||
for (let i = 0; i < thresholdState.thresholds.length; i++) {
|
||||
const threshold = thresholdState.thresholds[i];
|
||||
if (!threshold.label) {
|
||||
return 'Please enter a label for each threshold';
|
||||
}
|
||||
if (!notificationSettings.routingPolicies && !threshold.channels.length) {
|
||||
return 'Please select at least one channel for each threshold or enable routing policies';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get notification settings props for create alert api payload
|
||||
export function getNotificationSettingsProps(
|
||||
notificationSettings: NotificationSettingsState,
|
||||
): PostableAlertRuleV2['notificationSettings'] {
|
||||
const notificationSettingsProps: PostableAlertRuleV2['notificationSettings'] = {
|
||||
groupBy: notificationSettings.multipleNotifications || [],
|
||||
usePolicy: notificationSettings.routingPolicies,
|
||||
renotify: {
|
||||
enabled: notificationSettings.reNotification.enabled,
|
||||
interval: getFormattedTimeValue(
|
||||
notificationSettings.reNotification.value,
|
||||
notificationSettings.reNotification.unit,
|
||||
),
|
||||
alertStates: notificationSettings.reNotification.conditions,
|
||||
},
|
||||
};
|
||||
|
||||
return notificationSettingsProps;
|
||||
}
|
||||
|
||||
// Get alert on absent props for create alert api payload
|
||||
export function getAlertOnAbsentProps(
|
||||
advancedOptions: AdvancedOptionsState,
|
||||
): Partial<PostableAlertRuleV2['condition']> {
|
||||
if (advancedOptions.sendNotificationIfDataIsMissing.enabled) {
|
||||
return {
|
||||
alertOnAbsent: true,
|
||||
absentFor: advancedOptions.sendNotificationIfDataIsMissing.toleranceLimit,
|
||||
};
|
||||
}
|
||||
return {
|
||||
alertOnAbsent: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Get enforce minimum datapoints props for create alert api payload
|
||||
export function getEnforceMinimumDatapointsProps(
|
||||
advancedOptions: AdvancedOptionsState,
|
||||
): Partial<PostableAlertRuleV2['condition']> {
|
||||
if (advancedOptions.enforceMinimumDatapoints.enabled) {
|
||||
return {
|
||||
requireMinPoints: true,
|
||||
requiredNumPoints:
|
||||
advancedOptions.enforceMinimumDatapoints.minimumDatapoints,
|
||||
};
|
||||
}
|
||||
return {
|
||||
requireMinPoints: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Get evaluation props for create alert api payload
|
||||
export function getEvaluationProps(
|
||||
evaluationWindow: EvaluationWindowState,
|
||||
advancedOptions: AdvancedOptionsState,
|
||||
): PostableAlertRuleV2['evaluation'] {
|
||||
const frequency = getFormattedTimeValue(
|
||||
advancedOptions.evaluationCadence.default.value,
|
||||
advancedOptions.evaluationCadence.default.timeUnit,
|
||||
);
|
||||
|
||||
if (
|
||||
evaluationWindow.windowType === 'rolling' &&
|
||||
evaluationWindow.timeframe !== 'custom'
|
||||
) {
|
||||
return {
|
||||
kind: evaluationWindow.windowType,
|
||||
spec: {
|
||||
evalWindow: evaluationWindow.timeframe,
|
||||
frequency,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
evaluationWindow.windowType === 'rolling' &&
|
||||
evaluationWindow.timeframe === 'custom'
|
||||
) {
|
||||
return {
|
||||
kind: evaluationWindow.windowType,
|
||||
spec: {
|
||||
evalWindow: getFormattedTimeValue(
|
||||
Number(evaluationWindow.startingAt.number),
|
||||
evaluationWindow.startingAt.unit,
|
||||
),
|
||||
frequency,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Only cumulative window type left now
|
||||
if (evaluationWindow.timeframe === 'currentHour') {
|
||||
return {
|
||||
kind: evaluationWindow.windowType,
|
||||
spec: {
|
||||
schedule: {
|
||||
type: 'hourly',
|
||||
minute: Number(evaluationWindow.startingAt.number),
|
||||
},
|
||||
frequency,
|
||||
timezone: evaluationWindow.startingAt.timezone,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (evaluationWindow.timeframe === 'currentDay') {
|
||||
// time is in the format of "HH:MM:SS"
|
||||
const [hour, minute] = evaluationWindow.startingAt.time.split(':');
|
||||
return {
|
||||
kind: evaluationWindow.windowType,
|
||||
spec: {
|
||||
schedule: {
|
||||
type: 'daily',
|
||||
hour: Number(hour),
|
||||
minute: Number(minute),
|
||||
},
|
||||
frequency,
|
||||
timezone: evaluationWindow.startingAt.timezone,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (evaluationWindow.timeframe === 'currentMonth') {
|
||||
// time is in the format of "HH:MM:SS"
|
||||
const [hour, minute] = evaluationWindow.startingAt.time.split(':');
|
||||
return {
|
||||
kind: evaluationWindow.windowType,
|
||||
spec: {
|
||||
schedule: {
|
||||
type: 'monthly',
|
||||
day: Number(evaluationWindow.startingAt.number),
|
||||
hour: Number(hour),
|
||||
minute: Number(minute),
|
||||
},
|
||||
frequency,
|
||||
timezone: evaluationWindow.startingAt.timezone,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: evaluationWindow.windowType,
|
||||
spec: {
|
||||
evalWindow: evaluationWindow.timeframe,
|
||||
frequency,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Build Create Threshold Alert Rule Payload
|
||||
export function buildCreateThresholdAlertRulePayload(
|
||||
args: BuildCreateAlertRulePayloadArgs,
|
||||
): PostableAlertRuleV2 {
|
||||
const {
|
||||
alertType,
|
||||
basicAlertState,
|
||||
thresholdState,
|
||||
evaluationWindow,
|
||||
advancedOptions,
|
||||
notificationSettings,
|
||||
query,
|
||||
} = args;
|
||||
|
||||
const compositeQuery = compositeQueryToQueryEnvelope({
|
||||
builderQueries: {
|
||||
...mapQueryDataToApi(query.builder.queryData, 'queryName').data,
|
||||
...mapQueryDataToApi(query.builder.queryFormulas, 'queryName').data,
|
||||
},
|
||||
promQueries: mapQueryDataToApi(query.promql, 'name').data,
|
||||
chQueries: mapQueryDataToApi(query.clickhouse_sql, 'name').data,
|
||||
queryType: query.queryType,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
unit: basicAlertState.yAxisUnit,
|
||||
});
|
||||
|
||||
// Thresholds
|
||||
const thresholds: BasicThreshold[] = thresholdState.thresholds.map(
|
||||
(threshold) => ({
|
||||
name: threshold.label,
|
||||
target: parseFloat(threshold.thresholdValue.toString()),
|
||||
matchType: thresholdState.matchType,
|
||||
op: thresholdState.operator,
|
||||
channels: threshold.channels,
|
||||
targetUnit: threshold.unit,
|
||||
}),
|
||||
);
|
||||
|
||||
// Alert on absent data
|
||||
const alertOnAbsentProps = getAlertOnAbsentProps(advancedOptions);
|
||||
|
||||
// Enforce minimum datapoints
|
||||
const enforceMinimumDatapointsProps = getEnforceMinimumDatapointsProps(
|
||||
advancedOptions,
|
||||
);
|
||||
|
||||
// Notification settings
|
||||
const notificationSettingsProps = getNotificationSettingsProps(
|
||||
notificationSettings,
|
||||
);
|
||||
|
||||
// Evaluation
|
||||
const evaluationProps = getEvaluationProps(evaluationWindow, advancedOptions);
|
||||
|
||||
let ruleType: string = AlertDetectionTypes.THRESHOLD_ALERT;
|
||||
if (query.queryType === EQueryType.PROM) {
|
||||
ruleType = 'promql_rule';
|
||||
}
|
||||
|
||||
return {
|
||||
alert: basicAlertState.name,
|
||||
ruleType,
|
||||
alertType,
|
||||
condition: {
|
||||
thresholds: {
|
||||
kind: 'basic',
|
||||
spec: thresholds,
|
||||
},
|
||||
compositeQuery,
|
||||
selectedQueryName: thresholdState.selectedQuery,
|
||||
...alertOnAbsentProps,
|
||||
...enforceMinimumDatapointsProps,
|
||||
},
|
||||
evaluation: evaluationProps,
|
||||
labels: basicAlertState.labels,
|
||||
annotations: {
|
||||
description: notificationSettings.description,
|
||||
summary: notificationSettings.description,
|
||||
},
|
||||
notificationSettings: notificationSettingsProps,
|
||||
version: 'v5',
|
||||
schemaVersion: 'v2alpha1',
|
||||
source: window?.location.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
// Build Create Anomaly Alert Rule Payload
|
||||
// TODO: Update this function before enabling anomaly alert rule creation
|
||||
export function buildCreateAnomalyAlertRulePayload(
|
||||
args: BuildCreateAlertRulePayloadArgs,
|
||||
): PostableAlertRuleV2 {
|
||||
const {
|
||||
alertType,
|
||||
basicAlertState,
|
||||
query,
|
||||
notificationSettings,
|
||||
evaluationWindow,
|
||||
advancedOptions,
|
||||
} = args;
|
||||
|
||||
const compositeQuery = compositeQueryToQueryEnvelope({
|
||||
builderQueries: {
|
||||
...mapQueryDataToApi(query.builder.queryData, 'queryName').data,
|
||||
...mapQueryDataToApi(query.builder.queryFormulas, 'queryName').data,
|
||||
},
|
||||
promQueries: mapQueryDataToApi(query.promql, 'name').data,
|
||||
chQueries: mapQueryDataToApi(query.clickhouse_sql, 'name').data,
|
||||
queryType: query.queryType,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
unit: basicAlertState.yAxisUnit,
|
||||
});
|
||||
|
||||
const alertOnAbsentProps = getAlertOnAbsentProps(advancedOptions);
|
||||
const enforceMinimumDatapointsProps = getEnforceMinimumDatapointsProps(
|
||||
advancedOptions,
|
||||
);
|
||||
const evaluationProps = getEvaluationProps(evaluationWindow, advancedOptions);
|
||||
const notificationSettingsProps = getNotificationSettingsProps(
|
||||
notificationSettings,
|
||||
);
|
||||
|
||||
return {
|
||||
alert: basicAlertState.name,
|
||||
ruleType: AlertDetectionTypes.ANOMALY_DETECTION_ALERT,
|
||||
alertType,
|
||||
condition: {
|
||||
compositeQuery,
|
||||
...alertOnAbsentProps,
|
||||
...enforceMinimumDatapointsProps,
|
||||
},
|
||||
labels: basicAlertState.labels,
|
||||
annotations: {
|
||||
description: notificationSettings.description,
|
||||
summary: notificationSettings.description,
|
||||
},
|
||||
notificationSettings: notificationSettingsProps,
|
||||
evaluation: evaluationProps,
|
||||
version: '',
|
||||
schemaVersion: '',
|
||||
source: window?.location.toString(),
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import { Button, Popover, Tooltip, Typography } from 'antd';
|
||||
import TextArea from 'antd/lib/input/TextArea';
|
||||
import { Info } from 'lucide-react';
|
||||
|
||||
@@ -10,46 +10,46 @@ function NotificationMessage(): JSX.Element {
|
||||
setNotificationSettings,
|
||||
} = useCreateAlertState();
|
||||
|
||||
// const templateVariables = [
|
||||
// { variable: '{{alertname}}', description: 'Name of the alert rule' },
|
||||
// {
|
||||
// variable: '{{value}}',
|
||||
// description: 'Current value that triggered the alert',
|
||||
// },
|
||||
// {
|
||||
// variable: '{{threshold}}',
|
||||
// description: 'Threshold value from alert condition',
|
||||
// },
|
||||
// { variable: '{{unit}}', description: 'Unit of measurement for the metric' },
|
||||
// {
|
||||
// variable: '{{severity}}',
|
||||
// description: 'Alert severity level (Critical, Warning, Info)',
|
||||
// },
|
||||
// {
|
||||
// variable: '{{queryname}}',
|
||||
// description: 'Name of the query that triggered the alert',
|
||||
// },
|
||||
// {
|
||||
// variable: '{{labels}}',
|
||||
// description: 'All labels associated with the alert',
|
||||
// },
|
||||
// {
|
||||
// variable: '{{timestamp}}',
|
||||
// description: 'Timestamp when alert was triggered',
|
||||
// },
|
||||
// ];
|
||||
const templateVariables = [
|
||||
{ variable: '{{alertname}}', description: 'Name of the alert rule' },
|
||||
{
|
||||
variable: '{{value}}',
|
||||
description: 'Current value that triggered the alert',
|
||||
},
|
||||
{
|
||||
variable: '{{threshold}}',
|
||||
description: 'Threshold value from alert condition',
|
||||
},
|
||||
{ variable: '{{unit}}', description: 'Unit of measurement for the metric' },
|
||||
{
|
||||
variable: '{{severity}}',
|
||||
description: 'Alert severity level (Critical, Warning, Info)',
|
||||
},
|
||||
{
|
||||
variable: '{{queryname}}',
|
||||
description: 'Name of the query that triggered the alert',
|
||||
},
|
||||
{
|
||||
variable: '{{labels}}',
|
||||
description: 'All labels associated with the alert',
|
||||
},
|
||||
{
|
||||
variable: '{{timestamp}}',
|
||||
description: 'Timestamp when alert was triggered',
|
||||
},
|
||||
];
|
||||
|
||||
// const templateVariableContent = (
|
||||
// <div className="template-variable-content">
|
||||
// <Typography.Text strong>Available Template Variables:</Typography.Text>
|
||||
// {templateVariables.map((item) => (
|
||||
// <div className="template-variable-content-item" key={item.variable}>
|
||||
// <code>{item.variable}</code>
|
||||
// <Typography.Text>{item.description}</Typography.Text>
|
||||
// </div>
|
||||
// ))}
|
||||
// </div>
|
||||
// );
|
||||
const templateVariableContent = (
|
||||
<div className="template-variable-content">
|
||||
<Typography.Text strong>Available Template Variables:</Typography.Text>
|
||||
{templateVariables.map((item) => (
|
||||
<div className="template-variable-content-item" key={item.variable}>
|
||||
<code>{item.variable}</code>
|
||||
<Typography.Text>{item.description}</Typography.Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="notification-message-container">
|
||||
@@ -67,13 +67,12 @@ function NotificationMessage(): JSX.Element {
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="notification-message-header-actions">
|
||||
{/* TODO: Add back when the functionality is implemented */}
|
||||
{/* <Popover content={templateVariableContent}>
|
||||
<Popover content={templateVariableContent}>
|
||||
<Button type="text">
|
||||
<Info size={12} />
|
||||
Variables
|
||||
</Button>
|
||||
</Popover> */}
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<TextArea
|
||||
|
||||
@@ -4,15 +4,18 @@ import { Input, Select, Typography } from 'antd';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import {
|
||||
ADVANCED_OPTIONS_TIME_UNIT_OPTIONS as RE_NOTIFICATION_UNIT_OPTIONS,
|
||||
RE_NOTIFICATION_CONDITION_OPTIONS,
|
||||
RE_NOTIFICATION_TIME_UNIT_OPTIONS,
|
||||
} from '../context/constants';
|
||||
import AdvancedOptionItem from '../EvaluationSettings/AdvancedOptionItem';
|
||||
import Stepper from '../Stepper';
|
||||
import { showCondensedLayout } from '../utils';
|
||||
import MultipleNotifications from './MultipleNotifications';
|
||||
import NotificationMessage from './NotificationMessage';
|
||||
|
||||
function NotificationSettings(): JSX.Element {
|
||||
const showCondensedLayoutFlag = showCondensedLayout();
|
||||
|
||||
const {
|
||||
notificationSettings,
|
||||
setNotificationSettings,
|
||||
@@ -42,7 +45,7 @@ function NotificationSettings(): JSX.Element {
|
||||
value={notificationSettings.reNotification.unit || null}
|
||||
placeholder="Select unit"
|
||||
disabled={!notificationSettings.reNotification.enabled}
|
||||
options={RE_NOTIFICATION_TIME_UNIT_OPTIONS}
|
||||
options={RE_NOTIFICATION_UNIT_OPTIONS}
|
||||
onChange={(value): void => {
|
||||
setNotificationSettings({
|
||||
type: 'SET_RE_NOTIFICATION',
|
||||
@@ -79,7 +82,10 @@ function NotificationSettings(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="notification-settings-container">
|
||||
<Stepper stepNumber={3} label="Notification settings" />
|
||||
<Stepper
|
||||
stepNumber={showCondensedLayoutFlag ? 3 : 4}
|
||||
label="Notification settings"
|
||||
/>
|
||||
<NotificationMessage />
|
||||
<div className="notification-settings-content">
|
||||
<MultipleNotifications />
|
||||
@@ -97,7 +103,6 @@ function NotificationSettings(): JSX.Element {
|
||||
},
|
||||
});
|
||||
}}
|
||||
defaultShowInput={notificationSettings.reNotification.enabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import * as createAlertContext from 'container/CreateAlertV2/context';
|
||||
import { createMockAlertContextState } from 'container/CreateAlertV2/EvaluationSettings/__tests__/testUtils';
|
||||
import * as utils from 'container/CreateAlertV2/utils';
|
||||
|
||||
import NotificationSettings from '../NotificationSettings';
|
||||
|
||||
@@ -23,10 +24,6 @@ jest.mock(
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('container/CreateAlertV2/utils', () => ({
|
||||
...jest.requireActual('container/CreateAlertV2/utils'),
|
||||
}));
|
||||
|
||||
const initialNotificationSettings = createMockAlertContextState()
|
||||
.notificationSettings;
|
||||
const mockSetNotificationSettings = jest.fn();
|
||||
@@ -40,10 +37,10 @@ const REPEAT_NOTIFICATIONS_TEXT = 'Repeat notifications';
|
||||
const ENTER_TIME_INTERVAL_TEXT = 'Enter time interval...';
|
||||
|
||||
describe('NotificationSettings', () => {
|
||||
it('renders the notification settings tab with step number 3 and default values', () => {
|
||||
it('renders the notification settings tab with step number 4 and default values', () => {
|
||||
render(<NotificationSettings />);
|
||||
expect(screen.getByText('Notification settings')).toBeInTheDocument();
|
||||
expect(screen.getByText('3')).toBeInTheDocument();
|
||||
expect(screen.getByText('4')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('multiple-notifications')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('notification-message')).toBeInTheDocument();
|
||||
expect(screen.getByText(REPEAT_NOTIFICATIONS_TEXT)).toBeInTheDocument();
|
||||
@@ -54,6 +51,15 @@ describe('NotificationSettings', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the notification settings tab with step number 3 in condensed layout', () => {
|
||||
jest.spyOn(utils, 'showCondensedLayout').mockReturnValueOnce(true);
|
||||
render(<NotificationSettings />);
|
||||
expect(screen.getByText('Notification settings')).toBeInTheDocument();
|
||||
expect(screen.getByText('3')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('multiple-notifications')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('notification-message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Repeat notifications', () => {
|
||||
it('renders the repeat notifications with inputs hidden when the repeat notifications switch is off', () => {
|
||||
render(<NotificationSettings />);
|
||||
|
||||
@@ -84,28 +84,12 @@
|
||||
.ant-select {
|
||||
.ant-select-selector {
|
||||
width: 120px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-multiple {
|
||||
.ant-select-selector {
|
||||
width: 200px;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,15 +202,6 @@
|
||||
flex-shrink: 0;
|
||||
.ant-select-selector {
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,15 +327,6 @@
|
||||
.ant-select {
|
||||
.ant-select-selector {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-ink-300);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-ink-300);
|
||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useCreateAlertState } from 'container/CreateAlertV2/context';
|
||||
import ChartPreviewComponent from 'container/FormAlertRules/ChartPreview';
|
||||
@@ -17,7 +16,7 @@ export interface ChartPreviewProps {
|
||||
|
||||
function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
|
||||
const { currentQuery, panelType, stagedQuery } = useQueryBuilder();
|
||||
const { thresholdState, alertState, setAlertState } = useCreateAlertState();
|
||||
const { thresholdState, alertState } = useCreateAlertState();
|
||||
const { selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
@@ -26,24 +25,14 @@ function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
|
||||
|
||||
const yAxisUnit = alertState.yAxisUnit || '';
|
||||
|
||||
const headline = (
|
||||
<div className="chart-preview-headline">
|
||||
<PlotTag
|
||||
queryType={currentQuery.queryType}
|
||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||
/>
|
||||
<YAxisUnitSelector
|
||||
value={alertState.yAxisUnit}
|
||||
onChange={(value): void => {
|
||||
setAlertState({ type: 'SET_Y_AXIS_UNIT', payload: value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderQBChartPreview = (): JSX.Element => (
|
||||
<ChartPreviewComponent
|
||||
headline={headline}
|
||||
headline={
|
||||
<PlotTag
|
||||
queryType={currentQuery.queryType}
|
||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||
/>
|
||||
}
|
||||
name=""
|
||||
query={stagedQuery}
|
||||
selectedInterval={globalSelectedInterval}
|
||||
@@ -51,13 +40,19 @@ function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
|
||||
yAxisUnit={yAxisUnit || ''}
|
||||
graphType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||
setQueryStatus={setQueryStatus}
|
||||
showSideLegend
|
||||
additionalThresholds={thresholdState.thresholds}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderPromAndChQueryChartPreview = (): JSX.Element => (
|
||||
<ChartPreviewComponent
|
||||
headline={headline}
|
||||
headline={
|
||||
<PlotTag
|
||||
queryType={currentQuery.queryType}
|
||||
panelType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||
/>
|
||||
}
|
||||
name="Chart Preview"
|
||||
query={stagedQuery}
|
||||
alertDef={alertDef}
|
||||
@@ -65,6 +60,7 @@ function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
|
||||
yAxisUnit={yAxisUnit || ''}
|
||||
graphType={panelType || PANEL_TYPES.TIME_SERIES}
|
||||
setQueryStatus={setQueryStatus}
|
||||
showSideLegend
|
||||
additionalThresholds={thresholdState.thresholds}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,13 +2,12 @@ import './styles.scss';
|
||||
|
||||
import { Button } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import QuerySectionComponent from 'container/FormAlertRules/QuerySection';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { BarChart2, DraftingCompass, FileText, ScrollText } from 'lucide-react';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import Stepper from '../Stepper';
|
||||
@@ -16,20 +15,17 @@ import ChartPreview from './ChartPreview';
|
||||
import { buildAlertDefForChartPreview } from './utils';
|
||||
|
||||
function QuerySection(): JSX.Element {
|
||||
const { currentQuery, handleRunQuery } = useQueryBuilder();
|
||||
const {
|
||||
currentQuery,
|
||||
handleRunQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
} = useQueryBuilder();
|
||||
const { alertType, setAlertType, thresholdState } = useCreateAlertState();
|
||||
alertState,
|
||||
setAlertState,
|
||||
alertType,
|
||||
setAlertType,
|
||||
thresholdState,
|
||||
} = useCreateAlertState();
|
||||
|
||||
const alertDef = buildAlertDefForChartPreview({ alertType, thresholdState });
|
||||
|
||||
const onQueryCategoryChange = (val: EQueryType): void => {
|
||||
const query: Query = { ...currentQuery, queryType: val };
|
||||
redirectWithQueryBuilderData(query);
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: 'Metrics',
|
||||
@@ -55,8 +51,17 @@ function QuerySection(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="query-section">
|
||||
<Stepper stepNumber={1} label="Define the query" />
|
||||
<Stepper
|
||||
stepNumber={1}
|
||||
label="Define the query you want to set an alert on"
|
||||
/>
|
||||
<ChartPreview alertDef={alertDef} />
|
||||
<YAxisUnitSelector
|
||||
value={alertState.yAxisUnit}
|
||||
onChange={(value): void => {
|
||||
setAlertState({ type: 'SET_Y_AXIS_UNIT', payload: value });
|
||||
}}
|
||||
/>
|
||||
<div className="query-section-tabs">
|
||||
<div className="query-section-query-actions">
|
||||
{tabs.map((tab) => (
|
||||
@@ -77,7 +82,7 @@ function QuerySection(): JSX.Element {
|
||||
</div>
|
||||
<QuerySectionComponent
|
||||
queryCategory={currentQuery.queryType}
|
||||
setQueryCategory={onQueryCategoryChange}
|
||||
setQueryCategory={(): void => {}}
|
||||
alertType={alertType}
|
||||
runQuery={handleRunQuery}
|
||||
alertDef={alertDef}
|
||||
|
||||
@@ -134,7 +134,7 @@ const renderChartPreview = (): ReturnType<typeof render> =>
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||
<CreateAlertProvider>
|
||||
<ChartPreview alertDef={mockAlertDef} />
|
||||
</CreateAlertProvider>
|
||||
</MemoryRouter>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { AlertDetectionTypes } from 'container/FormAlertRules';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
@@ -105,7 +104,7 @@ const renderQuerySection = (): ReturnType<typeof render> =>
|
||||
<Provider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<CreateAlertProvider initialAlertType={AlertTypes.METRICS_BASED_ALERT}>
|
||||
<CreateAlertProvider>
|
||||
<QuerySection />
|
||||
</CreateAlertProvider>
|
||||
</MemoryRouter>
|
||||
@@ -136,7 +135,7 @@ describe('QuerySection', () => {
|
||||
expect(screen.getByTestId('stepper')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('step-number')).toHaveTextContent('1');
|
||||
expect(screen.getByTestId('step-label')).toHaveTextContent(
|
||||
'Define the query',
|
||||
'Define the query you want to set an alert on',
|
||||
);
|
||||
|
||||
// Check if ChartPreview is rendered
|
||||
@@ -187,7 +186,6 @@ describe('QuerySection', () => {
|
||||
expect.any(Object),
|
||||
{
|
||||
[QueryParams.alertType]: AlertTypes.LOGS_BASED_ALERT,
|
||||
[QueryParams.ruleType]: AlertDetectionTypes.THRESHOLD_ALERT,
|
||||
},
|
||||
undefined,
|
||||
true,
|
||||
@@ -202,7 +200,6 @@ describe('QuerySection', () => {
|
||||
expect.any(Object),
|
||||
{
|
||||
[QueryParams.alertType]: AlertTypes.TRACES_BASED_ALERT,
|
||||
[QueryParams.ruleType]: AlertDetectionTypes.THRESHOLD_ALERT,
|
||||
},
|
||||
undefined,
|
||||
true,
|
||||
|
||||
@@ -77,14 +77,6 @@
|
||||
.ant-select-selector {
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,18 +88,6 @@
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
.ant-card-body {
|
||||
background-color: var(--bg-ink-500);
|
||||
|
||||
.chart-preview-headline {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
|
||||
.y-axis-unit-selector-component {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,78 +99,3 @@
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.query-section {
|
||||
.query-section-tabs {
|
||||
.query-section-query-actions {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-300);
|
||||
|
||||
.explorer-view-option {
|
||||
border-left: 0.5px solid var(--bg-vanilla-300);
|
||||
border-bottom: 0.5px solid var(--bg-vanilla-300);
|
||||
|
||||
&.active-tab {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-vanilla-100) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--bg-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.y-axis-unit-selector-component {
|
||||
.ant-select {
|
||||
.ant-select-selector {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-300);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--bg-ink-300);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--bg-ink-300);
|
||||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-preview-container {
|
||||
.alert-chart-container {
|
||||
.ant-card {
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
.ant-card-body {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
.chart-preview-header {
|
||||
.plot-tag {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
color: var(--bg-slate-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert-query-section-container {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
border: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,22 +42,3 @@
|
||||
background-position: center;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.step-number {
|
||||
background-color: var(--bg-robin-400);
|
||||
color: var(--text-slate-400);
|
||||
}
|
||||
|
||||
.step-label {
|
||||
color: var(--bg-ink-300);
|
||||
}
|
||||
|
||||
.dotted-line {
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
var(--bg-ink-200) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,358 +0,0 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
|
||||
import { defaultPostableAlertRuleV2 } from '../constants';
|
||||
import { INITIAL_ALERT_STATE } from '../context/constants';
|
||||
import {
|
||||
AlertThresholdMatchType,
|
||||
AlertThresholdOperator,
|
||||
} from '../context/types';
|
||||
import {
|
||||
getAdvancedOptionsStateFromAlertDef,
|
||||
getColorForThreshold,
|
||||
getCreateAlertLocalStateFromAlertDef,
|
||||
getEvaluationWindowStateFromAlertDef,
|
||||
getNotificationSettingsStateFromAlertDef,
|
||||
getThresholdStateFromAlertDef,
|
||||
parseGoTime,
|
||||
} from '../utils';
|
||||
|
||||
describe('CreateAlertV2 utils', () => {
|
||||
describe('getColorForThreshold', () => {
|
||||
it('should return the correct color for the pre-defined threshold', () => {
|
||||
expect(getColorForThreshold('critical')).toBe(Color.BG_SAKURA_500);
|
||||
expect(getColorForThreshold('warning')).toBe(Color.BG_AMBER_500);
|
||||
expect(getColorForThreshold('info')).toBe(Color.BG_ROBIN_500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseGoTime', () => {
|
||||
it('should return the correct time and unit for the given input', () => {
|
||||
expect(parseGoTime('1h')).toStrictEqual({
|
||||
time: 1,
|
||||
unit: UniversalYAxisUnit.HOURS,
|
||||
});
|
||||
expect(parseGoTime('1m')).toStrictEqual({
|
||||
time: 1,
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
});
|
||||
expect(parseGoTime('1s')).toStrictEqual({
|
||||
time: 1,
|
||||
unit: UniversalYAxisUnit.SECONDS,
|
||||
});
|
||||
expect(parseGoTime('1h0m')).toStrictEqual({
|
||||
time: 1,
|
||||
unit: UniversalYAxisUnit.HOURS,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEvaluationWindowStateFromAlertDef', () => {
|
||||
it('for rolling window with non-custom timeframe', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
evaluation: {
|
||||
...defaultPostableAlertRuleV2.evaluation,
|
||||
kind: 'rolling',
|
||||
spec: {
|
||||
evalWindow: '5m0s',
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = getEvaluationWindowStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
windowType: 'rolling',
|
||||
timeframe: '5m0s',
|
||||
});
|
||||
});
|
||||
|
||||
it('for rolling window with custom timeframe', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
evaluation: {
|
||||
...defaultPostableAlertRuleV2.evaluation,
|
||||
kind: 'rolling',
|
||||
spec: {
|
||||
evalWindow: '13m0s',
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = getEvaluationWindowStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
windowType: 'rolling',
|
||||
timeframe: 'custom',
|
||||
startingAt: {
|
||||
number: '13',
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('for cumulative window with current hour', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
evaluation: {
|
||||
kind: 'cumulative',
|
||||
spec: {
|
||||
schedule: {
|
||||
type: 'hourly',
|
||||
minute: 14,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = getEvaluationWindowStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'currentHour',
|
||||
startingAt: {
|
||||
number: '14',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('for cumulative window with current day', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
evaluation: {
|
||||
...defaultPostableAlertRuleV2.evaluation,
|
||||
kind: 'cumulative',
|
||||
spec: {
|
||||
schedule: {
|
||||
type: 'daily',
|
||||
hour: 14,
|
||||
minute: 15,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = getEvaluationWindowStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'currentDay',
|
||||
startingAt: {
|
||||
time: '14:15:00',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('for cumulative window with current month', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
evaluation: {
|
||||
...defaultPostableAlertRuleV2.evaluation,
|
||||
kind: 'cumulative',
|
||||
spec: {
|
||||
schedule: {
|
||||
type: 'monthly',
|
||||
day: 12,
|
||||
hour: 16,
|
||||
minute: 34,
|
||||
},
|
||||
timezone: 'UTC',
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = getEvaluationWindowStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
windowType: 'cumulative',
|
||||
timeframe: 'currentMonth',
|
||||
startingAt: {
|
||||
number: '12',
|
||||
timezone: 'UTC',
|
||||
time: '16:34:00',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNotificationSettingsStateFromAlertDef', () => {
|
||||
it('should return the correct notification settings state for the given alert def', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
notificationSettings: {
|
||||
groupBy: ['email'],
|
||||
renotify: {
|
||||
enabled: true,
|
||||
interval: '1m0s',
|
||||
alertStates: ['firing'],
|
||||
},
|
||||
usePolicy: true,
|
||||
},
|
||||
};
|
||||
const props = getNotificationSettingsStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
multipleNotifications: ['email'],
|
||||
reNotification: {
|
||||
enabled: true,
|
||||
value: 1,
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
conditions: ['firing'],
|
||||
},
|
||||
description:
|
||||
'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})',
|
||||
routingPolicies: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('when renotification is not provided', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
notificationSettings: {
|
||||
groupBy: ['email'],
|
||||
usePolicy: false,
|
||||
},
|
||||
};
|
||||
const props = getNotificationSettingsStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
multipleNotifications: ['email'],
|
||||
reNotification: {
|
||||
enabled: false,
|
||||
value: 30,
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
conditions: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAdvancedOptionsStateFromAlertDef', () => {
|
||||
it('should return the correct advanced options state for the given alert def', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
condition: {
|
||||
...defaultPostableAlertRuleV2.condition,
|
||||
compositeQuery: {
|
||||
...defaultPostableAlertRuleV2.condition.compositeQuery,
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
requiredNumPoints: 13,
|
||||
requireMinPoints: true,
|
||||
alertOnAbsent: true,
|
||||
absentFor: 12,
|
||||
},
|
||||
evaluation: {
|
||||
...defaultPostableAlertRuleV2.evaluation,
|
||||
spec: {
|
||||
frequency: '1m0s',
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = getAdvancedOptionsStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
sendNotificationIfDataIsMissing: {
|
||||
enabled: true,
|
||||
toleranceLimit: 12,
|
||||
timeUnit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
enforceMinimumDatapoints: {
|
||||
enabled: true,
|
||||
minimumDatapoints: 13,
|
||||
},
|
||||
evaluationCadence: {
|
||||
mode: 'default',
|
||||
default: {
|
||||
value: 1,
|
||||
timeUnit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getThresholdStateFromAlertDef', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
annotations: {
|
||||
summary: 'test summary',
|
||||
description: 'test description',
|
||||
},
|
||||
condition: {
|
||||
...defaultPostableAlertRuleV2.condition,
|
||||
thresholds: {
|
||||
kind: 'basic',
|
||||
spec: [
|
||||
{
|
||||
name: 'critical',
|
||||
target: 1,
|
||||
targetUnit: UniversalYAxisUnit.MINUTES,
|
||||
channels: ['email'],
|
||||
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
|
||||
op: AlertThresholdOperator.IS_ABOVE,
|
||||
},
|
||||
],
|
||||
},
|
||||
selectedQueryName: 'test',
|
||||
},
|
||||
};
|
||||
const props = getThresholdStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
selectedQuery: 'test',
|
||||
operator: AlertThresholdOperator.IS_ABOVE,
|
||||
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
|
||||
thresholds: [
|
||||
{
|
||||
id: expect.any(String),
|
||||
label: 'critical',
|
||||
thresholdValue: 1,
|
||||
recoveryThresholdValue: null,
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
color: Color.BG_SAKURA_500,
|
||||
channels: ['email'],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCreateAlertLocalStateFromAlertDef', () => {
|
||||
it('should return the correct create alert local state for the given alert def', () => {
|
||||
const args: PostableAlertRuleV2 = {
|
||||
...defaultPostableAlertRuleV2,
|
||||
annotations: {
|
||||
summary: 'test summary',
|
||||
description: 'test description',
|
||||
},
|
||||
alert: 'test-alert',
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
team: 'test-team',
|
||||
},
|
||||
condition: {
|
||||
...defaultPostableAlertRuleV2.condition,
|
||||
compositeQuery: {
|
||||
...defaultPostableAlertRuleV2.condition.compositeQuery,
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = getCreateAlertLocalStateFromAlertDef(args);
|
||||
expect(props).toBeDefined();
|
||||
expect(props).toMatchObject({
|
||||
basicAlertState: {
|
||||
...INITIAL_ALERT_STATE,
|
||||
name: 'test-alert',
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
team: 'test-team',
|
||||
},
|
||||
yAxisUnit: UniversalYAxisUnit.MINUTES,
|
||||
},
|
||||
// as we have already verified these utils in their respective tests
|
||||
thresholdState: expect.any(Object),
|
||||
advancedOptionsState: expect.any(Object),
|
||||
evaluationWindowState: expect.any(Object),
|
||||
notificationSettingsState: expect.any(Object),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,74 +0,0 @@
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryPromQLData,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import {
|
||||
NEW_ALERT_SCHEMA_VERSION,
|
||||
PostableAlertRuleV2,
|
||||
} from 'types/api/alerts/alertTypesV2';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
const defaultAnnotations = {
|
||||
description:
|
||||
'This alert is fired when the defined metric (current value: {{$value}}) crosses the threshold ({{$threshold}})',
|
||||
summary:
|
||||
'The rule threshold is set to {{$threshold}}, and the observed metric value is {{$value}}',
|
||||
};
|
||||
|
||||
const defaultNotificationSettings: PostableAlertRuleV2['notificationSettings'] = {
|
||||
groupBy: [],
|
||||
renotify: {
|
||||
enabled: false,
|
||||
interval: '30m',
|
||||
alertStates: [],
|
||||
},
|
||||
usePolicy: false,
|
||||
};
|
||||
|
||||
const defaultEvaluation: PostableAlertRuleV2['evaluation'] = {
|
||||
kind: 'rolling',
|
||||
spec: {
|
||||
evalWindow: '5m0s',
|
||||
frequency: '1m',
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultPostableAlertRuleV2: PostableAlertRuleV2 = {
|
||||
alertType: AlertTypes.METRICS_BASED_ALERT,
|
||||
version: ENTITY_VERSION_V5,
|
||||
schemaVersion: NEW_ALERT_SCHEMA_VERSION,
|
||||
condition: {
|
||||
compositeQuery: {
|
||||
builderQueries: {
|
||||
A: initialQueryBuilderFormValuesMap.metrics,
|
||||
},
|
||||
promQueries: { A: initialQueryPromQLData },
|
||||
chQueries: {
|
||||
A: {
|
||||
name: 'A',
|
||||
query: ``,
|
||||
legend: '',
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
unit: undefined,
|
||||
},
|
||||
selectedQueryName: 'A',
|
||||
alertOnAbsent: true,
|
||||
absentFor: 10,
|
||||
requireMinPoints: false,
|
||||
requiredNumPoints: 0,
|
||||
},
|
||||
labels: {
|
||||
severity: 'warning',
|
||||
},
|
||||
annotations: defaultAnnotations,
|
||||
notificationSettings: defaultNotificationSettings,
|
||||
alert: 'TEST_ALERT',
|
||||
evaluation: defaultEvaluation,
|
||||
};
|
||||
@@ -21,15 +21,16 @@ import {
|
||||
|
||||
export const INITIAL_ALERT_STATE: AlertState = {
|
||||
name: '',
|
||||
description: '',
|
||||
labels: {},
|
||||
yAxisUnit: undefined,
|
||||
};
|
||||
|
||||
export const INITIAL_CRITICAL_THRESHOLD: Threshold = {
|
||||
id: v4(),
|
||||
label: 'critical',
|
||||
label: 'CRITICAL',
|
||||
thresholdValue: 0,
|
||||
recoveryThresholdValue: null,
|
||||
recoveryThresholdValue: 0,
|
||||
unit: '',
|
||||
channels: [],
|
||||
color: Color.BG_SAKURA_500,
|
||||
@@ -37,9 +38,9 @@ export const INITIAL_CRITICAL_THRESHOLD: Threshold = {
|
||||
|
||||
export const INITIAL_WARNING_THRESHOLD: Threshold = {
|
||||
id: v4(),
|
||||
label: 'warning',
|
||||
label: 'WARNING',
|
||||
thresholdValue: 0,
|
||||
recoveryThresholdValue: null,
|
||||
recoveryThresholdValue: 0,
|
||||
unit: '',
|
||||
channels: [],
|
||||
color: Color.BG_AMBER_500,
|
||||
@@ -47,9 +48,9 @@ export const INITIAL_WARNING_THRESHOLD: Threshold = {
|
||||
|
||||
export const INITIAL_INFO_THRESHOLD: Threshold = {
|
||||
id: v4(),
|
||||
label: 'info',
|
||||
label: 'INFO',
|
||||
thresholdValue: 0,
|
||||
recoveryThresholdValue: null,
|
||||
recoveryThresholdValue: 0,
|
||||
unit: '',
|
||||
channels: [],
|
||||
color: Color.BG_ROBIN_500,
|
||||
@@ -59,7 +60,7 @@ export const INITIAL_RANDOM_THRESHOLD: Threshold = {
|
||||
id: v4(),
|
||||
label: '',
|
||||
thresholdValue: 0,
|
||||
recoveryThresholdValue: null,
|
||||
recoveryThresholdValue: 0,
|
||||
unit: '',
|
||||
channels: [],
|
||||
color: getRandomColor(),
|
||||
@@ -79,11 +80,9 @@ export const INITIAL_ADVANCED_OPTIONS_STATE: AdvancedOptionsState = {
|
||||
sendNotificationIfDataIsMissing: {
|
||||
toleranceLimit: 15,
|
||||
timeUnit: UniversalYAxisUnit.MINUTES,
|
||||
enabled: false,
|
||||
},
|
||||
enforceMinimumDatapoints: {
|
||||
minimumDatapoints: 0,
|
||||
enabled: false,
|
||||
},
|
||||
delayEvaluation: {
|
||||
delay: 5,
|
||||
@@ -121,10 +120,10 @@ export const INITIAL_EVALUATION_WINDOW_STATE: EvaluationWindowState = {
|
||||
};
|
||||
|
||||
export const THRESHOLD_OPERATOR_OPTIONS = [
|
||||
{ value: AlertThresholdOperator.IS_ABOVE, label: 'ABOVE' },
|
||||
{ value: AlertThresholdOperator.IS_BELOW, label: 'BELOW' },
|
||||
{ value: AlertThresholdOperator.IS_EQUAL_TO, label: 'EQUAL TO' },
|
||||
{ value: AlertThresholdOperator.IS_NOT_EQUAL_TO, label: 'NOT EQUAL TO' },
|
||||
{ value: AlertThresholdOperator.IS_ABOVE, label: 'IS ABOVE' },
|
||||
{ value: AlertThresholdOperator.IS_BELOW, label: 'IS BELOW' },
|
||||
{ value: AlertThresholdOperator.IS_EQUAL_TO, label: 'IS EQUAL TO' },
|
||||
{ value: AlertThresholdOperator.IS_NOT_EQUAL_TO, label: 'IS NOT EQUAL TO' },
|
||||
];
|
||||
|
||||
export const ANOMALY_THRESHOLD_OPERATOR_OPTIONS = [
|
||||
@@ -170,11 +169,7 @@ export const ADVANCED_OPTIONS_TIME_UNIT_OPTIONS = [
|
||||
{ value: UniversalYAxisUnit.SECONDS, label: 'Seconds' },
|
||||
{ value: UniversalYAxisUnit.MINUTES, label: 'Minutes' },
|
||||
{ value: UniversalYAxisUnit.HOURS, label: 'Hours' },
|
||||
];
|
||||
|
||||
export const RE_NOTIFICATION_TIME_UNIT_OPTIONS = [
|
||||
{ value: UniversalYAxisUnit.MINUTES, label: 'Minutes' },
|
||||
{ value: UniversalYAxisUnit.HOURS, label: 'Hours' },
|
||||
{ value: UniversalYAxisUnit.DAYS, label: 'Days' },
|
||||
];
|
||||
|
||||
export const NOTIFICATION_MESSAGE_PLACEHOLDER =
|
||||
@@ -182,17 +177,16 @@ export const NOTIFICATION_MESSAGE_PLACEHOLDER =
|
||||
|
||||
export const RE_NOTIFICATION_CONDITION_OPTIONS = [
|
||||
{ value: 'firing', label: 'Firing' },
|
||||
{ value: 'nodata', label: 'No Data' },
|
||||
{ value: 'no-data', label: 'No Data' },
|
||||
];
|
||||
|
||||
export const INITIAL_NOTIFICATION_SETTINGS_STATE: NotificationSettingsState = {
|
||||
multipleNotifications: [],
|
||||
reNotification: {
|
||||
enabled: false,
|
||||
value: 30,
|
||||
value: 1,
|
||||
unit: UniversalYAxisUnit.MINUTES,
|
||||
conditions: [],
|
||||
},
|
||||
description: NOTIFICATION_MESSAGE_PLACEHOLDER,
|
||||
routingPolicies: false,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
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 {
|
||||
@@ -51,7 +47,7 @@ export const useCreateAlertState = (): ICreateAlertContextProps => {
|
||||
export function CreateAlertProvider(
|
||||
props: ICreateAlertProviderProps,
|
||||
): JSX.Element {
|
||||
const { children, initialAlertState, isEditMode, ruleId } = props;
|
||||
const { children } = props;
|
||||
|
||||
const [alertState, setAlertState] = useReducer(
|
||||
alertCreationReducer,
|
||||
@@ -76,10 +72,6 @@ export function CreateAlertProvider(
|
||||
currentQueryToRedirect,
|
||||
{
|
||||
[QueryParams.alertType]: value,
|
||||
[QueryParams.ruleType]:
|
||||
value === AlertTypes.ANOMALY_BASED_ALERT
|
||||
? AlertDetectionTypes.ANOMALY_DETECTION_ALERT
|
||||
: AlertDetectionTypes.THRESHOLD_ALERT,
|
||||
},
|
||||
undefined,
|
||||
true,
|
||||
@@ -115,65 +107,6 @@ export function CreateAlertProvider(
|
||||
});
|
||||
}, [alertType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditMode && initialAlertState) {
|
||||
setAlertState({
|
||||
type: 'SET_INITIAL_STATE',
|
||||
payload: initialAlertState.basicAlertState,
|
||||
});
|
||||
setThresholdState({
|
||||
type: 'SET_INITIAL_STATE',
|
||||
payload: initialAlertState.thresholdState,
|
||||
});
|
||||
setEvaluationWindow({
|
||||
type: 'SET_INITIAL_STATE',
|
||||
payload: initialAlertState.evaluationWindowState,
|
||||
});
|
||||
setAdvancedOptions({
|
||||
type: 'SET_INITIAL_STATE',
|
||||
payload: initialAlertState.advancedOptionsState,
|
||||
});
|
||||
setNotificationSettings({
|
||||
type: 'SET_INITIAL_STATE',
|
||||
payload: initialAlertState.notificationSettingsState,
|
||||
});
|
||||
}
|
||||
}, [initialAlertState, isEditMode]);
|
||||
|
||||
const discardAlertRule = useCallback(() => {
|
||||
setAlertState({
|
||||
type: 'RESET',
|
||||
});
|
||||
setThresholdState({
|
||||
type: 'RESET',
|
||||
});
|
||||
setEvaluationWindow({
|
||||
type: 'RESET',
|
||||
});
|
||||
setAdvancedOptions({
|
||||
type: 'RESET',
|
||||
});
|
||||
setNotificationSettings({
|
||||
type: 'RESET',
|
||||
});
|
||||
handleAlertTypeChange(AlertTypes.METRICS_BASED_ALERT);
|
||||
}, [handleAlertTypeChange]);
|
||||
|
||||
const {
|
||||
mutate: createAlertRule,
|
||||
isLoading: isCreatingAlertRule,
|
||||
} = useCreateAlertRule();
|
||||
|
||||
const {
|
||||
mutate: testAlertRule,
|
||||
isLoading: isTestingAlertRule,
|
||||
} = useTestAlertRule();
|
||||
|
||||
const {
|
||||
mutate: updateAlertRule,
|
||||
isLoading: isUpdatingAlertRule,
|
||||
} = useUpdateAlertRule(ruleId || '');
|
||||
|
||||
const contextValue: ICreateAlertContextProps = useMemo(
|
||||
() => ({
|
||||
alertState,
|
||||
@@ -188,14 +121,6 @@ export function CreateAlertProvider(
|
||||
setAdvancedOptions,
|
||||
notificationSettings,
|
||||
setNotificationSettings,
|
||||
discardAlertRule,
|
||||
createAlertRule,
|
||||
isCreatingAlertRule,
|
||||
testAlertRule,
|
||||
isTestingAlertRule,
|
||||
updateAlertRule,
|
||||
isUpdatingAlertRule,
|
||||
isEditMode: isEditMode || false,
|
||||
}),
|
||||
[
|
||||
alertState,
|
||||
@@ -205,14 +130,6 @@ export function CreateAlertProvider(
|
||||
evaluationWindow,
|
||||
advancedOptions,
|
||||
notificationSettings,
|
||||
discardAlertRule,
|
||||
createAlertRule,
|
||||
isCreatingAlertRule,
|
||||
testAlertRule,
|
||||
isTestingAlertRule,
|
||||
updateAlertRule,
|
||||
isUpdatingAlertRule,
|
||||
isEditMode,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import { CreateAlertRuleResponse } from 'api/alerts/createAlertRule';
|
||||
import { TestAlertRuleResponse } from 'api/alerts/testAlertRule';
|
||||
import { UpdateAlertRuleResponse } from 'api/alerts/updateAlertRule';
|
||||
import { Dayjs } from 'dayjs';
|
||||
import { Dispatch } from 'react';
|
||||
import { UseMutateFunction } from 'react-query';
|
||||
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';
|
||||
|
||||
import { GetCreateAlertLocalStateFromAlertDefReturn } from '../types';
|
||||
|
||||
export interface ICreateAlertContextProps {
|
||||
alertState: AlertState;
|
||||
setAlertState: Dispatch<CreateAlertAction>;
|
||||
@@ -24,37 +16,10 @@ export interface ICreateAlertContextProps {
|
||||
setEvaluationWindow: Dispatch<EvaluationWindowAction>;
|
||||
notificationSettings: NotificationSettingsState;
|
||||
setNotificationSettings: Dispatch<NotificationSettingsAction>;
|
||||
isCreatingAlertRule: boolean;
|
||||
createAlertRule: UseMutateFunction<
|
||||
SuccessResponse<CreateAlertRuleResponse, unknown> | ErrorResponse,
|
||||
Error,
|
||||
PostableAlertRuleV2,
|
||||
unknown
|
||||
>;
|
||||
isTestingAlertRule: boolean;
|
||||
testAlertRule: UseMutateFunction<
|
||||
SuccessResponse<TestAlertRuleResponse, unknown> | ErrorResponse,
|
||||
Error,
|
||||
PostableAlertRuleV2,
|
||||
unknown
|
||||
>;
|
||||
discardAlertRule: () => void;
|
||||
isUpdatingAlertRule: boolean;
|
||||
updateAlertRule: UseMutateFunction<
|
||||
SuccessResponse<UpdateAlertRuleResponse, unknown> | ErrorResponse,
|
||||
Error,
|
||||
PostableAlertRuleV2,
|
||||
unknown
|
||||
>;
|
||||
isEditMode: boolean;
|
||||
}
|
||||
|
||||
export interface ICreateAlertProviderProps {
|
||||
children: React.ReactNode;
|
||||
initialAlertType: AlertTypes;
|
||||
initialAlertState?: GetCreateAlertLocalStateFromAlertDefReturn;
|
||||
isEditMode?: boolean;
|
||||
ruleId?: string;
|
||||
}
|
||||
|
||||
export enum AlertCreationStep {
|
||||
@@ -66,22 +31,23 @@ export enum AlertCreationStep {
|
||||
|
||||
export interface AlertState {
|
||||
name: string;
|
||||
description: string;
|
||||
labels: Labels;
|
||||
yAxisUnit: string | undefined;
|
||||
}
|
||||
|
||||
export type CreateAlertAction =
|
||||
| { type: 'SET_ALERT_NAME'; payload: string }
|
||||
| { type: 'SET_ALERT_DESCRIPTION'; payload: string }
|
||||
| { type: 'SET_ALERT_LABELS'; payload: Labels }
|
||||
| { type: 'SET_Y_AXIS_UNIT'; payload: string | undefined }
|
||||
| { type: 'SET_INITIAL_STATE'; payload: AlertState }
|
||||
| { type: 'RESET' };
|
||||
|
||||
export interface Threshold {
|
||||
id: string;
|
||||
label: string;
|
||||
thresholdValue: number;
|
||||
recoveryThresholdValue: number | null;
|
||||
recoveryThresholdValue: number;
|
||||
unit: string;
|
||||
channels: string[];
|
||||
color: string;
|
||||
@@ -142,18 +108,15 @@ export type AlertThresholdAction =
|
||||
| { type: 'SET_ALGORITHM'; payload: string }
|
||||
| { type: 'SET_SEASONALITY'; payload: string }
|
||||
| { type: 'SET_THRESHOLDS'; payload: Threshold[] }
|
||||
| { type: 'SET_INITIAL_STATE'; payload: AlertThresholdState }
|
||||
| { type: 'RESET' };
|
||||
|
||||
export interface AdvancedOptionsState {
|
||||
sendNotificationIfDataIsMissing: {
|
||||
toleranceLimit: number;
|
||||
timeUnit: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
enforceMinimumDatapoints: {
|
||||
minimumDatapoints: number;
|
||||
enabled: boolean;
|
||||
};
|
||||
delayEvaluation: {
|
||||
delay: number;
|
||||
@@ -184,18 +147,10 @@ export type AdvancedOptionsAction =
|
||||
type: 'SET_SEND_NOTIFICATION_IF_DATA_IS_MISSING';
|
||||
payload: { toleranceLimit: number; timeUnit: string };
|
||||
}
|
||||
| {
|
||||
type: 'TOGGLE_SEND_NOTIFICATION_IF_DATA_IS_MISSING';
|
||||
payload: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'SET_ENFORCE_MINIMUM_DATAPOINTS';
|
||||
payload: { minimumDatapoints: number };
|
||||
}
|
||||
| {
|
||||
type: 'TOGGLE_ENFORCE_MINIMUM_DATAPOINTS';
|
||||
payload: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'SET_DELAY_EVALUATION';
|
||||
payload: { delay: number; timeUnit: string };
|
||||
@@ -214,7 +169,6 @@ export type AdvancedOptionsAction =
|
||||
};
|
||||
}
|
||||
| { type: 'SET_EVALUATION_CADENCE_MODE'; payload: EvaluationCadenceMode }
|
||||
| { type: 'SET_INITIAL_STATE'; payload: AdvancedOptionsState }
|
||||
| { type: 'RESET' };
|
||||
|
||||
export interface EvaluationWindowState {
|
||||
@@ -236,7 +190,6 @@ export type EvaluationWindowAction =
|
||||
payload: { time: string; number: string; timezone: string; unit: string };
|
||||
}
|
||||
| { type: 'SET_EVALUATION_CADENCE_MODE'; payload: EvaluationCadenceMode }
|
||||
| { type: 'SET_INITIAL_STATE'; payload: EvaluationWindowState }
|
||||
| { type: 'RESET' };
|
||||
|
||||
export type EvaluationCadenceMode = 'default' | 'custom' | 'rrule';
|
||||
@@ -247,10 +200,9 @@ export interface NotificationSettingsState {
|
||||
enabled: boolean;
|
||||
value: number;
|
||||
unit: string;
|
||||
conditions: ('firing' | 'nodata')[];
|
||||
conditions: ('firing' | 'no-data')[];
|
||||
};
|
||||
description: string;
|
||||
routingPolicies: boolean;
|
||||
}
|
||||
|
||||
export type NotificationSettingsAction =
|
||||
@@ -264,10 +216,8 @@ export type NotificationSettingsAction =
|
||||
enabled: boolean;
|
||||
value: number;
|
||||
unit: string;
|
||||
conditions: ('firing' | 'nodata')[];
|
||||
conditions: ('firing' | 'no-data')[];
|
||||
};
|
||||
}
|
||||
| { type: 'SET_DESCRIPTION'; payload: string }
|
||||
| { type: 'SET_ROUTING_POLICIES'; payload: boolean }
|
||||
| { type: 'SET_INITIAL_STATE'; payload: NotificationSettingsState }
|
||||
| { type: 'RESET' };
|
||||
|
||||
@@ -41,6 +41,11 @@ export const alertCreationReducer = (
|
||||
...state,
|
||||
name: action.payload,
|
||||
};
|
||||
case 'SET_ALERT_DESCRIPTION':
|
||||
return {
|
||||
...state,
|
||||
description: action.payload,
|
||||
};
|
||||
case 'SET_ALERT_LABELS':
|
||||
return {
|
||||
...state,
|
||||
@@ -53,8 +58,6 @@ export const alertCreationReducer = (
|
||||
};
|
||||
case 'RESET':
|
||||
return INITIAL_ALERT_STATE;
|
||||
case 'SET_INITIAL_STATE':
|
||||
return action.payload;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -96,10 +99,6 @@ export function getInitialAlertTypeFromURL(
|
||||
urlSearchParams: URLSearchParams,
|
||||
currentQuery: Query,
|
||||
): AlertTypes {
|
||||
const ruleType = urlSearchParams.get(QueryParams.ruleType);
|
||||
if (ruleType === 'anomaly_rule') {
|
||||
return AlertTypes.ANOMALY_BASED_ALERT;
|
||||
}
|
||||
const alertTypeFromURL = urlSearchParams.get(QueryParams.alertType);
|
||||
return alertTypeFromURL
|
||||
? (alertTypeFromURL as AlertTypes)
|
||||
@@ -121,8 +120,6 @@ export const alertThresholdReducer = (
|
||||
return { ...state, thresholds: action.payload };
|
||||
case 'RESET':
|
||||
return INITIAL_ALERT_THRESHOLD_STATE;
|
||||
case 'SET_INITIAL_STATE':
|
||||
return action.payload;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -134,38 +131,9 @@ export const advancedOptionsReducer = (
|
||||
): AdvancedOptionsState => {
|
||||
switch (action.type) {
|
||||
case 'SET_SEND_NOTIFICATION_IF_DATA_IS_MISSING':
|
||||
return {
|
||||
...state,
|
||||
sendNotificationIfDataIsMissing: {
|
||||
...state.sendNotificationIfDataIsMissing,
|
||||
toleranceLimit: action.payload.toleranceLimit,
|
||||
timeUnit: action.payload.timeUnit,
|
||||
},
|
||||
};
|
||||
case 'TOGGLE_SEND_NOTIFICATION_IF_DATA_IS_MISSING':
|
||||
return {
|
||||
...state,
|
||||
sendNotificationIfDataIsMissing: {
|
||||
...state.sendNotificationIfDataIsMissing,
|
||||
enabled: action.payload,
|
||||
},
|
||||
};
|
||||
return { ...state, sendNotificationIfDataIsMissing: action.payload };
|
||||
case 'SET_ENFORCE_MINIMUM_DATAPOINTS':
|
||||
return {
|
||||
...state,
|
||||
enforceMinimumDatapoints: {
|
||||
...state.enforceMinimumDatapoints,
|
||||
minimumDatapoints: action.payload.minimumDatapoints,
|
||||
},
|
||||
};
|
||||
case 'TOGGLE_ENFORCE_MINIMUM_DATAPOINTS':
|
||||
return {
|
||||
...state,
|
||||
enforceMinimumDatapoints: {
|
||||
...state.enforceMinimumDatapoints,
|
||||
enabled: action.payload,
|
||||
},
|
||||
};
|
||||
return { ...state, enforceMinimumDatapoints: action.payload };
|
||||
case 'SET_DELAY_EVALUATION':
|
||||
return { ...state, delayEvaluation: action.payload };
|
||||
case 'SET_EVALUATION_CADENCE':
|
||||
@@ -178,8 +146,6 @@ export const advancedOptionsReducer = (
|
||||
...state,
|
||||
evaluationCadence: { ...state.evaluationCadence, mode: action.payload },
|
||||
};
|
||||
case 'SET_INITIAL_STATE':
|
||||
return action.payload;
|
||||
case 'RESET':
|
||||
return INITIAL_ADVANCED_OPTIONS_STATE;
|
||||
default:
|
||||
@@ -208,8 +174,6 @@ export const evaluationWindowReducer = (
|
||||
return { ...state, startingAt: action.payload };
|
||||
case 'RESET':
|
||||
return INITIAL_EVALUATION_WINDOW_STATE;
|
||||
case 'SET_INITIAL_STATE':
|
||||
return action.payload;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
@@ -226,12 +190,8 @@ export const notificationSettingsReducer = (
|
||||
return { ...state, reNotification: action.payload };
|
||||
case 'SET_DESCRIPTION':
|
||||
return { ...state, description: action.payload };
|
||||
case 'SET_ROUTING_POLICIES':
|
||||
return { ...state, routingPolicies: action.payload };
|
||||
case 'RESET':
|
||||
return INITIAL_NOTIFICATION_SETTINGS_STATE;
|
||||
case 'SET_INITIAL_STATE':
|
||||
return action.payload;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import {
|
||||
AdvancedOptionsState,
|
||||
AlertState,
|
||||
AlertThresholdState,
|
||||
EvaluationWindowState,
|
||||
NotificationSettingsState,
|
||||
} from './context/types';
|
||||
|
||||
export interface CreateAlertV2Props {
|
||||
alertType: AlertTypes;
|
||||
}
|
||||
|
||||
export interface GetCreateAlertLocalStateFromAlertDefReturn {
|
||||
basicAlertState: AlertState;
|
||||
thresholdState: AlertThresholdState;
|
||||
advancedOptionsState: AdvancedOptionsState;
|
||||
evaluationWindowState: EvaluationWindowState;
|
||||
notificationSettingsState: NotificationSettingsState;
|
||||
}
|
||||
@@ -1,301 +1,9 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Spin } from 'antd';
|
||||
import { TIMEZONE_DATA } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
import { getRandomColor } from 'container/ExplorerOptions/utils';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
import { v4 } from 'uuid';
|
||||
// UI side feature flag
|
||||
export const showNewCreateAlertsPage = (): boolean =>
|
||||
localStorage.getItem('showNewCreateAlertsPage') === 'true';
|
||||
|
||||
import { useCreateAlertState } from './context';
|
||||
import {
|
||||
INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
INITIAL_ALERT_STATE,
|
||||
INITIAL_ALERT_THRESHOLD_STATE,
|
||||
INITIAL_EVALUATION_WINDOW_STATE,
|
||||
INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
} from './context/constants';
|
||||
import {
|
||||
AdvancedOptionsState,
|
||||
AlertState,
|
||||
AlertThresholdMatchType,
|
||||
AlertThresholdOperator,
|
||||
AlertThresholdState,
|
||||
EvaluationWindowState,
|
||||
NotificationSettingsState,
|
||||
} from './context/types';
|
||||
import { EVALUATION_WINDOW_TIMEFRAME } from './EvaluationSettings/constants';
|
||||
import { GetCreateAlertLocalStateFromAlertDefReturn } from './types';
|
||||
|
||||
export function Spinner(): JSX.Element | null {
|
||||
const { isCreatingAlertRule, isUpdatingAlertRule } = useCreateAlertState();
|
||||
|
||||
if (!isCreatingAlertRule && !isUpdatingAlertRule) return null;
|
||||
|
||||
return createPortal(
|
||||
<div className="sticky-page-spinner">
|
||||
<Spin size="large" spinning />
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
export function getColorForThreshold(thresholdLabel: string): string {
|
||||
if (thresholdLabel === 'critical') {
|
||||
return Color.BG_SAKURA_500;
|
||||
}
|
||||
if (thresholdLabel === 'warning') {
|
||||
return Color.BG_AMBER_500;
|
||||
}
|
||||
if (thresholdLabel === 'info') {
|
||||
return Color.BG_ROBIN_500;
|
||||
}
|
||||
return getRandomColor();
|
||||
}
|
||||
|
||||
export function parseGoTime(
|
||||
input: string,
|
||||
): { time: number; unit: UniversalYAxisUnit } {
|
||||
const regex = /(\d+)([hms])/g;
|
||||
const matches = [...input.matchAll(regex)];
|
||||
|
||||
const nonZero = matches.find(([, value]) => parseInt(value, 10) > 0);
|
||||
if (!nonZero) {
|
||||
return { time: 1, unit: UniversalYAxisUnit.MINUTES };
|
||||
}
|
||||
|
||||
const time = parseInt(nonZero[1], 10);
|
||||
const unitMap: Record<string, UniversalYAxisUnit> = {
|
||||
h: UniversalYAxisUnit.HOURS,
|
||||
m: UniversalYAxisUnit.MINUTES,
|
||||
s: UniversalYAxisUnit.SECONDS,
|
||||
};
|
||||
|
||||
return { time, unit: unitMap[nonZero[2]] };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function getEvaluationWindowStateFromAlertDef(
|
||||
alertDef: PostableAlertRuleV2,
|
||||
): EvaluationWindowState {
|
||||
const windowType = alertDef.evaluation?.kind as 'rolling' | 'cumulative';
|
||||
|
||||
function getRollingWindowTimeframe(): string {
|
||||
if (
|
||||
// Default values for rolling window
|
||||
EVALUATION_WINDOW_TIMEFRAME.rolling
|
||||
.map((option) => option.value)
|
||||
.includes(alertDef.evaluation?.spec?.evalWindow || '')
|
||||
) {
|
||||
return alertDef.evaluation?.spec?.evalWindow || '';
|
||||
}
|
||||
return 'custom';
|
||||
}
|
||||
|
||||
function getCumulativeWindowTimeframe(): string {
|
||||
switch (alertDef.evaluation?.spec?.schedule?.type) {
|
||||
case 'hourly':
|
||||
return 'currentHour';
|
||||
case 'daily':
|
||||
return 'currentDay';
|
||||
case 'monthly':
|
||||
return 'currentMonth';
|
||||
default:
|
||||
return 'currentHour';
|
||||
}
|
||||
}
|
||||
|
||||
function convertApiFieldToTime(hour: number, minute: number): string {
|
||||
return `${hour.toString().padStart(2, '0')}:${minute
|
||||
.toString()
|
||||
.padStart(2, '0')}:00`;
|
||||
}
|
||||
|
||||
function getCumulativeWindowStartingAt(): EvaluationWindowState['startingAt'] {
|
||||
const timeframe = getCumulativeWindowTimeframe();
|
||||
if (timeframe === 'currentHour') {
|
||||
return {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE.startingAt,
|
||||
number: alertDef.evaluation?.spec?.schedule?.minute?.toString() || '0',
|
||||
};
|
||||
}
|
||||
if (timeframe === 'currentDay') {
|
||||
return {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE.startingAt,
|
||||
time: convertApiFieldToTime(
|
||||
alertDef.evaluation?.spec?.schedule?.hour || 0,
|
||||
alertDef.evaluation?.spec?.schedule?.minute || 0,
|
||||
),
|
||||
timezone: alertDef.evaluation?.spec?.timezone || TIMEZONE_DATA[0].value,
|
||||
};
|
||||
}
|
||||
if (timeframe === 'currentMonth') {
|
||||
return {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE.startingAt,
|
||||
number: alertDef.evaluation?.spec?.schedule?.day?.toString() || '0',
|
||||
timezone: alertDef.evaluation?.spec?.timezone || TIMEZONE_DATA[0].value,
|
||||
time: convertApiFieldToTime(
|
||||
alertDef.evaluation?.spec?.schedule?.hour || 0,
|
||||
alertDef.evaluation?.spec?.schedule?.minute || 0,
|
||||
),
|
||||
};
|
||||
}
|
||||
return INITIAL_EVALUATION_WINDOW_STATE.startingAt;
|
||||
}
|
||||
|
||||
if (windowType === 'rolling') {
|
||||
const timeframe = getRollingWindowTimeframe();
|
||||
if (timeframe === 'custom') {
|
||||
return {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE,
|
||||
windowType,
|
||||
timeframe,
|
||||
startingAt: {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE.startingAt,
|
||||
number: parseGoTime(
|
||||
alertDef.evaluation?.spec?.evalWindow || '1m',
|
||||
).time.toString(),
|
||||
unit: parseGoTime(alertDef.evaluation?.spec?.evalWindow || '1m').unit,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE,
|
||||
windowType,
|
||||
timeframe,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...INITIAL_EVALUATION_WINDOW_STATE,
|
||||
windowType,
|
||||
timeframe: getCumulativeWindowTimeframe(),
|
||||
startingAt: getCumulativeWindowStartingAt(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getNotificationSettingsStateFromAlertDef(
|
||||
alertDef: PostableAlertRuleV2,
|
||||
): NotificationSettingsState {
|
||||
const description = alertDef.annotations?.description || '';
|
||||
const multipleNotifications = alertDef.notificationSettings?.groupBy || [];
|
||||
const routingPolicies = alertDef.notificationSettings?.usePolicy || false;
|
||||
|
||||
const reNotificationEnabled =
|
||||
alertDef.notificationSettings?.renotify?.enabled || false;
|
||||
const reNotificationConditions =
|
||||
alertDef.notificationSettings?.renotify?.alertStates?.map(
|
||||
(state) => state as 'firing' | 'nodata',
|
||||
) || [];
|
||||
const reNotificationValue = alertDef.notificationSettings?.renotify
|
||||
? parseGoTime(alertDef.notificationSettings.renotify.interval || '30m').time
|
||||
: 30;
|
||||
const reNotificationUnit = alertDef.notificationSettings?.renotify
|
||||
? parseGoTime(alertDef.notificationSettings.renotify.interval || '30m').unit
|
||||
: UniversalYAxisUnit.MINUTES;
|
||||
|
||||
return {
|
||||
...INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
description,
|
||||
multipleNotifications,
|
||||
routingPolicies,
|
||||
reNotification: {
|
||||
enabled: reNotificationEnabled,
|
||||
conditions: reNotificationConditions,
|
||||
value: reNotificationValue,
|
||||
unit: reNotificationUnit,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getAdvancedOptionsStateFromAlertDef(
|
||||
alertDef: PostableAlertRuleV2,
|
||||
): AdvancedOptionsState {
|
||||
return {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
sendNotificationIfDataIsMissing: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE.sendNotificationIfDataIsMissing,
|
||||
toleranceLimit: alertDef.condition.absentFor || 0,
|
||||
enabled: alertDef.condition.alertOnAbsent || false,
|
||||
},
|
||||
enforceMinimumDatapoints: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE.enforceMinimumDatapoints,
|
||||
minimumDatapoints: alertDef.condition.requiredNumPoints || 0,
|
||||
enabled: alertDef.condition.requireMinPoints || false,
|
||||
},
|
||||
evaluationCadence: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE.evaluationCadence,
|
||||
mode: 'default',
|
||||
default: {
|
||||
...INITIAL_ADVANCED_OPTIONS_STATE.evaluationCadence.default,
|
||||
value: parseGoTime(alertDef.evaluation?.spec?.frequency || '1m').time,
|
||||
timeUnit: parseGoTime(alertDef.evaluation?.spec?.frequency || '1m').unit,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getThresholdStateFromAlertDef(
|
||||
alertDef: PostableAlertRuleV2,
|
||||
): AlertThresholdState {
|
||||
return {
|
||||
...INITIAL_ALERT_THRESHOLD_STATE,
|
||||
thresholds:
|
||||
alertDef.condition.thresholds?.spec.map((threshold) => ({
|
||||
id: v4(),
|
||||
label: threshold.name,
|
||||
thresholdValue: threshold.target,
|
||||
recoveryThresholdValue: null,
|
||||
unit: threshold.targetUnit,
|
||||
color: getColorForThreshold(threshold.name),
|
||||
channels: threshold.channels,
|
||||
})) || [],
|
||||
selectedQuery: alertDef.condition.selectedQueryName || '',
|
||||
operator:
|
||||
(alertDef.condition.thresholds?.spec[0].op as AlertThresholdOperator) ||
|
||||
AlertThresholdOperator.IS_ABOVE,
|
||||
matchType:
|
||||
(alertDef.condition.thresholds?.spec[0]
|
||||
.matchType as AlertThresholdMatchType) ||
|
||||
AlertThresholdMatchType.AT_LEAST_ONCE,
|
||||
};
|
||||
}
|
||||
|
||||
export function getCreateAlertLocalStateFromAlertDef(
|
||||
alertDef: PostableAlertRuleV2 | undefined,
|
||||
): GetCreateAlertLocalStateFromAlertDefReturn {
|
||||
if (!alertDef) {
|
||||
return {
|
||||
basicAlertState: INITIAL_ALERT_STATE,
|
||||
thresholdState: INITIAL_ALERT_THRESHOLD_STATE,
|
||||
advancedOptionsState: INITIAL_ADVANCED_OPTIONS_STATE,
|
||||
evaluationWindowState: INITIAL_EVALUATION_WINDOW_STATE,
|
||||
notificationSettingsState: INITIAL_NOTIFICATION_SETTINGS_STATE,
|
||||
};
|
||||
}
|
||||
// Basic alert state
|
||||
const basicAlertState: AlertState = {
|
||||
...INITIAL_ALERT_STATE,
|
||||
name: alertDef.alert,
|
||||
labels: alertDef.labels || {},
|
||||
yAxisUnit: alertDef.condition.compositeQuery.unit,
|
||||
};
|
||||
|
||||
const thresholdState = getThresholdStateFromAlertDef(alertDef);
|
||||
|
||||
const advancedOptionsState = getAdvancedOptionsStateFromAlertDef(alertDef);
|
||||
|
||||
const evaluationWindowState = getEvaluationWindowStateFromAlertDef(alertDef);
|
||||
|
||||
const notificationSettingsState = getNotificationSettingsStateFromAlertDef(
|
||||
alertDef,
|
||||
);
|
||||
|
||||
return {
|
||||
basicAlertState,
|
||||
thresholdState,
|
||||
advancedOptionsState,
|
||||
evaluationWindowState,
|
||||
notificationSettingsState,
|
||||
};
|
||||
}
|
||||
// UI side FF to switch between the 2 layouts of the create alert page
|
||||
// Layout 1 - Default layout
|
||||
// Layout 2 - Condensed layout
|
||||
export const showCondensedLayout = (): boolean =>
|
||||
localStorage.getItem('showCondensedLayout') === 'true';
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import '../CreateAlertV2/CreateAlertV2.styles.scss';
|
||||
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||
import { useMemo } from 'react';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
|
||||
import AlertCondition from '../CreateAlertV2/AlertCondition';
|
||||
import { buildInitialAlertDef } from '../CreateAlertV2/context/utils';
|
||||
import Footer from '../CreateAlertV2/Footer';
|
||||
import NotificationSettings from '../CreateAlertV2/NotificationSettings';
|
||||
import QuerySection from '../CreateAlertV2/QuerySection';
|
||||
import { Spinner } from '../CreateAlertV2/utils';
|
||||
|
||||
interface EditAlertV2Props {
|
||||
alertType?: AlertTypes;
|
||||
initialAlert: PostableAlertRuleV2;
|
||||
}
|
||||
|
||||
function EditAlertV2({
|
||||
alertType = AlertTypes.METRICS_BASED_ALERT,
|
||||
initialAlert,
|
||||
}: EditAlertV2Props): JSX.Element {
|
||||
const currentQueryToRedirect = useMemo(() => {
|
||||
const basicAlertDef = buildInitialAlertDef(alertType);
|
||||
return mapQueryDataFromApi(
|
||||
initialAlert?.condition.compositeQuery ||
|
||||
basicAlertDef.condition.compositeQuery,
|
||||
);
|
||||
}, [initialAlert, alertType]);
|
||||
|
||||
useShareBuilderUrl({ defaultValue: currentQueryToRedirect });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Spinner />
|
||||
<div className="create-alert-v2-container">
|
||||
<QuerySection />
|
||||
<AlertCondition />
|
||||
<NotificationSettings />
|
||||
</div>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
EditAlertV2.defaultProps = {
|
||||
alertType: AlertTypes.METRICS_BASED_ALERT,
|
||||
};
|
||||
|
||||
export default EditAlertV2;
|
||||
@@ -1,3 +0,0 @@
|
||||
import EditAlertV2 from './EditAlertV2';
|
||||
|
||||
export default EditAlertV2;
|
||||
@@ -1,32 +1,11 @@
|
||||
import { Form } from 'antd';
|
||||
import EditAlertV2 from 'container/EditAlertV2';
|
||||
import FormAlertRules from 'container/FormAlertRules';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import {
|
||||
NEW_ALERT_SCHEMA_VERSION,
|
||||
PostableAlertRuleV2,
|
||||
} from 'types/api/alerts/alertTypesV2';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
function EditRules({
|
||||
initialValue,
|
||||
ruleId,
|
||||
initialV2AlertValue,
|
||||
}: EditRulesProps): JSX.Element {
|
||||
function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
|
||||
const [formInstance] = Form.useForm();
|
||||
|
||||
if (
|
||||
initialV2AlertValue !== null &&
|
||||
initialV2AlertValue.schemaVersion === NEW_ALERT_SCHEMA_VERSION
|
||||
) {
|
||||
return (
|
||||
<EditAlertV2
|
||||
initialAlert={initialV2AlertValue}
|
||||
alertType={initialValue.alertType as AlertTypes}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormAlertRules
|
||||
alertType={
|
||||
@@ -44,7 +23,6 @@ function EditRules({
|
||||
interface EditRulesProps {
|
||||
initialValue: AlertDef;
|
||||
ruleId: string;
|
||||
initialV2AlertValue: PostableAlertRuleV2 | null;
|
||||
}
|
||||
|
||||
export default EditRules;
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
.prom-ql-icon {
|
||||
height: 14px;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import './QuerySection.styles.scss';
|
||||
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Tabs, Tooltip, Typography } from 'antd';
|
||||
import { Button, Tabs, Tooltip } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import PromQLIcon from 'assets/Dashboard/PromQl';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
@@ -71,7 +71,6 @@ function QuerySection({
|
||||
<Tooltip title="Query Builder">
|
||||
<Button className="nav-btns">
|
||||
<Atom size={14} />
|
||||
<Typography.Text>Query Builder</Typography.Text>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
),
|
||||
@@ -82,7 +81,6 @@ function QuerySection({
|
||||
<Tooltip title="ClickHouse">
|
||||
<Button className="nav-btns">
|
||||
<Terminal size={14} />
|
||||
<Typography.Text>ClickHouse Query</Typography.Text>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
),
|
||||
@@ -97,7 +95,6 @@ function QuerySection({
|
||||
<Tooltip title="Query Builder">
|
||||
<Button className="nav-btns" data-testid="query-builder-tab">
|
||||
<Atom size={14} />
|
||||
<Typography.Text>Query Builder</Typography.Text>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
),
|
||||
@@ -108,7 +105,6 @@ function QuerySection({
|
||||
<Tooltip title="ClickHouse">
|
||||
<Button className="nav-btns">
|
||||
<Terminal size={14} />
|
||||
<Typography.Text>ClickHouse Query</Typography.Text>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
),
|
||||
@@ -121,7 +117,6 @@ function QuerySection({
|
||||
<PromQLIcon
|
||||
fillColor={isDarkMode ? Color.BG_VANILLA_200 : Color.BG_INK_300}
|
||||
/>
|
||||
<Typography.Text>PromQL</Typography.Text>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
),
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
Flex,
|
||||
Input,
|
||||
MenuProps,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { 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';
|
||||
@@ -39,7 +31,7 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { GettableAlert } from 'types/api/alerts/get';
|
||||
|
||||
import DeleteAlert from './DeleteAlert';
|
||||
import { ColumnButton, SearchContainer } from './styles';
|
||||
import { Button, ColumnButton, SearchContainer } from './styles';
|
||||
import Status from './TableComponents/Status';
|
||||
import ToggleAlertState from './ToggleAlertState';
|
||||
import { alertActionLogEvent, filterAlerts } from './utils';
|
||||
@@ -105,41 +97,14 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
});
|
||||
}, [notificationsApi, t]);
|
||||
|
||||
const onClickNewAlertV2Handler = useCallback(() => {
|
||||
const onClickNewAlertHandler = useCallback(() => {
|
||||
logEvent('Alert: New alert button clicked', {
|
||||
number: allAlertRules?.length,
|
||||
layout: 'new',
|
||||
});
|
||||
history.push(`${ROUTES.ALERTS_NEW}?showNewCreateAlertsPage=true`);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const onClickNewClassicAlertHandler = useCallback(() => {
|
||||
logEvent('Alert: New alert button clicked', {
|
||||
number: allAlertRules?.length,
|
||||
layout: 'classic',
|
||||
});
|
||||
history.push(ROUTES.ALERTS_NEW);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const newAlertMenuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: 'new',
|
||||
label: (
|
||||
<span>
|
||||
Try the new experience <Tag color="blue">Beta</Tag>
|
||||
</span>
|
||||
),
|
||||
onClick: onClickNewAlertV2Handler,
|
||||
},
|
||||
{
|
||||
key: 'classic',
|
||||
label: 'Continue with the classic experience',
|
||||
onClick: onClickNewClassicAlertHandler,
|
||||
},
|
||||
];
|
||||
|
||||
const onEditHandler = (record: GettableAlert, openInNewTab: boolean): void => {
|
||||
const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery);
|
||||
params.set(
|
||||
@@ -403,11 +368,13 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
/>
|
||||
<Flex gap={12}>
|
||||
{addNewAlert && (
|
||||
<Dropdown menu={{ items: newAlertMenuItems }} trigger={['click']}>
|
||||
<Button type="primary" icon={<PlusOutlined />}>
|
||||
New Alert
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={onClickNewAlertHandler}
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
New Alert
|
||||
</Button>
|
||||
)}
|
||||
<TextToolTip
|
||||
{...{
|
||||
|
||||
@@ -88,7 +88,6 @@ function RoutingPolicies(): JSX.Element {
|
||||
isRoutingPoliciesError={isErrorRoutingPolicies}
|
||||
handlePolicyDetailsModalOpen={handlePolicyDetailsModalOpen}
|
||||
handleDeleteModalOpen={handleDeleteModalOpen}
|
||||
hasSearchTerm={(searchTerm?.length ?? 0) > 0}
|
||||
/>
|
||||
{policyDetailsModalState.isOpen && (
|
||||
<RoutingPolicyDetails
|
||||
|
||||
@@ -10,7 +10,6 @@ function RoutingPolicyList({
|
||||
isRoutingPoliciesError,
|
||||
handlePolicyDetailsModalOpen,
|
||||
handleDeleteModalOpen,
|
||||
hasSearchTerm,
|
||||
}: RoutingPolicyListProps): JSX.Element {
|
||||
const columns: TableProps<RoutingPolicy>['columns'] = [
|
||||
{
|
||||
@@ -26,7 +25,6 @@ function RoutingPolicyList({
|
||||
},
|
||||
];
|
||||
|
||||
/* eslint-disable no-nested-ternary */
|
||||
const localeEmptyState = useMemo(
|
||||
() => (
|
||||
<div className="no-routing-policies-message-container">
|
||||
@@ -43,23 +41,12 @@ function RoutingPolicyList({
|
||||
<Typography.Text>
|
||||
Something went wrong while fetching routing policies.
|
||||
</Typography.Text>
|
||||
) : hasSearchTerm ? (
|
||||
<Typography.Text>No matching routing policies found.</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text>
|
||||
No routing policies yet,{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/alerts-management/routing-policy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn more here
|
||||
</a>
|
||||
</Typography.Text>
|
||||
<Typography.Text>No routing policies found.</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
[isRoutingPoliciesError, hasSearchTerm],
|
||||
[isRoutingPoliciesError],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -28,7 +28,6 @@ describe('RoutingPoliciesList', () => {
|
||||
isRoutingPoliciesError={useRoutingPolicesMockData.isErrorRoutingPolicies}
|
||||
handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen}
|
||||
handleDeleteModalOpen={mockHandleDeleteModalOpen}
|
||||
hasSearchTerm={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -52,7 +51,6 @@ describe('RoutingPoliciesList', () => {
|
||||
isRoutingPoliciesError={false}
|
||||
handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen}
|
||||
handleDeleteModalOpen={mockHandleDeleteModalOpen}
|
||||
hasSearchTerm={false}
|
||||
/>,
|
||||
);
|
||||
// Check for loading spinner by class name
|
||||
@@ -69,7 +67,6 @@ describe('RoutingPoliciesList', () => {
|
||||
isRoutingPoliciesError
|
||||
handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen}
|
||||
handleDeleteModalOpen={mockHandleDeleteModalOpen}
|
||||
hasSearchTerm={false}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
@@ -85,9 +82,8 @@ describe('RoutingPoliciesList', () => {
|
||||
isRoutingPoliciesError={false}
|
||||
handlePolicyDetailsModalOpen={mockHandlePolicyDetailsModalOpen}
|
||||
handleDeleteModalOpen={mockHandleDeleteModalOpen}
|
||||
hasSearchTerm={false}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('No routing policies yet,')).toBeInTheDocument();
|
||||
expect(screen.getByText('No routing policies found.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,6 @@ export interface RoutingPolicyListProps {
|
||||
isRoutingPoliciesError: boolean;
|
||||
handlePolicyDetailsModalOpen: HandlePolicyDetailsModalOpen;
|
||||
handleDeleteModalOpen: HandleDeleteModalOpen;
|
||||
hasSearchTerm: boolean;
|
||||
}
|
||||
|
||||
export interface RoutingPolicyListItemProps {
|
||||
|
||||
@@ -5,6 +5,10 @@ import { SuccessResponseV2 } from 'types/api';
|
||||
|
||||
import { RoutingPolicy } from './types';
|
||||
|
||||
export function showRoutingPoliciesPage(): boolean {
|
||||
return localStorage.getItem('showRoutingPoliciesPage') === 'true';
|
||||
}
|
||||
|
||||
export function mapApiResponseToRoutingPolicies(
|
||||
response: SuccessResponseV2<GetRoutingPoliciesResponse>,
|
||||
): RoutingPolicy[] {
|
||||
@@ -32,7 +36,9 @@ export function mapRoutingPolicyToCreateApiPayload(
|
||||
return {
|
||||
name,
|
||||
expression,
|
||||
channels,
|
||||
actions: {
|
||||
channels,
|
||||
},
|
||||
description,
|
||||
};
|
||||
}
|
||||
@@ -47,7 +53,9 @@ export function mapRoutingPolicyToUpdateApiPayload(
|
||||
return {
|
||||
name,
|
||||
expression,
|
||||
channels,
|
||||
actions: {
|
||||
channels,
|
||||
},
|
||||
description,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import createAlertRule, {
|
||||
CreateAlertRuleResponse,
|
||||
} from 'api/alerts/createAlertRule';
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
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),
|
||||
});
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import testAlertRule, { TestAlertRuleResponse } from 'api/alerts/testAlertRule';
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
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),
|
||||
});
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import updateAlertRule, {
|
||||
UpdateAlertRuleResponse,
|
||||
} from 'api/alerts/updateAlertRule';
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
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),
|
||||
});
|
||||
}
|
||||
@@ -6,14 +6,10 @@ import { Filters } from 'components/AlertDetailsFilters/Filters';
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import Spinner from 'components/Spinner';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { CreateAlertProvider } from 'container/CreateAlertV2/context';
|
||||
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';
|
||||
import history from 'lib/history';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2';
|
||||
|
||||
import AlertHeader from './AlertHeader/AlertHeader';
|
||||
import { useGetAlertRuleDetails, useRouteTabUtils } from './hooks';
|
||||
@@ -89,16 +85,6 @@ function AlertDetails(): JSX.Element {
|
||||
document.title = alertTitle || document.title;
|
||||
}, [alertDetailsResponse?.payload?.data.alert, isRefetching]);
|
||||
|
||||
const alertRuleDetails = useMemo(
|
||||
() => alertDetailsResponse?.payload?.data as PostableAlertRuleV2 | undefined,
|
||||
[alertDetailsResponse],
|
||||
);
|
||||
|
||||
const initialAlertState = useMemo(
|
||||
() => getCreateAlertLocalStateFromAlertDef(alertRuleDetails),
|
||||
[alertRuleDetails],
|
||||
);
|
||||
|
||||
if (
|
||||
isError ||
|
||||
!isValidRuleId ||
|
||||
@@ -118,43 +104,36 @@ function AlertDetails(): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<CreateAlertProvider
|
||||
ruleId={ruleId || ''}
|
||||
isEditMode
|
||||
initialAlertType={alertRuleDetails?.alertType as AlertTypes}
|
||||
initialAlertState={initialAlertState}
|
||||
>
|
||||
<div className="alert-details">
|
||||
<Breadcrumb
|
||||
className="alert-details__breadcrumb"
|
||||
items={[
|
||||
{
|
||||
title: (
|
||||
<BreadCrumbItem title="Alert Rules" route={ROUTES.LIST_ALL_ALERT} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <BreadCrumbItem title={ruleId} isLast />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Divider className="divider breadcrumb-divider" />
|
||||
<div className="alert-details">
|
||||
<Breadcrumb
|
||||
className="alert-details__breadcrumb"
|
||||
items={[
|
||||
{
|
||||
title: (
|
||||
<BreadCrumbItem title="Alert Rules" route={ROUTES.LIST_ALL_ALERT} />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: <BreadCrumbItem title={ruleId} isLast />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Divider className="divider breadcrumb-divider" />
|
||||
|
||||
<AlertDetailsStatusRenderer
|
||||
{...{ isLoading, isError, isRefetching, data: alertDetailsResponse }}
|
||||
<AlertDetailsStatusRenderer
|
||||
{...{ isLoading, isError, isRefetching, data: alertDetailsResponse }}
|
||||
/>
|
||||
<Divider className="divider" />
|
||||
<div className="tabs-and-filters">
|
||||
<RouteTab
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
onChangeHandler={handleTabChange}
|
||||
tabBarExtraContent={<Filters />}
|
||||
/>
|
||||
<Divider className="divider" />
|
||||
<div className="tabs-and-filters">
|
||||
<RouteTab
|
||||
routes={routes}
|
||||
activeKey={pathname}
|
||||
history={history}
|
||||
onChangeHandler={handleTabChange}
|
||||
tabBarExtraContent={<Filters />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CreateAlertProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import CopyToClipboard from 'periscope/components/CopyToClipboard';
|
||||
import { useAlertRule } from 'providers/Alert';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { CSSProperties } from 'styled-components';
|
||||
import { NEW_ALERT_SCHEMA_VERSION } from 'types/api/alerts/alertTypesV2';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
import { AlertHeaderProps } from '../AlertHeader';
|
||||
@@ -61,20 +60,14 @@ function AlertActionButtons({
|
||||
setIsRenameAlertOpen(false);
|
||||
}, [handleAlertUpdate]);
|
||||
|
||||
const isV2Alert = alertDetails.schemaVersion === NEW_ALERT_SCHEMA_VERSION;
|
||||
|
||||
const menuItems: MenuProps['items'] = [
|
||||
...(!isV2Alert
|
||||
? [
|
||||
{
|
||||
key: 'rename-rule',
|
||||
label: 'Rename',
|
||||
icon: <PenLine size={16} color={Color.BG_VANILLA_400} />,
|
||||
onClick: handleRename,
|
||||
style: menuItemStyle,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: 'rename-rule',
|
||||
label: 'Rename',
|
||||
icon: <PenLine size={16} color={Color.BG_VANILLA_400} />,
|
||||
onClick: handleRename,
|
||||
style: menuItemStyle,
|
||||
},
|
||||
{
|
||||
key: 'duplicate-rule',
|
||||
label: 'Duplicate',
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import './AlertHeader.styles.scss';
|
||||
|
||||
import CreateAlertV2Header from 'container/CreateAlertV2/CreateAlertHeader';
|
||||
import LineClampedText from 'periscope/components/LineClampedText/LineClampedText';
|
||||
import { useAlertRule } from 'providers/Alert';
|
||||
import { useMemo, useState } from 'react';
|
||||
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 +10,13 @@ import AlertSeverity from './AlertSeverity/AlertSeverity';
|
||||
import AlertState from './AlertState/AlertState';
|
||||
|
||||
export type AlertHeaderProps = {
|
||||
alertDetails: GettableAlert | PostableAlertRuleV2;
|
||||
alertDetails: {
|
||||
state: string;
|
||||
alert: string;
|
||||
id: string;
|
||||
labels: Record<string, string | undefined> | undefined;
|
||||
disabled: boolean;
|
||||
};
|
||||
};
|
||||
function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
||||
const { state, alert: alertName, labels } = alertDetails;
|
||||
@@ -32,38 +32,32 @@ function AlertHeader({ alertDetails }: AlertHeaderProps): JSX.Element {
|
||||
return {};
|
||||
}, [labels]);
|
||||
|
||||
const isV2Alert = alertDetails.schemaVersion === NEW_ALERT_SCHEMA_VERSION;
|
||||
|
||||
const CreateAlertV1Header = (
|
||||
<div className="alert-info__info-wrapper">
|
||||
<div className="top-section">
|
||||
<div className="alert-title-wrapper">
|
||||
<AlertState state={alertRuleState ?? state ?? ''} />
|
||||
<div className="alert-title">
|
||||
<LineClampedText text={updatedName || alertName} />
|
||||
return (
|
||||
<div className="alert-info">
|
||||
<div className="alert-info__info-wrapper">
|
||||
<div className="top-section">
|
||||
<div className="alert-title-wrapper">
|
||||
<AlertState state={alertRuleState ?? state} />
|
||||
<div className="alert-title">
|
||||
<LineClampedText text={updatedName || alertName} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bottom-section">
|
||||
{labels?.severity && <AlertSeverity severity={labels.severity} />}
|
||||
<div className="bottom-section">
|
||||
{labels?.severity && <AlertSeverity severity={labels.severity} />}
|
||||
|
||||
{/* // TODO(shaheer): Get actual data when we are able to get alert firing from state from API */}
|
||||
{/* <AlertStatus
|
||||
{/* // TODO(shaheer): Get actual data when we are able to get alert firing from state from API */}
|
||||
{/* <AlertStatus
|
||||
status="firing"
|
||||
timestamp={dayjs().subtract(1, 'd').valueOf()}
|
||||
/> */}
|
||||
<AlertLabels labels={labelsWithoutSeverity} />
|
||||
<AlertLabels labels={labelsWithoutSeverity} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="alert-info">
|
||||
{isV2Alert ? <CreateAlertV2Header /> : CreateAlertV1Header}
|
||||
<div className="alert-info__action-buttons">
|
||||
<AlertActionButtons
|
||||
alertDetails={alertDetails}
|
||||
ruleId={alertDetails?.id || ''}
|
||||
ruleId={alertDetails.id}
|
||||
setUpdatedName={setUpdatedName}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ import ROUTES from 'constants/routes';
|
||||
import AllAlertRules from 'container/ListAlertRules';
|
||||
import { PlannedDowntime } from 'container/PlannedDowntime/PlannedDowntime';
|
||||
import RoutingPolicies from 'container/RoutingPolicies';
|
||||
import { showRoutingPoliciesPage } from 'container/RoutingPolicies/utils';
|
||||
import TriggeredAlerts from 'container/TriggeredAlerts';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
@@ -27,27 +28,36 @@ function AllAlertList(): JSX.Element {
|
||||
|
||||
const search = urlQuery.get('search');
|
||||
|
||||
const showRoutingPoliciesPageFlag = showRoutingPoliciesPage();
|
||||
|
||||
const configurationTab = useMemo(() => {
|
||||
const tabs = [
|
||||
{
|
||||
label: 'Planned Downtime',
|
||||
key: 'planned-downtime',
|
||||
children: <PlannedDowntime />,
|
||||
},
|
||||
{
|
||||
label: 'Routing Policies',
|
||||
key: 'routing-policies',
|
||||
children: <RoutingPolicies />,
|
||||
},
|
||||
];
|
||||
if (showRoutingPoliciesPageFlag) {
|
||||
const tabs = [
|
||||
{
|
||||
label: 'Planned Downtime',
|
||||
key: 'planned-downtime',
|
||||
children: <PlannedDowntime />,
|
||||
},
|
||||
{
|
||||
label: 'Routing Policies',
|
||||
key: 'routing-policies',
|
||||
children: <RoutingPolicies />,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Tabs
|
||||
className="configuration-tabs"
|
||||
defaultActiveKey="planned-downtime"
|
||||
items={tabs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Tabs
|
||||
className="configuration-tabs"
|
||||
defaultActiveKey="planned-downtime"
|
||||
items={tabs}
|
||||
/>
|
||||
<div className="planned-downtime-container">
|
||||
<PlannedDowntime />
|
||||
</div>
|
||||
);
|
||||
}, []);
|
||||
}, [showRoutingPoliciesPageFlag]);
|
||||
|
||||
const items: TabsProps['items'] = [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import CreateAlertRule from 'container/CreateAlertRule';
|
||||
import { showNewCreateAlertsPage } from 'container/CreateAlertV2/utils';
|
||||
import { lazy } from 'react';
|
||||
|
||||
const CreateAlertV2 = lazy(() => import('container/CreateAlertV2'));
|
||||
|
||||
function CreateAlertPage(): JSX.Element {
|
||||
const showNewCreateAlertsPageFlag = showNewCreateAlertsPage();
|
||||
|
||||
if (showNewCreateAlertsPageFlag) {
|
||||
return <CreateAlertV2 />;
|
||||
}
|
||||
|
||||
return <CreateAlertRule />;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,6 @@ import history from 'lib/history';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import {
|
||||
NEW_ALERT_SCHEMA_VERSION,
|
||||
PostableAlertRuleV2,
|
||||
} from 'types/api/alerts/alertTypesV2';
|
||||
|
||||
import {
|
||||
errorMessageReceivedFromBackend,
|
||||
@@ -92,18 +88,9 @@ function EditRules(): JSX.Element {
|
||||
return <Spinner tip="Loading Rules..." />;
|
||||
}
|
||||
|
||||
let initialV2AlertValue: PostableAlertRuleV2 | null = null;
|
||||
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={data.payload.data}
|
||||
initialV2AlertValue={initialV2AlertValue}
|
||||
/>
|
||||
<EditRulesContainer ruleId={ruleId || ''} initialValue={data.payload.data} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { AlertTypes } from './alertTypes';
|
||||
import { ICompositeMetricQuery } from './compositeQuery';
|
||||
import { Labels } from './def';
|
||||
|
||||
export interface BasicThreshold {
|
||||
name: string;
|
||||
target: number;
|
||||
matchType: string;
|
||||
op: string;
|
||||
channels: string[];
|
||||
targetUnit: string;
|
||||
}
|
||||
|
||||
export interface PostableAlertRuleV2 {
|
||||
schemaVersion: string;
|
||||
id?: string;
|
||||
alert: string;
|
||||
alertType?: AlertTypes;
|
||||
ruleType?: string;
|
||||
condition: {
|
||||
thresholds?: {
|
||||
kind: string;
|
||||
spec: BasicThreshold[];
|
||||
};
|
||||
compositeQuery: ICompositeMetricQuery;
|
||||
selectedQueryName?: string;
|
||||
alertOnAbsent?: boolean;
|
||||
absentFor?: number;
|
||||
requireMinPoints?: boolean;
|
||||
requiredNumPoints?: number;
|
||||
};
|
||||
evaluation?: {
|
||||
kind?: 'rolling' | 'cumulative';
|
||||
spec?: {
|
||||
evalWindow?: string;
|
||||
frequency?: string;
|
||||
schedule?: {
|
||||
type?: 'hourly' | 'daily' | 'monthly';
|
||||
minute?: number;
|
||||
hour?: number;
|
||||
day?: number;
|
||||
};
|
||||
timezone?: string;
|
||||
};
|
||||
};
|
||||
labels?: Labels;
|
||||
annotations?: {
|
||||
description: string;
|
||||
summary: string;
|
||||
};
|
||||
notificationSettings?: {
|
||||
groupBy?: string[];
|
||||
renotify?: {
|
||||
enabled: boolean;
|
||||
interval?: string;
|
||||
alertStates?: string[];
|
||||
};
|
||||
usePolicy?: boolean;
|
||||
};
|
||||
version?: string;
|
||||
source?: string;
|
||||
state?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface AlertRuleV2 extends PostableAlertRuleV2 {
|
||||
schemaVersion: string;
|
||||
state: string;
|
||||
disabled: boolean;
|
||||
createAt: string;
|
||||
createBy: string;
|
||||
updateAt: string;
|
||||
updateBy: string;
|
||||
}
|
||||
|
||||
export const NEW_ALERT_SCHEMA_VERSION = 'v2alpha1';
|
||||
@@ -13,7 +13,6 @@ export interface GettableAlert extends AlertDef {
|
||||
createBy: string;
|
||||
updateAt: string;
|
||||
updateBy: string;
|
||||
schemaVersion: string;
|
||||
}
|
||||
|
||||
export type PayloadProps = {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user