chore: make new alerting experience as default with the ability to switch to classic (#10040)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled

This commit is contained in:
Amlan Kumar Nandy
2026-01-23 05:15:21 +07:00
committed by GitHub
parent b5901ac174
commit f017b07525
16 changed files with 205 additions and 86 deletions

View File

@@ -52,6 +52,6 @@ export enum QueryParams {
selectedExplorerView = 'selectedExplorerView',
variables = 'variables',
version = 'version',
showNewCreateAlertsPage = 'showNewCreateAlertsPage',
source = 'source',
showClassicCreateAlertsPage = 'showClassicCreateAlertsPage',
}

View File

@@ -123,7 +123,7 @@ describe('Create alert page redirection', () => {
<CreateAlertPage />,
{},
{
initialRoute: `${ROUTES.ALERTS_NEW}?alertType=${alertType}`,
initialRoute: `${ROUTES.ALERTS_NEW}?alertType=${alertType}&showClassicCreateAlertsPage=true`,
},
);

View File

@@ -29,8 +29,17 @@ jest.mock('container/FormAlertRules', () => ({
}));
jest.mock('container/CreateAlertV2', () => ({
__esModule: true,
default: function MockCreateAlertV2(): JSX.Element {
return <div>Create Alert V2</div>;
default: function MockCreateAlertV2({
alertType,
}: {
alertType: AlertTypes;
}): JSX.Element {
return (
<div>
<h1>Create Alert V2</h1>
<p>{alertType}</p>
</div>
);
},
}));
@@ -58,27 +67,27 @@ describe('CreateAlertRule', () => {
useCompositeQueryParamSpy.mockReturnValue(initialQueriesMap.metrics);
});
it('should render v1 flow when showNewCreateAlertsPage is false', () => {
mockGetUrlQuery.mockReturnValue(null);
render(<CreateAlertRule />);
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
});
it('should render v2 flow when showNewCreateAlertsPage is true', () => {
it('should render classic flow when showClassicCreateAlertsPage is true', () => {
mockGetUrlQuery.mockImplementation((key: string) => {
if (key === QueryParams.showNewCreateAlertsPage) {
if (key === QueryParams.showClassicCreateAlertsPage) {
return 'true';
}
return null;
});
render(<CreateAlertRule />);
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
});
it('should render new flow by default', () => {
mockGetUrlQuery.mockReturnValue(null);
render(<CreateAlertRule />);
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
});
it('should render v1 flow when ruleType is anomaly_rule even if showNewCreateAlertsPage is true', () => {
it('should render classic flow when ruleType is anomaly_rule even if showClassicCreateAlertsPage is not true', () => {
mockGetUrlQuery.mockImplementation((key: string) => {
if (key === QueryParams.showNewCreateAlertsPage) {
return 'true';
if (key === QueryParams.showClassicCreateAlertsPage) {
return 'false';
}
if (key === QueryParams.ruleType) {
return 'anomaly_rule';
@@ -98,7 +107,7 @@ describe('CreateAlertRule', () => {
return null;
});
render(<CreateAlertRule />);
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
expect(screen.getByText(AlertTypes.LOGS_BASED_ALERT)).toBeInTheDocument();
});
@@ -117,7 +126,7 @@ describe('CreateAlertRule', () => {
},
});
render(<CreateAlertRule />);
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
expect(screen.getByText(AlertTypes.TRACES_BASED_ALERT)).toBeInTheDocument();
});
@@ -125,7 +134,7 @@ describe('CreateAlertRule', () => {
mockGetUrlQuery.mockReturnValue(null);
useCompositeQueryParamSpy.mockReturnValue(null);
render(<CreateAlertRule />);
expect(screen.getByText(FORM_ALERT_RULES_TEXT)).toBeInTheDocument();
expect(screen.getByText(CREATE_ALERT_V2_TEXT)).toBeInTheDocument();
expect(screen.getByText(AlertTypes.METRICS_BASED_ALERT)).toBeInTheDocument();
});
});

View File

@@ -20,8 +20,8 @@ function CreateRules(): JSX.Element {
const ruleTypeFromURL = queryParams.get(QueryParams.ruleType);
const alertTypeFromURL = queryParams.get(QueryParams.alertType);
const version = queryParams.get(QueryParams.version);
const showNewCreateAlertsPageFlag =
queryParams.get(QueryParams.showNewCreateAlertsPage) === 'true';
const showClassicCreateAlertsPageFlag =
queryParams.get(QueryParams.showClassicCreateAlertsPage) === 'true';
const alertType = useMemo(() => {
if (ruleTypeFromURL === AlertDetectionTypes.ANOMALY_DETECTION_ALERT) {
@@ -45,13 +45,11 @@ function CreateRules(): JSX.Element {
[alertType, version],
);
// Load old alerts flow always for anomaly based alerts and when showClassicCreateAlertsPage is true
if (
showNewCreateAlertsPageFlag &&
alertType !== AlertTypes.ANOMALY_BASED_ALERT
showClassicCreateAlertsPageFlag ||
alertType === AlertTypes.ANOMALY_BASED_ALERT
) {
return <CreateAlertV2 alertType={alertType} />;
}
return (
<FormAlertRules
alertType={alertType}
@@ -62,4 +60,7 @@ function CreateRules(): JSX.Element {
);
}
return <CreateAlertV2 alertType={alertType} />;
}
export default CreateRules;

View File

@@ -1,7 +1,14 @@
import './styles.scss';
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import classNames from 'classnames';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { RotateCcw } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import { Labels } from 'types/api/alerts/def';
@@ -12,6 +19,8 @@ function CreateAlertHeader(): JSX.Element {
const { alertState, setAlertState, isEditMode } = useCreateAlertState();
const { currentQuery } = useQueryBuilder();
const { safeNavigate } = useSafeNavigate();
const urlQuery = useUrlQuery();
const groupByLabels = useMemo(() => {
const labels = new Array<string>();
@@ -34,6 +43,14 @@ function CreateAlertHeader(): JSX.Element {
[groupByLabels],
);
const handleSwitchToClassicExperience = useCallback(() => {
logEvent('Alert: Switch to classic experience button clicked', {});
urlQuery.set(QueryParams.showClassicCreateAlertsPage, 'true');
const url = `${ROUTES.ALERTS_NEW}?${urlQuery.toString()}`;
safeNavigate(url, { replace: true });
}, [safeNavigate, urlQuery]);
return (
<div
className={classNames('alert-header', { 'edit-alert-header': isEditMode })}
@@ -41,6 +58,12 @@ function CreateAlertHeader(): JSX.Element {
{!isEditMode && (
<div className="alert-header__tab-bar">
<div className="alert-header__tab">New Alert Rule</div>
<Button
icon={<RotateCcw size={16} />}
onClick={handleSwitchToClassicExperience}
>
Switch to Classic Experience
</Button>
</div>
)}
<div className="alert-header__content">

View File

@@ -1,7 +1,11 @@
/* eslint-disable react/jsx-props-no-spreading */
import { fireEvent, render, screen } from '@testing-library/react';
import { QueryParams } from 'constants/query';
import { initialQueriesMap } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { defaultPostableAlertRuleV2 } from 'container/CreateAlertV2/constants';
import { getCreateAlertLocalStateFromAlertDef } from 'container/CreateAlertV2/utils';
import * as useSafeNavigateHook from 'hooks/useSafeNavigate';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import * as useCreateAlertRuleHook from '../../../../hooks/alerts/useCreateAlertRule';
@@ -10,6 +14,11 @@ import * as useUpdateAlertRuleHook from '../../../../hooks/alerts/useUpdateAlert
import { CreateAlertProvider } from '../../context';
import CreateAlertHeader from '../CreateAlertHeader';
const mockSafeNavigate = jest.fn();
jest.spyOn(useSafeNavigateHook, 'useSafeNavigate').mockReturnValue({
safeNavigate: mockSafeNavigate,
});
jest.spyOn(useCreateAlertRuleHook, 'useCreateAlertRule').mockReturnValue({
mutate: jest.fn(),
isLoading: false,
@@ -100,4 +109,37 @@ describe('CreateAlertHeader', () => {
screen.getByPlaceholderText(ENTER_ALERT_RULE_NAME_PLACEHOLDER),
).toHaveValue('TEST_ALERT');
});
it('should navigate to classic experience when button is clicked', () => {
renderCreateAlertHeader();
const switchToClassicExperienceButton = screen.getByText(
'Switch to Classic Experience',
);
expect(switchToClassicExperienceButton).toBeInTheDocument();
fireEvent.click(switchToClassicExperienceButton);
const params = new URLSearchParams();
params.set(QueryParams.showClassicCreateAlertsPage, 'true');
expect(mockSafeNavigate).toHaveBeenCalledWith(
`${ROUTES.ALERTS_NEW}?${params.toString()}`,
{ replace: true },
);
});
it('should not render "switch to classic experience" button when isEditMode is true', () => {
render(
<CreateAlertProvider
isEditMode
initialAlertType={AlertTypes.METRICS_BASED_ALERT}
initialAlertState={getCreateAlertLocalStateFromAlertDef(
defaultPostableAlertRuleV2,
)}
>
<CreateAlertHeader />
</CreateAlertProvider>,
);
expect(
screen.queryByText('Switch to Classic Experience'),
).not.toBeInTheDocument();
});
});

View File

@@ -3,6 +3,12 @@
font-family: inherit;
color: var(--text-vanilla-100);
&__tab-bar {
display: flex;
align-items: center;
justify-content: space-between;
}
/* Tab block visuals */
&__tab {
display: flex;

View File

@@ -6,7 +6,7 @@ import ChartPreviewComponent from 'container/FormAlertRules/ChartPreview';
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useGetYAxisUnit from 'hooks/useGetYAxisUnit';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { AlertTypes } from 'types/api/alerts/alertTypes';
@@ -16,9 +16,10 @@ import { GlobalReducer } from 'types/reducer/globalTime';
export interface ChartPreviewProps {
alertDef: AlertDef;
source?: YAxisSource;
}
function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
function ChartPreview({ alertDef, source }: ChartPreviewProps): JSX.Element {
const { currentQuery, panelType, stagedQuery } = useQueryBuilder();
const {
alertType,
@@ -35,8 +36,14 @@ function ChartPreview({ alertDef }: ChartPreviewProps): JSX.Element {
const yAxisUnit = alertState.yAxisUnit || '';
const shouldUpdateYAxisUnit =
!isEditMode && alertType === AlertTypes.METRICS_BASED_ALERT;
// Only update automatically when creating a new metrics-based alert rule
const shouldUpdateYAxisUnit = useMemo(() => {
// Do not update if we are coming to the page from dashboards (we still show warning)
if (source === YAxisSource.DASHBOARDS) {
return false;
}
return !isEditMode && alertType === AlertTypes.METRICS_BASED_ALERT;
}, [isEditMode, alertType, source]);
const selectedQueryName = thresholdState.selectedQuery;
const { yAxisUnit: initialYAxisUnit, isLoading } = useGetYAxisUnit(

View File

@@ -2,10 +2,15 @@ import './styles.scss';
import { Button } from 'antd';
import classNames from 'classnames';
import { YAxisSource } from 'components/YAxisUnitSelector/types';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import QuerySectionComponent from 'container/FormAlertRules/QuerySection';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getMetricNameFromQueryData } from 'hooks/useGetYAxisUnit';
import useUrlQuery from 'hooks/useUrlQuery';
import { BarChart2, DraftingCompass, FileText, ScrollText } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
@@ -18,10 +23,12 @@ import { buildAlertDefForChartPreview } from './utils';
function QuerySection(): JSX.Element {
const {
currentQuery,
stagedQuery,
handleRunQuery,
redirectWithQueryBuilderData,
} = useQueryBuilder();
const { alertType, setAlertType, thresholdState } = useCreateAlertState();
const urlQuery = useUrlQuery();
const alertDef = buildAlertDefForChartPreview({ alertType, thresholdState });
@@ -30,6 +37,49 @@ function QuerySection(): JSX.Element {
redirectWithQueryBuilderData(query);
};
const source = useMemo(() => urlQuery.get(QueryParams.source) as YAxisSource, [
urlQuery,
]);
const didQueryChange = useMemo(() => {
if (alertType !== AlertTypes.METRICS_BASED_ALERT) {
return false;
}
const selectedQueryName = thresholdState.selectedQuery;
const currentQueryData = currentQuery.builder.queryData.find(
(query) => query.queryName === selectedQueryName,
);
const stagedQueryData = stagedQuery?.builder.queryData.find(
(query) => query.queryName === selectedQueryName,
);
if (!currentQueryData || !stagedQueryData) {
return false;
}
const currentQueryKey = getMetricNameFromQueryData(currentQueryData);
const stagedQueryKey = getMetricNameFromQueryData(stagedQueryData);
return currentQueryKey !== stagedQueryKey;
}, [currentQuery, alertType, thresholdState, stagedQuery]);
const runQueryHandler = useCallback(() => {
// Reset the source param when the query is changed
// Then manually run the query
if (source === YAxisSource.DASHBOARDS && didQueryChange) {
redirectWithQueryBuilderData(currentQuery, {
[QueryParams.source]: null,
});
} else {
handleRunQuery();
}
}, [
currentQuery,
didQueryChange,
handleRunQuery,
redirectWithQueryBuilderData,
source,
]);
const tabs = [
{
label: 'Metrics',
@@ -56,7 +106,7 @@ function QuerySection(): JSX.Element {
return (
<div className="query-section">
<Stepper stepNumber={1} label="Define the query" />
<ChartPreview alertDef={alertDef} />
<ChartPreview alertDef={alertDef} source={source} />
<div className="query-section-tabs">
<div className="query-section-query-actions">
{tabs.map((tab) => (
@@ -79,7 +129,7 @@ function QuerySection(): JSX.Element {
queryCategory={currentQuery.queryType}
setQueryCategory={onQueryCategoryChange}
alertType={alertType}
runQuery={handleRunQuery}
runQuery={runQueryHandler}
alertDef={alertDef}
panelType={PANEL_TYPES.TIME_SERIES}
key={currentQuery.queryType}

View File

@@ -63,12 +63,13 @@ export function CreateAlertProvider(
initialAlertType,
} = props;
const [alertState, setAlertState] = useReducer(
alertCreationReducer,
INITIAL_ALERT_STATE,
);
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const [alertState, setAlertState] = useReducer(alertCreationReducer, {
...INITIAL_ALERT_STATE,
yAxisUnit: currentQuery.unit,
});
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
const thresholdsFromURL = queryParams.get(QueryParams.thresholds);

View File

@@ -762,7 +762,7 @@ function MultiIngestionSettings(): JSX.Element {
const thresholds = cloneDeep(INITIAL_ALERT_THRESHOLD_STATE.thresholds);
thresholds[0].thresholdValue = threshold;
const URL = `${ROUTES.ALERTS_NEW}?showNewCreateAlertsPage=true&${
const URL = `${ROUTES.ALERTS_NEW}?${
QueryParams.compositeQuery
}=${encodeURIComponent(stringifiedQuery)}&${
QueryParams.thresholds

View File

@@ -142,7 +142,6 @@ describe('MultiIngestionSettings Page', () => {
// Check URL contains alerts/new route
expect(navigationCall).toContain('/alerts/new');
expect(navigationCall).toContain('showNewCreateAlertsPage=true');
// Parse query parameters
const urlParams = new URLSearchParams(navigationCall.split('?')[1]);
@@ -231,7 +230,6 @@ describe('MultiIngestionSettings Page', () => {
// Check URL contains alerts/new route
expect(navigationCall).toContain('/alerts/new');
expect(navigationCall).toContain('showNewCreateAlertsPage=true');
// Parse query parameters
const urlParams = new URLSearchParams(navigationCall.split('?')[1]);

View File

@@ -108,42 +108,15 @@ 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',
});
params.set(QueryParams.showNewCreateAlertsPage, 'true');
safeNavigate(`${ROUTES.ALERT_TYPE_SELECTION}?${params.toString()}`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onClickNewClassicAlertHandler = useCallback(() => {
logEvent('Alert: New alert button clicked', {
number: allAlertRules?.length,
layout: 'classic',
});
safeNavigate(ROUTES.ALERT_TYPE_SELECTION);
// 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 = sanitizeDefaultAlertQuery(
mapQueryDataFromApi(record.condition.compositeQuery),
@@ -414,11 +387,13 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
/>
<Flex gap={12}>
{addNewAlert && (
<Dropdown menu={{ items: newAlertMenuItems }} trigger={['click']}>
<Button type="primary" icon={<PlusOutlined />}>
<Button
type="primary"
onClick={onClickNewAlertHandler}
icon={<PlusOutlined />}
>
New Alert
</Button>
</Dropdown>
)}
<TextToolTip
{...{

View File

@@ -16,7 +16,9 @@ interface UseGetYAxisUnitResult {
isError: boolean;
}
function getMetricNameFromQueryData(queryData: IBuilderQuery): string | null {
export function getMetricNameFromQueryData(
queryData: IBuilderQuery,
): string | null {
if (queryData.dataSource !== DataSource.METRICS) {
return null;
}

View File

@@ -34,11 +34,11 @@ function AlertTypeSelectionPage(): JSX.Element {
queryParams.set(QueryParams.alertType, type);
}
const showNewCreateAlertsPageFlag = queryParams.get(
QueryParams.showNewCreateAlertsPage,
const showClassicCreateAlertsPageFlag = queryParams.get(
QueryParams.showClassicCreateAlertsPage,
);
if (showNewCreateAlertsPageFlag === 'true') {
queryParams.set(QueryParams.showNewCreateAlertsPage, 'true');
if (showClassicCreateAlertsPageFlag === 'true') {
queryParams.set(QueryParams.showClassicCreateAlertsPage, 'true');
}
safeNavigate(`${ROUTES.ALERTS_NEW}?${queryParams.toString()}`);

View File

@@ -157,11 +157,16 @@ describe('AlertTypeSelection', () => {
expect(mockSafeNavigate).toHaveBeenCalled();
});
it('should navigate to new create alerts page with correct params if showNewCreateAlertsPage is true', () => {
it('should navigate to classic create alerts page with correct params if showClassicCreateAlertsPage is true', () => {
useUrlQuerySpy.mockReturnValue(({
set: mockSetUrlQuery,
toString: mockToString,
get: mockGetUrlQuery.mockReturnValue('true'),
get: mockGetUrlQuery.mockImplementation((key: string) => {
if (key === QueryParams.showClassicCreateAlertsPage) {
return 'true';
}
return null;
}),
} as Partial<URLSearchParams>) as URLSearchParams);
render(<AlertTypeSelection />);
@@ -176,7 +181,7 @@ describe('AlertTypeSelection', () => {
AlertTypes.METRICS_BASED_ALERT,
);
expect(mockSetUrlQuery).toHaveBeenCalledWith(
QueryParams.showNewCreateAlertsPage,
QueryParams.showClassicCreateAlertsPage,
'true',
);
expect(mockSafeNavigate).toHaveBeenCalled();