From 73ff89a80a8bd57f3caebcc58fd9ce50fbe2e79c Mon Sep 17 00:00:00 2001 From: manika-signoz Date: Tue, 23 Sep 2025 20:47:39 +0530 Subject: [PATCH 1/7] feat: revamp onboarding (#9068) * feat: revamp onboarding, send list to mixpanel, join logic to convert to single string * chore: props changes * fix: allow user to proceed even if api fails * chore: remove console.log * chore: remove commented code * chore: minor colour tweaks * chore: resolve comments --- .../AboutSigNozQuestions.tsx | 100 ++++++----- .../OnboardingQuestionaire.styles.scss | 51 ++++++ .../OrgQuestions/OrgQuestions.tsx | 166 ++++++------------ .../OnboardingQuestionaire/index.tsx | 19 +- frontend/src/types/api/onboarding/types.ts | 2 +- 5 files changed, 177 insertions(+), 161 deletions(-) diff --git a/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx b/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx index 2124e995150f..913da1ec351d 100644 --- a/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx +++ b/frontend/src/container/OnboardingQuestionaire/AboutSigNozQuestions/AboutSigNozQuestions.tsx @@ -2,14 +2,14 @@ import '../OnboardingQuestionaire.styles.scss'; import { Color } from '@signozhq/design-tokens'; -import { Button, Input, Typography } from 'antd'; +import { Button, Checkbox, Input, Typography } from 'antd'; import TextArea from 'antd/lib/input/TextArea'; import logEvent from 'api/common/logEvent'; import { ArrowLeft, ArrowRight, CheckCircle } from 'lucide-react'; import { useEffect, useState } from 'react'; export interface SignozDetails { - interestInSignoz: string | null; + interestInSignoz: string[] | null; otherInterestInSignoz: string | null; discoverSignoz: string | null; } @@ -22,9 +22,12 @@ interface AboutSigNozQuestionsProps { } const interestedInOptions: Record = { - savingCosts: 'Saving costs', - otelNativeStack: 'Interested in Otel-native stack', - allInOne: 'All in one (Logs, Metrics & Traces)', + loweringCosts: 'Lowering observability costs', + otelNativeStack: 'Interested in OTel-native stack', + deploymentFlexibility: 'Deployment flexibility (Cloud/Self-Host) in future', + singleTool: + 'Single Tool (logs, metrics & traces) to reduce operational overhead', + correlateSignals: 'Correlate signals for faster troubleshooting', }; export function AboutSigNozQuestions({ @@ -33,8 +36,8 @@ export function AboutSigNozQuestions({ onNext, onBack, }: AboutSigNozQuestionsProps): JSX.Element { - const [interestInSignoz, setInterestInSignoz] = useState( - signozDetails?.interestInSignoz || null, + const [interestInSignoz, setInterestInSignoz] = useState( + signozDetails?.interestInSignoz || [], ); const [otherInterestInSignoz, setOtherInterestInSignoz] = useState( signozDetails?.otherInterestInSignoz || '', @@ -47,8 +50,8 @@ export function AboutSigNozQuestions({ useEffect((): void => { if ( discoverSignoz !== '' && - interestInSignoz !== null && - (interestInSignoz !== 'Others' || otherInterestInSignoz !== '') + interestInSignoz.length > 0 && + (!interestInSignoz.includes('Others') || otherInterestInSignoz !== '') ) { setIsNextDisabled(false); } else { @@ -56,6 +59,14 @@ export function AboutSigNozQuestions({ } }, [interestInSignoz, otherInterestInSignoz, discoverSignoz]); + const handleInterestChange = (option: string, checked: boolean): void => { + if (checked) { + setInterestInSignoz((prev) => [...prev, option]); + } else { + setInterestInSignoz((prev) => prev.filter((item) => item !== option)); + } + }; + const handleOnNext = (): void => { setSignozDetails({ discoverSignoz, @@ -108,50 +119,45 @@ export function AboutSigNozQuestions({
What got you interested in SigNoz?
-
+
{Object.keys(interestedInOptions).map((option: string) => ( - +
+ handleInterestChange(option, e.target.checked)} + > + {interestedInOptions[option]} + +
))} - {interestInSignoz === 'Others' ? ( - - ) : ( - '' - ) +
+ + handleInterestChange('Others', e.target.checked) } - onChange={(e): void => setOtherInterestInSignoz(e.target.value)} - /> - ) : ( - - )} + + {interestInSignoz.includes('Others') && ( + + ) : ( + '' + ) + } + onChange={(e): void => setOtherInterestInSignoz(e.target.value)} + /> + )} +
diff --git a/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss b/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss index 70f68070f862..e266e450dbca 100644 --- a/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss +++ b/frontend/src/container/OnboardingQuestionaire/OnboardingQuestionaire.styles.scss @@ -94,6 +94,7 @@ border-radius: 4px; font-size: 14px; padding: 12px; + font-weight: 400; &::placeholder { color: var(--bg-vanilla-400); @@ -290,6 +291,37 @@ gap: 10px; } + .checkbox-grid { + display: flex; + flex-direction: column; + gap: 12px; + margin-top: 12px; + } + + .checkbox-item { + display: flex; + flex-direction: column; + gap: 8px; + + .ant-checkbox-wrapper { + color: var(--bg-vanilla-400); + font-size: 14px; + font-weight: 400; + + .ant-checkbox { + .ant-checkbox-inner { + border-color: var(--bg-slate-100); + background-color: var(--bg-ink-200); + } + + &.ant-checkbox-checked .ant-checkbox-inner { + background-color: var(--bg-robin-500); + border-color: var(--bg-robin-500); + } + } + } + } + .onboarding-questionaire-button, .add-another-member-button, .remove-team-member-button { @@ -466,6 +498,7 @@ border: 1px solid var(--bg-vanilla-300); background: var(--bg-vanilla-100); color: var(--text-ink-300); + font-weight: 400; &::placeholder { color: var(--bg-slate-400); @@ -527,6 +560,24 @@ color: var(--bg-slate-300); } + .checkbox-item { + .ant-checkbox-wrapper { + color: var(--bg-ink-300); + + .ant-checkbox { + .ant-checkbox-inner { + border-color: var(--bg-vanilla-300); + background-color: var(--bg-vanilla-100); + } + + &.ant-checkbox-checked .ant-checkbox-inner { + background-color: var(--bg-robin-500); + border-color: var(--bg-robin-500); + } + } + } + } + input[type='text'] { border: 1px solid var(--bg-vanilla-300); background: var(--bg-vanilla-100); diff --git a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx index 71c8aa2f2e82..6cd6e224d156 100644 --- a/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx +++ b/frontend/src/container/OnboardingQuestionaire/OrgQuestions/OrgQuestions.tsx @@ -38,6 +38,7 @@ const observabilityTools = { AzureAppMonitor: 'Azure App Monitor', GCPNativeO11yTools: 'GCP-native o11y tools', Honeycomb: 'Honeycomb', + None: 'None/Starting fresh', }; function OrgQuestions({ @@ -53,9 +54,6 @@ function OrgQuestions({ const [organisationName, setOrganisationName] = useState( orgDetails?.organisationName || '', ); - const [usesObservability, setUsesObservability] = useState( - orgDetails?.usesObservability || null, - ); const [observabilityTool, setObservabilityTool] = useState( orgDetails?.observabilityTool || null, ); @@ -83,7 +81,7 @@ function OrgQuestions({ orgDetails.organisationName === organisationName ) { logEvent('Org Onboarding: Answered', { - usesObservability, + usesObservability: !observabilityTool?.includes('None'), observabilityTool, otherTool, usesOtel, @@ -91,7 +89,7 @@ function OrgQuestions({ onNext({ organisationName, - usesObservability, + usesObservability: !observabilityTool?.includes('None'), observabilityTool, otherTool, usesOtel, @@ -114,7 +112,7 @@ function OrgQuestions({ }); logEvent('Org Onboarding: Answered', { - usesObservability, + usesObservability: !observabilityTool?.includes('None'), observabilityTool, otherTool, usesOtel, @@ -122,7 +120,7 @@ function OrgQuestions({ onNext({ organisationName, - usesObservability, + usesObservability: !observabilityTool?.includes('None'), observabilityTool, otherTool, usesOtel, @@ -152,16 +150,16 @@ function OrgQuestions({ }; const isValidUsesObservability = (): boolean => { - if (usesObservability === null) { - return false; - } - - if (usesObservability && (!observabilityTool || observabilityTool === '')) { + if (!observabilityTool || observabilityTool === '') { return false; } // eslint-disable-next-line sonarjs/prefer-single-boolean-return - if (usesObservability && observabilityTool === 'Others' && otherTool === '') { + if ( + !observabilityTool?.includes('None') && + observabilityTool === 'Others' && + otherTool === '' + ) { return false; } @@ -177,13 +175,7 @@ function OrgQuestions({ setIsNextDisabled(true); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - organisationName, - usesObservability, - usesOtel, - observabilityTool, - otherTool, - ]); + }, [organisationName, usesOtel, observabilityTool, otherTool]); const handleOnNext = (): void => { handleOrgNameUpdate(); @@ -217,99 +209,57 @@ function OrgQuestions({
-
- {usesObservability && ( -
- -
- {Object.keys(observabilityTools).map((tool) => ( - - ))} - - {observabilityTool === 'Others' ? ( - - ) : ( - '' - ) - } - onChange={(e): void => setOtherTool(e.target.value)} - /> - ) : ( - - )} -
-
- )} -
Do you already use OpenTelemetry?
diff --git a/frontend/src/container/OnboardingQuestionaire/index.tsx b/frontend/src/container/OnboardingQuestionaire/index.tsx index 57832e28c2c5..435902511f8d 100644 --- a/frontend/src/container/OnboardingQuestionaire/index.tsx +++ b/frontend/src/container/OnboardingQuestionaire/index.tsx @@ -46,7 +46,7 @@ const INITIAL_ORG_DETAILS: OrgDetails = { }; const INITIAL_SIGNOZ_DETAILS: SignozDetails = { - interestInSignoz: '', + interestInSignoz: [], otherInterestInSignoz: '', discoverSignoz: '', }; @@ -145,6 +145,9 @@ function OnboardingQuestionaire(): JSX.Element { }, onError: (error) => { showErrorNotification(notifications, error as AxiosError); + + // Allow user to proceed even if API fails + setCurrentStep(4); }, }, ); @@ -174,10 +177,16 @@ function OnboardingQuestionaire(): JSX.Element { ? (orgDetails?.otherTool as string) : (orgDetails?.observabilityTool as string), where_did_you_discover_signoz: signozDetails?.discoverSignoz as string, - reasons_for_interest_in_signoz: - signozDetails?.interestInSignoz === 'Others' - ? (signozDetails?.otherInterestInSignoz as string) - : (signozDetails?.interestInSignoz as string), + reasons_for_interest_in_signoz: signozDetails?.interestInSignoz?.includes( + 'Others', + ) + ? ([ + ...(signozDetails?.interestInSignoz?.filter( + (item) => item !== 'Others', + ) || []), + signozDetails?.otherInterestInSignoz, + ] as string[]) + : (signozDetails?.interestInSignoz as string[]), logs_scale_per_day_in_gb: optimiseSignozDetails?.logsPerDay as number, number_of_hosts: optimiseSignozDetails?.hostsPerDay as number, number_of_services: optimiseSignozDetails?.services as number, diff --git a/frontend/src/types/api/onboarding/types.ts b/frontend/src/types/api/onboarding/types.ts index 9827f7fa415a..f33b8d679b2e 100644 --- a/frontend/src/types/api/onboarding/types.ts +++ b/frontend/src/types/api/onboarding/types.ts @@ -1,5 +1,5 @@ export interface UpdateProfileProps { - reasons_for_interest_in_signoz: string; + reasons_for_interest_in_signoz: string[]; uses_otel: boolean; has_existing_observability_tool: boolean; existing_observability_tool: string; From 2c59c1196d15ccd6693cbf897dd80e4af518495a Mon Sep 17 00:00:00 2001 From: Amlan Kumar Nandy <45410599+amlannandy@users.noreply.github.com> Date: Tue, 23 Sep 2025 22:36:40 +0700 Subject: [PATCH 2/7] chore: add evaluation settings section (#9134) --- .../AlertCondition/AlertCondition.tsx | 8 + .../AlertCondition/AlertThreshold.tsx | 18 +- .../CreateAlertV2/AlertCondition/styles.scss | 43 +++ .../container/CreateAlertV2/CreateAlertV2.tsx | 13 +- .../EvaluationSettings/AdvancedOptions.tsx | 129 ++++++++ .../EvaluationSettings/EvaluationSettings.tsx | 91 ++++++ .../EvaluationWindowDetails.tsx | 221 +++++++++++++ .../EvaluationWindowPopover.tsx | 161 ++++++++++ .../EvaluationWindowPopover/index.ts | 3 + .../useKeyboardNavigation.ts | 180 +++++++++++ .../__tests__/AdvancedOptions.test.tsx | 141 +++++++++ .../__tests__/EvaluationSettings.test.tsx | 64 ++++ .../EvaluationWindowDetails.test.tsx | 200 ++++++++++++ .../EvaluationWindowPopover.test.tsx | 298 ++++++++++++++++++ .../EvaluationSettings/__tests__/testUtils.ts | 12 +- .../EvaluationSettings/constants.ts | 7 + .../CreateAlertV2/EvaluationSettings/index.ts | 3 + .../EvaluationSettings/styles.scss | 2 +- .../CreateAlertV2/EvaluationSettings/types.ts | 2 - .../src/container/CreateAlertV2/utils.tsx | 6 + 20 files changed, 1592 insertions(+), 10 deletions(-) create mode 100644 frontend/src/container/CreateAlertV2/EvaluationSettings/AdvancedOptions.tsx create mode 100644 frontend/src/container/CreateAlertV2/EvaluationSettings/EvaluationSettings.tsx create mode 100644 frontend/src/container/CreateAlertV2/EvaluationSettings/EvaluationWindowPopover/EvaluationWindowDetails.tsx create mode 100644 frontend/src/container/CreateAlertV2/EvaluationSettings/EvaluationWindowPopover/EvaluationWindowPopover.tsx create mode 100644 frontend/src/container/CreateAlertV2/EvaluationSettings/EvaluationWindowPopover/index.ts create mode 100644 frontend/src/container/CreateAlertV2/EvaluationSettings/EvaluationWindowPopover/useKeyboardNavigation.ts create mode 100644 frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/AdvancedOptions.test.tsx create mode 100644 frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/EvaluationSettings.test.tsx create mode 100644 frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/EvaluationWindowDetails.test.tsx create mode 100644 frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/EvaluationWindowPopover.test.tsx create mode 100644 frontend/src/container/CreateAlertV2/EvaluationSettings/index.ts diff --git a/frontend/src/container/CreateAlertV2/AlertCondition/AlertCondition.tsx b/frontend/src/container/CreateAlertV2/AlertCondition/AlertCondition.tsx index e45a44d2d2b4..a0dd357cd01c 100644 --- a/frontend/src/container/CreateAlertV2/AlertCondition/AlertCondition.tsx +++ b/frontend/src/container/CreateAlertV2/AlertCondition/AlertCondition.tsx @@ -6,13 +6,16 @@ import { Activity, ChartLine } from 'lucide-react'; import { AlertTypes } from 'types/api/alerts/alertTypes'; 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 showMultipleTabs = alertType === AlertTypes.ANOMALY_BASED_ALERT || @@ -75,6 +78,11 @@ function AlertCondition(): JSX.Element {
{alertType !== AlertTypes.ANOMALY_BASED_ALERT && } {alertType === AlertTypes.ANOMALY_BASED_ALERT && } + {showCondensedLayoutFlag ? ( +
+ +
+ ) : null}
); } diff --git a/frontend/src/container/CreateAlertV2/AlertCondition/AlertThreshold.tsx b/frontend/src/container/CreateAlertV2/AlertCondition/AlertThreshold.tsx index 1610602daf2f..ca1934a774a6 100644 --- a/frontend/src/container/CreateAlertV2/AlertCondition/AlertThreshold.tsx +++ b/frontend/src/container/CreateAlertV2/AlertCondition/AlertThreshold.tsx @@ -2,6 +2,7 @@ import './styles.scss'; import { Button, Select, Typography } from 'antd'; import getAllChannels from 'api/channels/getAll'; +import classNames from 'classnames'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { Plus } from 'lucide-react'; import { useQuery } from 'react-query'; @@ -17,6 +18,8 @@ import { THRESHOLD_MATCH_TYPE_OPTIONS, THRESHOLD_OPERATOR_OPTIONS, } from '../context/constants'; +import EvaluationSettings from '../EvaluationSettings/EvaluationSettings'; +import { showCondensedLayout } from '../utils'; import ThresholdItem from './ThresholdItem'; import { UpdateThreshold } from './types'; import { @@ -37,6 +40,7 @@ function AlertThreshold(): JSX.Element { >(['getChannels'], { queryFn: () => getAllChannels(), }); + const showCondensedLayoutFlag = showCondensedLayout(); const channels = data?.data || []; const { currentQuery } = useQueryBuilder(); @@ -81,8 +85,18 @@ function AlertThreshold(): JSX.Element { }); }; + const evaluationWindowContext = showCondensedLayoutFlag ? ( + + ) : ( + Evaluation Window. + ); + return ( -
+
{/* Main condition sentence */}
@@ -128,7 +142,7 @@ function AlertThreshold(): JSX.Element { options={THRESHOLD_MATCH_TYPE_OPTIONS} /> - during the Evaluation Window. + during the {evaluationWindowContext}
diff --git a/frontend/src/container/CreateAlertV2/AlertCondition/styles.scss b/frontend/src/container/CreateAlertV2/AlertCondition/styles.scss index bdde72598d6a..8e9fc6e223cc 100644 --- a/frontend/src/container/CreateAlertV2/AlertCondition/styles.scss +++ b/frontend/src/container/CreateAlertV2/AlertCondition/styles.scss @@ -84,6 +84,9 @@ color: var(--text-vanilla-400); font-size: 14px; line-height: 1.5; + display: flex; + align-items: center; + gap: 8px; } .ant-select { @@ -275,3 +278,43 @@ } } } + +.condensed-alert-threshold-container, +.condensed-anomaly-threshold-container { + width: 100%; +} + +.condensed-advanced-options-container { + margin-top: 16px; + width: fit-parent; +} + +.condensed-evaluation-settings-container { + .ant-btn { + display: flex; + align-items: center; + width: 240px; + justify-content: space-between; + background-color: var(--bg-ink-300); + border: 1px solid var(--bg-slate-400); + + .evaluate-alert-conditions-button-left { + color: var(--bg-vanilla-400); + font-size: 12px; + } + + .evaluate-alert-conditions-button-right { + display: flex; + align-items: center; + color: var(--bg-vanilla-400); + gap: 8px; + + .evaluate-alert-conditions-button-right-text { + font-size: 12px; + font-weight: 500; + background-color: var(--bg-slate-400); + padding: 1px 4px; + } + } + } +} diff --git a/frontend/src/container/CreateAlertV2/CreateAlertV2.tsx b/frontend/src/container/CreateAlertV2/CreateAlertV2.tsx index 589a18fc6ca1..185241254d3b 100644 --- a/frontend/src/container/CreateAlertV2/CreateAlertV2.tsx +++ b/frontend/src/container/CreateAlertV2/CreateAlertV2.tsx @@ -7,7 +7,9 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData'; import AlertCondition from './AlertCondition'; import { CreateAlertProvider } from './context'; import CreateAlertHeader from './CreateAlertHeader'; +import EvaluationSettings from './EvaluationSettings'; import QuerySection from './QuerySection'; +import { showCondensedLayout } from './utils'; function CreateAlertV2({ initialQuery = initialQueriesMap.metrics, @@ -16,14 +18,17 @@ function CreateAlertV2({ }): JSX.Element { useShareBuilderUrl({ defaultValue: initialQuery }); + const showCondensedLayoutFlag = showCondensedLayout(); + return ( -
- + +
- -
+ {!showCondensedLayoutFlag ? : null} +
+ ); } diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/AdvancedOptions.tsx b/frontend/src/container/CreateAlertV2/EvaluationSettings/AdvancedOptions.tsx new file mode 100644 index 000000000000..a560c449e8aa --- /dev/null +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/AdvancedOptions.tsx @@ -0,0 +1,129 @@ +import { Collapse, Input, Select, Typography } from 'antd'; +import { Y_AXIS_CATEGORIES } from 'components/YAxisUnitSelector/constants'; + +import { useCreateAlertState } from '../context'; +import AdvancedOptionItem from './AdvancedOptionItem'; +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 ( +
+ + + + + + setAdvancedOptions({ + type: 'SET_SEND_NOTIFICATION_IF_DATA_IS_MISSING', + payload: { + toleranceLimit: Number(e.target.value), + timeUnit: advancedOptions.sendNotificationIfDataIsMissing.timeUnit, + }, + }) + } + value={advancedOptions.sendNotificationIfDataIsMissing.toleranceLimit} + /> + + setAdvancedOptions({ + type: 'SET_ENFORCE_MINIMUM_DATAPOINTS', + payload: { + minimumDatapoints: Number(e.target.value), + }, + }) + } + value={advancedOptions.enforceMinimumDatapoints.minimumDatapoints} + /> + Datapoints +
+ } + /> + + + setAdvancedOptions({ + type: 'SET_DELAY_EVALUATION', + payload: { + delay: Number(e.target.value), + timeUnit: advancedOptions.delayEvaluation.timeUnit, + }, + }) + } + value={advancedOptions.delayEvaluation.delay} + /> + +
+
+ ); + } + + if (isCurrentDay) { + return ( +
+ {CUMULATIVE_WINDOW_DESCRIPTION} + {displayText} +
+ STARTING AT + +
+
+ SELECT TIMEZONE + +
+
+ STARTING AT + +
+
+ SELECT TIMEZONE + handleNumberChange(e.target.value)} + placeholder="Enter value" + /> +
+
+ UNIT + +
+ ), +})); + +describe('EvaluationWindowPopover', () => { + it('should render the evaluation window popover with 3 sections', () => { + render( + , + ); + expect(screen.getByText(EVALUATION_WINDOW_TEXT)).toBeInTheDocument(); + }); + + it('should render all window type options with rolling selected', () => { + render( + , + ); + EVALUATION_WINDOW_TYPE.forEach((option) => { + expect(screen.getByText(option.label)).toBeInTheDocument(); + }); + const rollingItem = screen + .getByText('Rolling') + .closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement; + expect(rollingItem).toHaveClass('active'); + + const cumulativeItem = screen + .getByText('Cumulative') + .closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement; + expect(cumulativeItem).not.toHaveClass('active'); + }); + + it('should render all window type options with cumulative selected', () => { + render( + , + ); + EVALUATION_WINDOW_TYPE.forEach((option) => { + expect(screen.getByText(option.label)).toBeInTheDocument(); + }); + + const cumulativeItem = screen + .getByText('Cumulative') + .closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement; + expect(cumulativeItem).toHaveClass('active'); + const rollingItem = screen + .getByText('Rolling') + .closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement; + expect(rollingItem).not.toHaveClass('active'); + }); + + it('should render all timeframe options in rolling mode with last 5 minutes selected by default', () => { + render( + , + ); + EVALUATION_WINDOW_TIMEFRAME.rolling.forEach((option) => { + expect(screen.getByText(option.label)).toBeInTheDocument(); + }); + const last5MinutesItem = screen + .getByText(LAST_5_MINUTES_TEXT) + .closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement; + expect(last5MinutesItem).toHaveClass('active'); + }); + + it('should render all timeframe options in cumulative mode with current hour selected by default', () => { + render( + , + ); + EVALUATION_WINDOW_TIMEFRAME.cumulative.forEach((option) => { + expect(screen.getByText(option.label)).toBeInTheDocument(); + }); + const currentHourItem = screen + .getByText('Current hour') + .closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement; + expect(currentHourItem).toHaveClass('active'); + }); + + it('renders help text in details section for rolling mode with non-custom timeframe', () => { + render( + , + ); + expect( + 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), + ).not.toBeInTheDocument(); + }); + + it('renders EvaluationWindowDetails component in details section for rolling mode with custom timeframe', () => { + render( + , + ); + + expect( + screen.queryByText( + 'A Rolling Window has a fixed size and shifts its starting point over time based on when the rules are evaluated.', + ), + ).not.toBeInTheDocument(); + expect( + screen.getByTestId(EVALUATION_WINDOW_DETAILS_TEST_ID), + ).toBeInTheDocument(); + }); + + it('renders EvaluationWindowDetails component in details section for cumulative mode', () => { + render( + , + ); + expect( + screen.queryByText( + 'A Cumulative Window has a fixed starting point and expands over time.', + ), + ).not.toBeInTheDocument(); + expect( + screen.getByTestId(EVALUATION_WINDOW_DETAILS_TEST_ID), + ).toBeInTheDocument(); + }); + + describe('keyboard navigation', () => { + it('should navigate down through window type options', () => { + render( + , + ); + + const rollingItem = screen + .getByText('Rolling') + .closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement; + rollingItem?.focus(); + + fireEvent.keyDown(rollingItem, { key: 'ArrowDown' }); + const cumulativeItem = screen + .getByText('Cumulative') + .closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS); + expect(cumulativeItem).toHaveFocus(); + }); + + it('should navigate up through window type options', () => { + render( + , + ); + + const cumulativeItem = screen + .getByText('Cumulative') + .closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement; + cumulativeItem?.focus(); + + fireEvent.keyDown(cumulativeItem, { key: 'ArrowUp' }); + const rollingItem = screen + .getByText('Rolling') + .closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS); + expect(rollingItem).toHaveFocus(); + }); + + it('should navigate right from window type to timeframe', () => { + render( + , + ); + + const rollingItem = screen + .getByText('Rolling') + .closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement; + rollingItem?.focus(); + + fireEvent.keyDown(rollingItem, { key: 'ArrowRight' }); + const timeframeItem = screen + .getByText(LAST_5_MINUTES_TEXT) + .closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS); + expect(timeframeItem).toHaveFocus(); + }); + + it('should navigate left from timeframe to window type', () => { + render( + , + ); + + const timeframeItem = screen + .getByText(LAST_5_MINUTES_TEXT) + .closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement; + timeframeItem?.focus(); + + fireEvent.keyDown(timeframeItem, { key: 'ArrowLeft' }); + const rollingItem = screen + .getByText('Rolling') + .closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS); + expect(rollingItem).toHaveFocus(); + }); + + it('should select option with Enter key', () => { + render( + , + ); + + const cumulativeItem = screen + .getByText('Cumulative') + .closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement; + cumulativeItem?.focus(); + + fireEvent.keyDown(cumulativeItem, { key: 'Enter' }); + expect(mockSetEvaluationWindow).toHaveBeenCalledWith({ + type: 'SET_WINDOW_TYPE', + payload: 'cumulative', + }); + }); + + it('should select option with Space key', () => { + render( + , + ); + + const cumulativeItem = screen + .getByText('Cumulative') + .closest(EVALUATION_WINDOW_CONTENT_LIST_ITEM_CLASS) as HTMLElement; + cumulativeItem?.focus(); + + fireEvent.keyDown(cumulativeItem, { key: ' ' }); + expect(mockSetEvaluationWindow).toHaveBeenCalledWith({ + type: 'SET_WINDOW_TYPE', + payload: 'cumulative', + }); + }); + }); +}); diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/testUtils.ts b/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/testUtils.ts index f73910bec1f2..b1f9029ef440 100644 --- a/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/testUtils.ts +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/testUtils.ts @@ -4,7 +4,10 @@ import { INITIAL_ALERT_THRESHOLD_STATE, INITIAL_EVALUATION_WINDOW_STATE, } from 'container/CreateAlertV2/context/constants'; -import { ICreateAlertContextProps } from 'container/CreateAlertV2/context/types'; +import { + EvaluationWindowState, + ICreateAlertContextProps, +} from 'container/CreateAlertV2/context/types'; import { AlertTypes } from 'types/api/alerts/alertTypes'; export const createMockAlertContextState = ( @@ -22,3 +25,10 @@ export const createMockAlertContextState = ( setEvaluationWindow: jest.fn(), ...overrides, }); + +export const createMockEvaluationWindowState = ( + overrides?: Partial, +): EvaluationWindowState => ({ + ...INITIAL_EVALUATION_WINDOW_STATE, + ...overrides, +}); diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/constants.ts b/frontend/src/container/CreateAlertV2/EvaluationSettings/constants.ts index 61f07dc8829b..4319b765565e 100644 --- a/frontend/src/container/CreateAlertV2/EvaluationSettings/constants.ts +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/constants.ts @@ -14,6 +14,7 @@ export const EVALUATION_WINDOW_TIMEFRAME = { { label: 'Last 1 hour', value: '1h0m0s' }, { label: 'Last 2 hours', value: '2h0m0s' }, { label: 'Last 4 hours', value: '4h0m0s' }, + { label: 'Custom', value: 'custom' }, ], cumulative: [ { label: 'Current hour', value: 'currentHour' }, @@ -60,3 +61,9 @@ export const TIMEZONE_DATA = generateTimezoneData().map((timezone) => ({ label: `${timezone.name} (${timezone.offset})`, value: timezone.value, })); + +export const CUMULATIVE_WINDOW_DESCRIPTION = + 'A Cumulative Window has a fixed starting point and expands over time.'; + +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.'; diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/index.ts b/frontend/src/container/CreateAlertV2/EvaluationSettings/index.ts new file mode 100644 index 000000000000..e3637027affc --- /dev/null +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/index.ts @@ -0,0 +1,3 @@ +import EvaluationSettings from './EvaluationSettings'; + +export default EvaluationSettings; diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/styles.scss b/frontend/src/container/CreateAlertV2/EvaluationSettings/styles.scss index 80860e4ead46..6cb15a2a87ef 100644 --- a/frontend/src/container/CreateAlertV2/EvaluationSettings/styles.scss +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/styles.scss @@ -238,7 +238,7 @@ } } - .ant-input { + .select-group .ant-input:not(.time-input-field) { background-color: var(--bg-ink-300); border: 1px solid var(--bg-slate-400); color: var(--bg-vanilla-100); diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/types.ts b/frontend/src/container/CreateAlertV2/EvaluationSettings/types.ts index 5b6fecc4e97f..f3f0afadd69c 100644 --- a/frontend/src/container/CreateAlertV2/EvaluationSettings/types.ts +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/types.ts @@ -32,8 +32,6 @@ export enum CumulativeWindowTimeframes { export interface IEvaluationWindowPopoverProps { evaluationWindow: EvaluationWindowState; setEvaluationWindow: Dispatch; - isOpen: boolean; - setIsOpen: Dispatch>; } export interface IEvaluationWindowDetailsProps { diff --git a/frontend/src/container/CreateAlertV2/utils.tsx b/frontend/src/container/CreateAlertV2/utils.tsx index 9451a2fc55f7..f164e9bed149 100644 --- a/frontend/src/container/CreateAlertV2/utils.tsx +++ b/frontend/src/container/CreateAlertV2/utils.tsx @@ -1,3 +1,9 @@ // UI side feature flag export const showNewCreateAlertsPage = (): boolean => localStorage.getItem('showNewCreateAlertsPage') === 'true'; + +// 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'; From a54c3a3d7f9afcc41b3a80dd7f8c239cf9dc143c Mon Sep 17 00:00:00 2001 From: Amlan Kumar Nandy <45410599+amlannandy@users.noreply.github.com> Date: Wed, 24 Sep 2025 10:22:05 +0700 Subject: [PATCH 3/7] chore: add notification settings section to create alert (#9162) --- .../container/CreateAlertV2/CreateAlertV2.tsx | 2 + .../TimeInput/TimeInput.scss | 37 ++ .../EvaluationSettings/__tests__/testUtils.ts | 3 + .../MultipleNotifications.tsx | 97 +++++ .../NotificationMessage.tsx | 92 +++++ .../NotificationSettings.tsx | 112 ++++++ .../__tests__/MultipleNotifications.test.tsx | 172 +++++++++ .../__tests__/NotificationMessage.test.tsx | 75 ++++ .../__tests__/NotificationSettings.test.tsx | 120 ++++++ .../NotificationSettings/index.ts | 3 + .../NotificationSettings/styles.scss | 346 ++++++++++++++++++ .../CreateAlertV2/context/constants.ts | 20 + .../container/CreateAlertV2/context/index.tsx | 10 + .../container/CreateAlertV2/context/types.ts | 33 +- .../container/CreateAlertV2/context/utils.tsx | 24 ++ 15 files changed, 1145 insertions(+), 1 deletion(-) create mode 100644 frontend/src/container/CreateAlertV2/NotificationSettings/MultipleNotifications.tsx create mode 100644 frontend/src/container/CreateAlertV2/NotificationSettings/NotificationMessage.tsx create mode 100644 frontend/src/container/CreateAlertV2/NotificationSettings/NotificationSettings.tsx create mode 100644 frontend/src/container/CreateAlertV2/NotificationSettings/__tests__/MultipleNotifications.test.tsx create mode 100644 frontend/src/container/CreateAlertV2/NotificationSettings/__tests__/NotificationMessage.test.tsx create mode 100644 frontend/src/container/CreateAlertV2/NotificationSettings/__tests__/NotificationSettings.test.tsx create mode 100644 frontend/src/container/CreateAlertV2/NotificationSettings/index.ts create mode 100644 frontend/src/container/CreateAlertV2/NotificationSettings/styles.scss diff --git a/frontend/src/container/CreateAlertV2/CreateAlertV2.tsx b/frontend/src/container/CreateAlertV2/CreateAlertV2.tsx index 185241254d3b..0ce7e0821fff 100644 --- a/frontend/src/container/CreateAlertV2/CreateAlertV2.tsx +++ b/frontend/src/container/CreateAlertV2/CreateAlertV2.tsx @@ -8,6 +8,7 @@ import AlertCondition from './AlertCondition'; import { CreateAlertProvider } from './context'; import CreateAlertHeader from './CreateAlertHeader'; import EvaluationSettings from './EvaluationSettings'; +import NotificationSettings from './NotificationSettings'; import QuerySection from './QuerySection'; import { showCondensedLayout } from './utils'; @@ -27,6 +28,7 @@ function CreateAlertV2({ {!showCondensedLayoutFlag ? : null} +
); diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/TimeInput/TimeInput.scss b/frontend/src/container/CreateAlertV2/EvaluationSettings/TimeInput/TimeInput.scss index 90306dcac286..fe4574f53ea7 100644 --- a/frontend/src/container/CreateAlertV2/EvaluationSettings/TimeInput/TimeInput.scss +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/TimeInput/TimeInput.scss @@ -49,3 +49,40 @@ user-select: none; } } + +.lightMode { + .time-input-container { + .time-input-field { + background-color: var(--bg-vanilla-300); + border: 1px solid var(--bg-vanilla-300); + color: var(--bg-ink-400); + + &::placeholder { + color: var(--bg-ink-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); + } + + &:disabled { + background-color: var(--bg-vanilla-300); + color: var(--bg-ink-300); + cursor: not-allowed; + + &:hover { + border-color: var(--bg-vanilla-300); + } + } + } + + .time-input-separator { + color: var(--bg-ink-300); + } + } +} diff --git a/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/testUtils.ts b/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/testUtils.ts index b1f9029ef440..b37bca228b41 100644 --- a/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/testUtils.ts +++ b/frontend/src/container/CreateAlertV2/EvaluationSettings/__tests__/testUtils.ts @@ -3,6 +3,7 @@ import { INITIAL_ALERT_STATE, INITIAL_ALERT_THRESHOLD_STATE, INITIAL_EVALUATION_WINDOW_STATE, + INITIAL_NOTIFICATION_SETTINGS_STATE, } from 'container/CreateAlertV2/context/constants'; import { EvaluationWindowState, @@ -23,6 +24,8 @@ export const createMockAlertContextState = ( setAdvancedOptions: jest.fn(), evaluationWindow: INITIAL_EVALUATION_WINDOW_STATE, setEvaluationWindow: jest.fn(), + notificationSettings: INITIAL_NOTIFICATION_SETTINGS_STATE, + setNotificationSettings: jest.fn(), ...overrides, }); diff --git a/frontend/src/container/CreateAlertV2/NotificationSettings/MultipleNotifications.tsx b/frontend/src/container/CreateAlertV2/NotificationSettings/MultipleNotifications.tsx new file mode 100644 index 000000000000..21274cc6a983 --- /dev/null +++ b/frontend/src/container/CreateAlertV2/NotificationSettings/MultipleNotifications.tsx @@ -0,0 +1,97 @@ +import { Select, Tooltip, Typography } from 'antd'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { Info } from 'lucide-react'; +import { useMemo } from 'react'; + +import { useCreateAlertState } from '../context'; + +function MultipleNotifications(): JSX.Element { + const { + notificationSettings, + setNotificationSettings, + } = useCreateAlertState(); + const { currentQuery } = useQueryBuilder(); + + const spaceAggregationOptions = useMemo(() => { + const allGroupBys = currentQuery.builder.queryData?.reduce( + (acc, query) => { + const groupByKeys = query.groupBy?.map((groupBy) => groupBy.key) || []; + return [...acc, ...groupByKeys]; + }, + [], + ); + const uniqueGroupBys = [...new Set(allGroupBys)]; + return uniqueGroupBys.map((key) => ({ + label: key, + value: key, + })); + }, [currentQuery.builder.queryData]); + + const isMultipleNotificationsEnabled = spaceAggregationOptions.length > 0; + + const multipleNotificationsInput = useMemo(() => { + const placeholder = isMultipleNotificationsEnabled + ? 'Select fields to group by (optional)' + : 'No grouping fields available'; + let input = ( +
+