diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/custom_threshold_rule_expression.test.tsx b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/custom_threshold_rule_expression.test.tsx index 7ad399658caf1..1013a6ae2048c 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/custom_threshold_rule_expression.test.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/custom_threshold_rule_expression.test.tsx @@ -42,7 +42,7 @@ describe('Expression', () => { async function setup( currentOptions?: CustomThresholdPrefillOptions, - customRuleParams?: Record + customRuleParams?: Partial ) { const ruleParams: RuleTypeParams & AlertParams = { criteria: [], @@ -164,7 +164,8 @@ describe('Expression', () => { it('should prefill the rule using the context metadata', async () => { const index = 'changedMockedIndex'; const currentOptions: CustomThresholdPrefillOptions = { - alertOnGroupDisappear: false, + alertOnGroupDisappear: true, + alertOnNoData: true, groupBy: ['host.hostname'], searchConfiguration: { index, @@ -191,7 +192,8 @@ describe('Expression', () => { const { ruleParams } = await setup(currentOptions, { searchConfiguration: undefined }); - expect(ruleParams.alertOnGroupDisappear).toEqual(false); + expect(ruleParams.alertOnGroupDisappear).toEqual(true); + expect(ruleParams.alertOnNoData).toEqual(true); expect(ruleParams.groupBy).toEqual(['host.hostname']); expect((ruleParams.searchConfiguration.query as Query).query).toBe('foo'); expect(ruleParams.searchConfiguration.index).toBe(index); @@ -211,6 +213,68 @@ describe('Expression', () => { ]); }); + it('should only set alertOnGroupDisappear to true if there is a group by field', async () => { + const customRuleParams: Partial = { + groupBy: ['host.hostname'], + }; + + const { ruleParams, wrapper } = await setup({}, customRuleParams); + + act(() => { + wrapper + .find('[data-test-subj="thresholdRuleAlertOnNoDataCheckbox"]') + .at(1) + .prop('onChange')?.({ + target: { checked: true }, + } as React.ChangeEvent); + }); + + expect(ruleParams.alertOnGroupDisappear).toEqual(true); + expect(ruleParams.alertOnNoData).toEqual(false); + + // Uncheck + act(() => { + wrapper + .find('[data-test-subj="thresholdRuleAlertOnNoDataCheckbox"]') + .at(1) + .prop('onChange')?.({ + target: { checked: false }, + } as React.ChangeEvent); + }); + + expect(ruleParams.alertOnGroupDisappear).toEqual(false); + expect(ruleParams.alertOnNoData).toEqual(false); + }); + + it('should only set alertOnNoData to true if there is no group by', async () => { + const { ruleParams, wrapper } = await setup(); + + act(() => { + wrapper + .find('[data-test-subj="thresholdRuleAlertOnNoDataCheckbox"]') + .at(1) + .prop('onChange')?.({ + target: { checked: true }, + } as React.ChangeEvent); + }); + + expect(ruleParams.alertOnGroupDisappear).toEqual(false); + expect(ruleParams.alertOnNoData).toEqual(true); + + // Uncheck + act(() => { + wrapper + .find('[data-test-subj="thresholdRuleAlertOnNoDataCheckbox"]') + .at(1) + .prop('onChange')?.({ + target: { checked: false }, + } as React.ChangeEvent); + }); + + expect(ruleParams.alertOnGroupDisappear).toEqual(false); + expect(ruleParams.alertOnNoData).toEqual(false); + }); + it('should show an error message when searchSource throws an error', async () => { const errorMessage = 'Error in searchSource create'; const kibanaMock = kibanaStartMock.startContract(); diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx index 061805a709ea6..bbbcb59057824 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/custom_threshold_rule_expression.tsx @@ -75,6 +75,11 @@ export default function Expressions(props: Props) { }, } = useKibana().services; + const hasGroupBy = useMemo( + () => !!ruleParams.groupBy && ruleParams.groupBy.length > 0, + [ruleParams.groupBy] + ); + const [timeSize, setTimeSize] = useState(1); const [timeUnit, setTimeUnit] = useState('m'); const [dataView, setDataView] = useState(); @@ -82,6 +87,10 @@ export default function Expressions(props: Props) { const [searchSource, setSearchSource] = useState(); const [paramsError, setParamsError] = useState(); const [paramsWarning, setParamsWarning] = useState(); + const [isNoDataChecked, setIsNoDataChecked] = useState( + (hasGroupBy && !!ruleParams.alertOnGroupDisappear) || + (!hasGroupBy && !!ruleParams.alertOnNoData) + ); const derivedIndexPattern = useMemo( () => ({ fields: dataView?.fields || [], @@ -177,11 +186,15 @@ export default function Expressions(props: Props) { } if (typeof ruleParams.alertOnNoData === 'undefined') { - setRuleParams('alertOnNoData', true); + preFillAlertOnNoData(); } if (typeof ruleParams.alertOnGroupDisappear === 'undefined') { preFillAlertOnGroupDisappear(); } + setIsNoDataChecked( + (hasGroupBy && !!ruleParams.alertOnGroupDisappear) || + (!hasGroupBy && !!ruleParams.alertOnNoData) + ); }, [metadata]); // eslint-disable-line react-hooks/exhaustive-deps const onSelectDataView = useCallback( @@ -250,9 +263,12 @@ export default function Expressions(props: Props) { const onGroupByChange = useCallback( (group: string | null | string[]) => { + const hasGroup = !!group && group.length > 0; setRuleParams('groupBy', group && group.length ? group : ''); + setRuleParams('alertOnGroupDisappear', hasGroup && isNoDataChecked); + setRuleParams('alertOnNoData', !hasGroup && isNoDataChecked); }, - [setRuleParams] + [setRuleParams, isNoDataChecked] ); const emptyError = useMemo(() => { @@ -314,20 +330,24 @@ export default function Expressions(props: Props) { } }, [metadata, setRuleParams]); + const preFillAlertOnNoData = useCallback(() => { + const md = metadata; + if (md && typeof md.currentOptions?.alertOnNoData !== 'undefined') { + setRuleParams('alertOnNoData', md.currentOptions.alertOnNoData); + } else { + setRuleParams('alertOnNoData', false); + } + }, [metadata, setRuleParams]); + const preFillAlertOnGroupDisappear = useCallback(() => { const md = metadata; if (md && typeof md.currentOptions?.alertOnGroupDisappear !== 'undefined') { setRuleParams('alertOnGroupDisappear', md.currentOptions.alertOnGroupDisappear); } else { - setRuleParams('alertOnGroupDisappear', true); + setRuleParams('alertOnGroupDisappear', false); } }, [metadata, setRuleParams]); - const hasGroupBy = useMemo( - () => ruleParams.groupBy && ruleParams.groupBy.length > 0, - [ruleParams.groupBy] - ); - if (paramsError) { return ( <> @@ -540,30 +560,55 @@ export default function Expressions(props: Props) { {i18n.translate( 'xpack.observability.customThreshold.rule.alertFlyout.alertOnGroupDisappear', { - defaultMessage: 'Alert me if a group stops reporting data', + defaultMessage: "Alert me if there's no data", } )}{' '} } - disabled={!hasGroupBy} - checked={Boolean(hasGroupBy && ruleParams.alertOnGroupDisappear)} - onChange={(e) => setRuleParams('alertOnGroupDisappear', e.target.checked)} + checked={isNoDataChecked} + onChange={(e) => { + const checked = e.target.checked; + setIsNoDataChecked(checked); + if (!checked) { + setRuleParams('alertOnGroupDisappear', false); + setRuleParams('alertOnNoData', false); + } else { + if (hasGroupBy) { + setRuleParams('alertOnGroupDisappear', true); + setRuleParams('alertOnNoData', false); + } else { + setRuleParams('alertOnGroupDisappear', false); + setRuleParams('alertOnNoData', true); + } + } + }} />