diff --git a/ee/query-service/rules/anomaly.go b/ee/query-service/rules/anomaly.go index 53f205e8d004..af988b5d6777 100644 --- a/ee/query-service/rules/anomaly.go +++ b/ee/query-service/rules/anomaly.go @@ -251,7 +251,7 @@ func (r *AnomalyRule) buildAndRunQuery(ctx context.Context, orgID valuer.UUID, t continue } } - results, err := r.Threshold.ShouldAlert(*series) + results, err := r.Threshold.ShouldAlert(*series, r.Unit()) 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) + results, err := r.Threshold.ShouldAlert(*series, r.Unit()) if err != nil { return nil, err } @@ -336,14 +336,19 @@ 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(r.TargetVal(), r.Unit()) + threshold := valueFormatter.Format(smpl.Target, smpl.TargetUnit) 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) @@ -408,13 +413,12 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro State: model.StatePending, Value: smpl.V, GeneratorURL: r.GeneratorURL(), - Receivers: r.PreferredChannels(), + Receivers: ruleReceiverMap[lbs.Map()[ruletypes.LabelThresholdName]], 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. @@ -423,7 +427,9 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (interface{}, erro alert.Value = a.Value alert.Annotations = a.Annotations - alert.Receivers = r.PreferredChannels() + if v, ok := alert.Labels.Map()[ruletypes.LabelThresholdName]; ok { + alert.Receivers = ruleReceiverMap[v] + } continue } diff --git a/ee/query-service/rules/manager.go b/ee/query-service/rules/manager.go index 3212031f9f3f..31009b3c3091 100644 --- a/ee/query-service/rules/manager.go +++ b/ee/query-service/rules/manager.go @@ -126,7 +126,6 @@ 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] = "" diff --git a/frontend/src/api/alerts/updateAlertRule.ts b/frontend/src/api/alerts/updateAlertRule.ts new file mode 100644 index 000000000000..6553d685338a --- /dev/null +++ b/frontend/src/api/alerts/updateAlertRule.ts @@ -0,0 +1,26 @@ +import axios from 'api'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { PostableAlertRuleV2 } from 'types/api/alerts/alertTypesV2'; + +export interface UpdateAlertRuleResponse { + data: string; + status: string; +} + +const updateAlertRule = async ( + id: string, + postableAlertRule: PostableAlertRuleV2, +): Promise | 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; diff --git a/frontend/src/api/routingPolicies/createRoutingPolicy.ts b/frontend/src/api/routingPolicies/createRoutingPolicy.ts index 5ec9847b69b7..69bf58bd8c86 100644 --- a/frontend/src/api/routingPolicies/createRoutingPolicy.ts +++ b/frontend/src/api/routingPolicies/createRoutingPolicy.ts @@ -6,9 +6,7 @@ import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api'; export interface CreateRoutingPolicyBody { name: string; expression: string; - actions: { - channels: string[]; - }; + channels: string[]; description?: string; } @@ -23,7 +21,7 @@ const createRoutingPolicy = async ( SuccessResponseV2 | ErrorResponseV2 > => { try { - const response = await axios.post(`/notification-policy`, props); + const response = await axios.post(`/route_policies`, props); return { httpStatusCode: response.status, data: response.data, diff --git a/frontend/src/api/routingPolicies/deleteRoutingPolicy.ts b/frontend/src/api/routingPolicies/deleteRoutingPolicy.ts index 5b0d3df14d97..444ec0b1ab99 100644 --- a/frontend/src/api/routingPolicies/deleteRoutingPolicy.ts +++ b/frontend/src/api/routingPolicies/deleteRoutingPolicy.ts @@ -14,9 +14,7 @@ const deleteRoutingPolicy = async ( SuccessResponseV2 | ErrorResponseV2 > => { try { - const response = await axios.delete( - `/notification-policy/${routingPolicyId}`, - ); + const response = await axios.delete(`/route_policies/${routingPolicyId}`); return { httpStatusCode: response.status, diff --git a/frontend/src/api/routingPolicies/getRoutingPolicies.ts b/frontend/src/api/routingPolicies/getRoutingPolicies.ts index 43191aebd77f..b06d359c9d8f 100644 --- a/frontend/src/api/routingPolicies/getRoutingPolicies.ts +++ b/frontend/src/api/routingPolicies/getRoutingPolicies.ts @@ -25,7 +25,7 @@ export const getRoutingPolicies = async ( headers?: Record, ): Promise | ErrorResponseV2> => { try { - const response = await axios.get('/notification-policy', { + const response = await axios.get('/route_policies', { signal, headers, }); diff --git a/frontend/src/api/routingPolicies/updateRoutingPolicy.ts b/frontend/src/api/routingPolicies/updateRoutingPolicy.ts index 08448562cdd0..63731fe5bf98 100644 --- a/frontend/src/api/routingPolicies/updateRoutingPolicy.ts +++ b/frontend/src/api/routingPolicies/updateRoutingPolicy.ts @@ -6,9 +6,7 @@ import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api'; export interface UpdateRoutingPolicyBody { name: string; expression: string; - actions: { - channels: string[]; - }; + channels: string[]; description: string; } @@ -24,7 +22,7 @@ const updateRoutingPolicy = async ( SuccessResponseV2 | ErrorResponseV2 > => { try { - const response = await axios.put(`/notification-policy/${id}`, { + const response = await axios.put(`/route_policies/${id}`, { ...props, }); diff --git a/frontend/src/container/CreateAlertRule/index.tsx b/frontend/src/container/CreateAlertRule/index.tsx index 89fc094584d9..b502dd09717a 100644 --- a/frontend/src/container/CreateAlertRule/index.tsx +++ b/frontend/src/container/CreateAlertRule/index.tsx @@ -3,7 +3,6 @@ import logEvent from 'api/common/logEvent'; import { ENTITY_VERSION_V5 } from 'constants/app'; import { QueryParams } from 'constants/query'; import CreateAlertV2 from 'container/CreateAlertV2'; -import { showNewCreateAlertsPage } from 'container/CreateAlertV2/utils'; import FormAlertRules, { AlertDetectionTypes } from 'container/FormAlertRules'; import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types'; import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam'; @@ -127,7 +126,8 @@ function CreateRules(): JSX.Element { ); } - const showNewCreateAlertsPageFlag = showNewCreateAlertsPage(); + const showNewCreateAlertsPageFlag = + queryParams.get('showNewCreateAlertsPage') === 'true'; if ( showNewCreateAlertsPageFlag && diff --git a/frontend/src/container/CreateAlertV2/AlertCondition/AlertCondition.tsx b/frontend/src/container/CreateAlertV2/AlertCondition/AlertCondition.tsx index 174af6e93de2..e25142deb482 100644 --- a/frontend/src/container/CreateAlertV2/AlertCondition/AlertCondition.tsx +++ b/frontend/src/container/CreateAlertV2/AlertCondition/AlertCondition.tsx @@ -13,14 +13,12 @@ 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 showCondensedLayoutFlag = showCondensedLayout(); const { data, @@ -108,11 +106,9 @@ function AlertCondition(): JSX.Element { refreshChannels={refreshChannels} /> )} - {showCondensedLayoutFlag ? ( -
- -
- ) : null} +
+ +
); } diff --git a/frontend/src/container/CreateAlertV2/AlertCondition/AlertThreshold.tsx b/frontend/src/container/CreateAlertV2/AlertCondition/AlertThreshold.tsx index c2d57e4c3829..95663daa518c 100644 --- a/frontend/src/container/CreateAlertV2/AlertCondition/AlertThreshold.tsx +++ b/frontend/src/container/CreateAlertV2/AlertCondition/AlertThreshold.tsx @@ -4,7 +4,10 @@ import '../EvaluationSettings/styles.scss'; import { Button, Select, Tooltip, Typography } from 'antd'; 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 { useCreateAlertState } from '../context'; import { @@ -15,7 +18,6 @@ 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 { @@ -40,12 +42,23 @@ function AlertThreshold({ setNotificationSettings, } = useCreateAlertState(); - const showCondensedLayoutFlag = showCondensedLayout(); - 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 || '', @@ -54,11 +67,15 @@ function AlertThreshold({ const addThreshold = (): void => { let newThreshold; if (thresholdState.thresholds.length === 1) { - newThreshold = INITIAL_WARNING_THRESHOLD; + newThreshold = { ...INITIAL_WARNING_THRESHOLD, id: v4() }; } else if (thresholdState.thresholds.length === 2) { - newThreshold = INITIAL_INFO_THRESHOLD; + newThreshold = { ...INITIAL_INFO_THRESHOLD, id: v4() }; } else { - newThreshold = INITIAL_RANDOM_THRESHOLD; + newThreshold = { + ...INITIAL_RANDOM_THRESHOLD, + id: v4(), + color: getRandomColor(), + }; } setThresholdState({ type: 'SET_THRESHOLDS', @@ -143,17 +160,12 @@ function AlertThreshold({ }), ); - const evaluationWindowContext = showCondensedLayoutFlag ? ( - - ) : ( - Evaluation Window. - ); - return (
{/* Main condition sentence */}
@@ -199,7 +211,7 @@ function AlertThreshold({ options={matchTypeOptionsWithTooltips} /> - during the {evaluationWindowContext} + during the
diff --git a/frontend/src/container/CreateAlertV2/AlertCondition/__tests__/AlertThreshold.test.tsx b/frontend/src/container/CreateAlertV2/AlertCondition/__tests__/AlertThreshold.test.tsx index 78cfadcb5bbe..468d67b8a840 100644 --- a/frontend/src/container/CreateAlertV2/AlertCondition/__tests__/AlertThreshold.test.tsx +++ b/frontend/src/container/CreateAlertV2/AlertCondition/__tests__/AlertThreshold.test.tsx @@ -108,6 +108,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', @@ -154,7 +158,9 @@ 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.getByText('Evaluation Window.')).toBeInTheDocument(); + expect( + screen.getByTestId('condensed-evaluation-settings-container'), + ).toBeInTheDocument(); }); it('renders query selection dropdown', async () => { @@ -204,11 +210,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); @@ -280,7 +286,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); }); diff --git a/frontend/src/container/CreateAlertV2/AlertCondition/styles.scss b/frontend/src/container/CreateAlertV2/AlertCondition/styles.scss index d8ef5bd295d9..c9ab64447862 100644 --- a/frontend/src/container/CreateAlertV2/AlertCondition/styles.scss +++ b/frontend/src/container/CreateAlertV2/AlertCondition/styles.scss @@ -494,15 +494,21 @@ } } } + } - .add-threshold-btn { - border: 1px dashed var(--bg-vanilla-300); - color: var(--bg-ink-300); + .add-threshold-btn, + .ant-btn.add-threshold-btn { + border: 1px dashed var(--bg-vanilla-300); + color: var(--bg-ink-300); + background-color: transparent; - &:hover { - border-color: var(--bg-ink-300); - color: var(--bg-ink-400); - } + .ant-typography { + color: var(--bg-ink-400); + } + + &:hover { + border-color: var(--bg-ink-300); + color: var(--bg-ink-400); } } } diff --git a/frontend/src/container/CreateAlertV2/CreateAlertHeader/CreateAlertHeader.tsx b/frontend/src/container/CreateAlertV2/CreateAlertHeader/CreateAlertHeader.tsx index becfe20fdd87..e076f23e19f8 100644 --- a/frontend/src/container/CreateAlertV2/CreateAlertHeader/CreateAlertHeader.tsx +++ b/frontend/src/container/CreateAlertV2/CreateAlertHeader/CreateAlertHeader.tsx @@ -1,5 +1,6 @@ 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'; @@ -8,7 +9,7 @@ import { useCreateAlertState } from '../context'; import LabelsInput from './LabelsInput'; function CreateAlertHeader(): JSX.Element { - const { alertState, setAlertState } = useCreateAlertState(); + const { alertState, setAlertState, isEditMode } = useCreateAlertState(); const { currentQuery } = useQueryBuilder(); @@ -34,10 +35,14 @@ function CreateAlertHeader(): JSX.Element { ); return ( -
-
-
New Alert Rule
-
+
+ {!isEditMode && ( +
+
New Alert Rule
+
+ )}
{ const paths = { @@ -37,6 +44,8 @@ jest.mock('react-router-dom', () => ({ }), })); +const ENTER_ALERT_RULE_NAME_PLACEHOLDER = 'Enter alert rule name'; + const renderCreateAlertHeader = (): ReturnType => render( @@ -52,7 +61,9 @@ describe('CreateAlertHeader', () => { it('renders name input with placeholder', () => { renderCreateAlertHeader(); - const nameInput = screen.getByPlaceholderText('Enter alert rule name'); + const nameInput = screen.getByPlaceholderText( + ENTER_ALERT_RULE_NAME_PLACEHOLDER, + ); expect(nameInput).toBeInTheDocument(); }); @@ -63,10 +74,30 @@ describe('CreateAlertHeader', () => { it('updates name when typing in name input', () => { renderCreateAlertHeader(); - const nameInput = screen.getByPlaceholderText('Enter alert rule name'); + const nameInput = screen.getByPlaceholderText( + ENTER_ALERT_RULE_NAME_PLACEHOLDER, + ); fireEvent.change(nameInput, { target: { value: 'Test Alert' } }); expect(nameInput).toHaveValue('Test Alert'); }); + + it('renders the header with title when isEditMode is true', () => { + render( + + + , + ); + expect(screen.queryByText('New Alert Rule')).not.toBeInTheDocument(); + expect( + screen.getByPlaceholderText(ENTER_ALERT_RULE_NAME_PLACEHOLDER), + ).toHaveValue('TEST_ALERT'); + }); }); diff --git a/frontend/src/container/CreateAlertV2/CreateAlertHeader/styles.scss b/frontend/src/container/CreateAlertV2/CreateAlertHeader/styles.scss index e586d98a5016..b1d0511262f3 100644 --- a/frontend/src/container/CreateAlertV2/CreateAlertHeader/styles.scss +++ b/frontend/src/container/CreateAlertV2/CreateAlertHeader/styles.scss @@ -175,10 +175,19 @@ } } + .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); diff --git a/frontend/src/container/CreateAlertV2/CreateAlertV2.tsx b/frontend/src/container/CreateAlertV2/CreateAlertV2.tsx index dc60cedb7c93..e4f87d95e481 100644 --- a/frontend/src/container/CreateAlertV2/CreateAlertV2.tsx +++ b/frontend/src/container/CreateAlertV2/CreateAlertV2.tsx @@ -8,12 +8,11 @@ import AlertCondition from './AlertCondition'; import { CreateAlertProvider } from './context'; import { buildInitialAlertDef } from './context/utils'; import CreateAlertHeader from './CreateAlertHeader'; -import EvaluationSettings from './EvaluationSettings'; import Footer from './Footer'; import NotificationSettings from './NotificationSettings'; import QuerySection from './QuerySection'; import { CreateAlertV2Props } from './types'; -import { showCondensedLayout, Spinner } from './utils'; +import { Spinner } from './utils'; function CreateAlertV2({ alertType }: CreateAlertV2Props): JSX.Element { const queryToRedirect = buildInitialAlertDef(alertType); @@ -23,8 +22,6 @@ function CreateAlertV2({ alertType }: CreateAlertV2Props): JSX.Element { useShareBuilderUrl({ defaultValue: currentQueryToRedirect }); - const showCondensedLayoutFlag = showCondensedLayout(); - return ( @@ -32,7 +29,6 @@ function CreateAlertV2({ alertType }: CreateAlertV2Props): JSX.Element { - {!showCondensedLayoutFlag ? : null}
diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/AdvancedOptionItem/AdvancedOptionItem.tsx b/frontend/src/container/CreateAlertV2/EvaluationSettings/AdvancedOptionItem/AdvancedOptionItem.tsx index 7efb380a388c..89dc56e3b84c 100644 --- a/frontend/src/container/CreateAlertV2/EvaluationSettings/AdvancedOptionItem/AdvancedOptionItem.tsx +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/AdvancedOptionItem/AdvancedOptionItem.tsx @@ -2,7 +2,7 @@ import './styles.scss'; import { Switch, Tooltip, Typography } from 'antd'; import { Info } from 'lucide-react'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { IAdvancedOptionItemProps } from '../types'; @@ -12,9 +12,14 @@ function AdvancedOptionItem({ input, tooltipText, onToggle, + defaultShowInput, }: IAdvancedOptionItemProps): JSX.Element { const [showInput, setShowInput] = useState(false); + useEffect(() => { + setShowInput(defaultShowInput); + }, [defaultShowInput]); + const handleOnToggle = (): void => { onToggle?.(); setShowInput((currentShowInput) => !currentShowInput); @@ -42,7 +47,7 @@ function AdvancedOptionItem({ > {input}
- +
); diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/AdvancedOptions.tsx b/frontend/src/container/CreateAlertV2/EvaluationSettings/AdvancedOptions.tsx index dfa8d033cfc0..a02ff1881ad9 100644 --- a/frontend/src/container/CreateAlertV2/EvaluationSettings/AdvancedOptions.tsx +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/AdvancedOptions.tsx @@ -42,6 +42,7 @@ function AdvancedOptions(): JSX.Element { payload: !advancedOptions.sendNotificationIfDataIsMissing.enabled, }) } + defaultShowInput={advancedOptions.sendNotificationIfDataIsMissing.enabled} /> {/* TODO: Add back when the functionality is implemented */} {/* ); - // Layout consists of only the evaluation window popover - if (showCondensedLayoutFlag) { - return ( -
- {popoverContent} -
- ); - } - - // Layout consists of - // - Stepper header - // - Evaluation window popover - // - Advanced options return ( -
- - {alertType !== AlertTypes.ANOMALY_BASED_ALERT && ( -
- Check conditions using data from -
- {popoverContent} -
- )} - +
+ {popoverContent}
); } diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/AdvancedOptionItem.test.tsx b/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/AdvancedOptionItem.test.tsx index 8facb717dc9b..312c55beeb03 100644 --- a/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/AdvancedOptionItem.test.tsx +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/AdvancedOptionItem.test.tsx @@ -33,6 +33,7 @@ describe('AdvancedOptionItem', () => { title={defaultProps.title} description={defaultProps.description} input={defaultProps.input} + defaultShowInput={false} />, ); @@ -50,6 +51,7 @@ describe('AdvancedOptionItem', () => { title={defaultProps.title} description={defaultProps.description} input={defaultProps.input} + defaultShowInput={false} />, ); @@ -65,6 +67,7 @@ describe('AdvancedOptionItem', () => { title={defaultProps.title} description={defaultProps.description} input={defaultProps.input} + defaultShowInput={false} />, ); @@ -88,6 +91,7 @@ describe('AdvancedOptionItem', () => { title={defaultProps.title} description={defaultProps.description} input={defaultProps.input} + defaultShowInput={false} />, ); @@ -117,6 +121,7 @@ describe('AdvancedOptionItem', () => { title={defaultProps.title} description={defaultProps.description} input={defaultProps.input} + defaultShowInput={false} />, ); @@ -146,6 +151,7 @@ describe('AdvancedOptionItem', () => { title={defaultProps.title} description={defaultProps.description} input={defaultProps.input} + defaultShowInput={false} />, ); @@ -160,9 +166,24 @@ 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( + , + ); + const inputElement = screen.getByTestId(TEST_INPUT_TEST_ID); + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toBeVisible(); + }); }); diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/EvaluationSettings.test.tsx b/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/EvaluationSettings.test.tsx index 741a918404c6..c3b31f6e5ba8 100644 --- a/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/EvaluationSettings.test.tsx +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/EvaluationSettings.test.tsx @@ -1,11 +1,13 @@ 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({ @@ -13,52 +15,14 @@ jest.spyOn(alertState, 'useCreateAlertState').mockReturnValue( }), ); -jest.mock('../AdvancedOptions', () => ({ - __esModule: true, - default: (): JSX.Element => ( -
AdvancedOptions
- ), -})); - -const EVALUATION_SETTINGS_TEXT = 'Evaluation settings'; -const CHECK_CONDITIONS_USING_DATA_FROM_TEXT = - 'Check conditions using data from'; - describe('EvaluationSettings', () => { - it('should render the default evaluation settings layout', () => { - render(); - 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(); - 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(); - // 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(); }); }); diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/testUtils.ts b/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/testUtils.ts index 1486e73e81a2..3055250b61dc 100644 --- a/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/testUtils.ts +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/testUtils.ts @@ -31,6 +31,9 @@ export const createMockAlertContextState = ( isCreatingAlertRule: false, isTestingAlertRule: false, createAlertRule: jest.fn(), + isUpdatingAlertRule: false, + updateAlertRule: jest.fn(), + isEditMode: false, ...overrides, }); diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/types.ts b/frontend/src/container/CreateAlertV2/EvaluationSettings/types.ts index f3f0afadd69c..1372163f6cba 100644 --- a/frontend/src/container/CreateAlertV2/EvaluationSettings/types.ts +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/types.ts @@ -11,6 +11,7 @@ export interface IAdvancedOptionItemProps { input: JSX.Element; tooltipText?: string; onToggle?: () => void; + defaultShowInput: boolean; } export enum RollingWindowTimeframes { diff --git a/frontend/src/container/CreateAlertV2/Footer/Footer.tsx b/frontend/src/container/CreateAlertV2/Footer/Footer.tsx index c4b39531c931..d40cbfd9d638 100644 --- a/frontend/src/container/CreateAlertV2/Footer/Footer.tsx +++ b/frontend/src/container/CreateAlertV2/Footer/Footer.tsx @@ -26,11 +26,17 @@ function Footer(): JSX.Element { isCreatingAlertRule, testAlertRule, isTestingAlertRule, + updateAlertRule, + isUpdatingAlertRule, + isEditMode, } = useCreateAlertState(); const { currentQuery } = useQueryBuilder(); const { safeNavigate } = useSafeNavigate(); - const handleDiscard = (): void => discardAlertRule(); + const handleDiscard = (): void => { + discardAlertRule(); + safeNavigate('/alerts'); + }; const alertValidationMessage = useMemo( () => @@ -99,15 +105,27 @@ function Footer(): JSX.Element { notificationSettings, query: currentQuery, }); - createAlertRule(payload, { - onSuccess: () => { - toast.success('Alert rule created successfully'); - safeNavigate('/alerts'); - }, - onError: (error) => { - toast.error(error.message); - }, - }); + 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, @@ -116,16 +134,22 @@ function Footer(): JSX.Element { evaluationWindow, notificationSettings, currentQuery, + isEditMode, + updateAlertRule, createAlertRule, safeNavigate, ]); const disableButtons = - isCreatingAlertRule || isTestingAlertRule || !!alertValidationMessage; + isCreatingAlertRule || isTestingAlertRule || isUpdatingAlertRule; const saveAlertButton = useMemo(() => { let button = ( - @@ -141,7 +165,7 @@ function Footer(): JSX.Element {
diff --git a/frontend/src/container/CreateAlertV2/Footer/__tests__/Footer.test.tsx b/frontend/src/container/CreateAlertV2/Footer/__tests__/Footer.test.tsx new file mode 100644 index 000000000000..7e4de2c7852f --- /dev/null +++ b/frontend/src/container/CreateAlertV2/Footer/__tests__/Footer.test.tsx @@ -0,0 +1,248 @@ +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(