From 7e3f861f54ac1c5d2834e9c255fca77b2697084b Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 28 Mar 2023 15:24:17 +0200 Subject: [PATCH 01/15] support to filter fields from grouping --- .../progress_controls/progress_controls.tsx | 14 +- .../api/explain_log_rate_spikes/schema.ts | 1 + .../explain_log_rate_spikes_analysis.tsx | 212 ++++++++++++++++-- .../spike_analysis_table.tsx | 2 +- .../spike_analysis_table_groups.tsx | 5 +- .../server/routes/explain_log_rate_spikes.ts | 6 +- 6 files changed, 208 insertions(+), 32 deletions(-) diff --git a/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx b/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx index 52d45da53459c..205df82d1a819 100644 --- a/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx +++ b/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { type FC } from 'react'; import { useEuiTheme, @@ -34,19 +34,20 @@ interface ProgressControlProps { shouldRerunAnalysis: boolean; } -export function ProgressControls({ +export const ProgressControls: FC = ({ + children, progress, progressMessage, onRefresh, onCancel, isRunning, shouldRerunAnalysis, -}: ProgressControlProps) { +}) => { const { euiTheme } = useEuiTheme(); const runningProgressBarStyles = useAnimatedProgressBarBackground(euiTheme.colors.success); return ( - + @@ -105,11 +106,12 @@ export function ProgressControls({ )} {isRunning && ( - + )} + {children} ); -} +}; diff --git a/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/schema.ts b/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/schema.ts index 5114070711929..7ca9ecc28f2da 100644 --- a/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/schema.ts +++ b/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/schema.ts @@ -31,6 +31,7 @@ export const aiopsExplainLogRateSpikesSchema = schema.object({ remainingFieldCandidates: schema.maybe(schema.arrayOf(schema.string())), // TODO Improve schema significantTerms: schema.maybe(schema.arrayOf(schema.any())), + skipSignificantTermsHistograms: schema.maybe(schema.boolean()), }) ), }); diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx index 6006365e631bb..dc0ce7ccc2729 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx @@ -5,14 +5,24 @@ * 2.0. */ -import React, { useEffect, useMemo, useState, FC } from 'react'; -import { isEqual } from 'lodash'; +import React, { useEffect, useMemo, useState, type ChangeEvent, type FC } from 'react'; +import { isEqual, uniq } from 'lodash'; +import { css } from '@emotion/react'; import { + euiYScrollWithShadows, + useEuiTheme, EuiButton, + EuiButtonEmpty, EuiCallOut, EuiEmptyPrompt, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, EuiFormRow, + EuiPopover, + EuiPopoverFooter, + EuiPopoverTitle, EuiSpacer, EuiSwitch, EuiText, @@ -74,9 +84,19 @@ export const ExplainLogRateSpikesAnalysis: FC windowParameters, searchQuery, }) => { + const euiThemeContext = useEuiTheme(); const { http } = useAiopsAppContext(); const basePath = http.basePath.get() ?? ''; + // maxHeight: $euiDataGridPopoverMaxHeight + const fieldSelectPopover = useMemo( + () => css` + ${euiYScrollWithShadows(euiThemeContext, {})} + max-height: 400px; + `, + [euiThemeContext] + ); + const { clearAllRowState } = useSpikeAnalysisTableRowContext(); const [currentAnalysisWindowParameters, setCurrentAnalysisWindowParameters] = useState< @@ -95,6 +115,30 @@ export const ExplainLogRateSpikesAnalysis: FC clearAllRowState(); }; + const [fieldSearchText, setFieldSearchText] = useState(''); + const [skippedFields, setSkippedFields] = useState([]); + const setFieldsFilter = (fieldNames: string[], checked: boolean) => { + let updatedSkippedFields = [...skippedFields]; + if (!checked) { + updatedSkippedFields.push(...fieldNames); + } else { + updatedSkippedFields = skippedFields.filter((d) => !fieldNames.includes(d)); + } + setSkippedFields(updatedSkippedFields); + setOverrides({ + loaded: 0, + remainingFieldCandidates: [], + significantTerms: data.significantTerms.filter( + (d) => !updatedSkippedFields.includes(d.fieldName) + ), + skipSignificantTermsHistograms: true, + }); + }; + + const [isFieldSelectionPopoverOpen, setIsFieldSelectionPopoverOpen] = useState(false); + const onFieldSelectionButtonClick = () => setIsFieldSelectionPopoverOpen((isOpen) => !isOpen); + const closePopover = () => setIsFieldSelectionPopoverOpen(false); + const { cancel, start, @@ -118,6 +162,17 @@ export const ExplainLogRateSpikesAnalysis: FC { reducer: streamReducer, initialState } ); + const { significantTerms } = data; + const uniqueFieldNames = useMemo( + () => uniq(significantTerms.map((d) => d.fieldName)).sort(), + [significantTerms] + ); + const filteredUniqueFieldNames = useMemo(() => { + return uniqueFieldNames.filter( + (d) => d.toLowerCase().indexOf(fieldSearchText.toLowerCase()) !== -1 + ); + }, [fieldSearchText, uniqueFieldNames]); + useEffect(() => { if (!isRunning) { const { loaded, remainingFieldCandidates, groupsMissing } = data; @@ -139,14 +194,18 @@ export const ExplainLogRateSpikesAnalysis: FC // Start handler clears possibly hovered or pinned // significant terms on analysis refresh. - function startHandler(continueAnalysis = false) { + function startHandler(continueAnalysis = false, resetGroupButton = true) { if (!continueAnalysis) { setOverrides(undefined); } // Reset grouping to false and clear all row selections when restarting the analysis. - setGroupResults(false); - clearAllRowState(); + if (resetGroupButton) { + setGroupResults(false); + clearAllRowState(); + setSkippedFields([]); + setIsFieldSelectionPopoverOpen(false); + } setCurrentAnalysisWindowParameters(windowParameters); @@ -196,7 +255,130 @@ export const ExplainLogRateSpikesAnalysis: FC onRefresh={() => startHandler(false)} onCancel={cancel} shouldRerunAnalysis={shouldRerunAnalysis} - /> + > + + + + + + + <> + + + + } + isOpen={isFieldSelectionPopoverOpen} + closePopover={closePopover} + > + + ) => + setFieldSearchText(e.currentTarget.value) + } + data-test-subj="dataGridColumnSelectorSearch" + /> + +
+ {filteredUniqueFieldNames.map((fieldName) => ( +
+ setFieldsFilter([fieldName], e.target.checked)} + checked={!skippedFields.includes(fieldName)} + /> +
+ ))} +
+ + + {fieldSearchText.length > 0 && ( + <> + + setFieldsFilter(filteredUniqueFieldNames, true)} + data-test-subj="dataGridColumnSelectorShowAllButton" + > + + + + + setFieldsFilter(filteredUniqueFieldNames, false)} + data-test-subj="dataGridColumnSelectorHideAllButton" + > + + + + + )} + + { + startHandler(true, false); + setFieldSearchText(''); + closePopover(); + }} + disabled={isRunning} + > + + + + + +
+ +
+ {errors.length > 0 ? ( <> @@ -235,24 +417,10 @@ export const ExplainLogRateSpikesAnalysis: FC ) : null} - {showSpikeAnalysisTable && foundGroups && ( + {showSpikeAnalysisTable && groupResults && foundGroups && ( <> - - - + {groupResults ? groupResultsHelpMessage : undefined} )} diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table.tsx index 619cd11ea757f..b670a42c45a3b 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table.tsx @@ -395,7 +395,7 @@ export const SpikeAnalysisTable: FC = ({ columns={columns} items={pageOfItems} onChange={onChange} - pagination={pagination} + pagination={pagination.totalItemCount > pagination.pageSize ? pagination : undefined} loading={false} sorting={sorting as EuiTableSortingType} rowProps={(significantTerm) => { diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx index 9f4aa2a148d76..3b33c4a44e806 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import md5 from 'md5'; import React, { FC, useCallback, useMemo, useState } from 'react'; import { sortBy } from 'lodash'; @@ -253,7 +254,7 @@ export const SpikeAnalysisGroupsTable: FC = ({ {(duplicate ?? 0) <= 1 ? '* ' : ''} {`${fieldName}: `} - {`${fieldValue}`} + {`${md5(fieldValue + '')}`} @@ -502,7 +503,7 @@ export const SpikeAnalysisGroupsTable: FC = ({ itemId="id" itemIdToExpandedRowMap={itemIdToExpandedRowMap} onChange={onChange} - pagination={pagination} + pagination={pagination.totalItemCount > pagination.pageSize ? pagination : undefined} loading={false} sorting={sorting as EuiTableSortingType} rowProps={(group) => { diff --git a/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts b/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts index c4edc64c7ee76..24c294628321e 100644 --- a/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts +++ b/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts @@ -560,7 +560,11 @@ export const defineExplainLogRateSpikesRoute = ( logDebugMessage(`Fetch ${significantTerms.length} field/value histograms.`); // time series filtered by fields - if (significantTerms.length > 0 && overallTimeSeries !== undefined) { + if ( + significantTerms.length > 0 && + overallTimeSeries !== undefined && + !request.body.overrides?.skipSignificantTermsHistograms + ) { const fieldValueHistogramQueue = queue(async function (cp: SignificantTerm) { if (shouldStop) { logDebugMessage('shouldStop abort fetching field/value histograms.'); From 63dec48673ea289a2ad4ae9ee548e56ae383a598 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 29 Mar 2023 08:06:37 +0200 Subject: [PATCH 02/15] move field selection popover to its own component --- .../explain_log_rate_spikes_analysis.tsx | 161 ++------------- .../field_filter_popover.tsx | 187 ++++++++++++++++++ .../spike_analysis_table_groups.tsx | 3 +- 3 files changed, 200 insertions(+), 151 deletions(-) create mode 100644 x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx index dc0ce7ccc2729..dc315d819e1c6 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx @@ -5,24 +5,15 @@ * 2.0. */ -import React, { useEffect, useMemo, useState, type ChangeEvent, type FC } from 'react'; +import React, { useEffect, useMemo, useState, type FC } from 'react'; import { isEqual, uniq } from 'lodash'; -import { css } from '@emotion/react'; import { - euiYScrollWithShadows, - useEuiTheme, EuiButton, - EuiButtonEmpty, EuiCallOut, EuiEmptyPrompt, - EuiFieldText, - EuiFlexGroup, EuiFlexItem, EuiFormRow, - EuiPopover, - EuiPopoverFooter, - EuiPopoverTitle, EuiSpacer, EuiSwitch, EuiText, @@ -48,6 +39,8 @@ import { import {} from '../spike_analysis_table'; import { useSpikeAnalysisTableRowContext } from '../spike_analysis_table/spike_analysis_table_row_provider'; +import { FieldFilterPopover } from './field_filter_popover'; + const groupResultsMessage = i18n.translate( 'xpack.aiops.spikeAnalysisTable.groupedSwitchLabel.groupResults', { @@ -84,19 +77,9 @@ export const ExplainLogRateSpikesAnalysis: FC windowParameters, searchQuery, }) => { - const euiThemeContext = useEuiTheme(); const { http } = useAiopsAppContext(); const basePath = http.basePath.get() ?? ''; - // maxHeight: $euiDataGridPopoverMaxHeight - const fieldSelectPopover = useMemo( - () => css` - ${euiYScrollWithShadows(euiThemeContext, {})} - max-height: 400px; - `, - [euiThemeContext] - ); - const { clearAllRowState } = useSpikeAnalysisTableRowContext(); const [currentAnalysisWindowParameters, setCurrentAnalysisWindowParameters] = useState< @@ -115,30 +98,16 @@ export const ExplainLogRateSpikesAnalysis: FC clearAllRowState(); }; - const [fieldSearchText, setFieldSearchText] = useState(''); - const [skippedFields, setSkippedFields] = useState([]); - const setFieldsFilter = (fieldNames: string[], checked: boolean) => { - let updatedSkippedFields = [...skippedFields]; - if (!checked) { - updatedSkippedFields.push(...fieldNames); - } else { - updatedSkippedFields = skippedFields.filter((d) => !fieldNames.includes(d)); - } - setSkippedFields(updatedSkippedFields); + const onFieldsFilterChange = (skippedFields: string[]) => { setOverrides({ loaded: 0, remainingFieldCandidates: [], - significantTerms: data.significantTerms.filter( - (d) => !updatedSkippedFields.includes(d.fieldName) - ), + significantTerms: data.significantTerms.filter((d) => !skippedFields.includes(d.fieldName)), skipSignificantTermsHistograms: true, }); + startHandler(true, false); }; - const [isFieldSelectionPopoverOpen, setIsFieldSelectionPopoverOpen] = useState(false); - const onFieldSelectionButtonClick = () => setIsFieldSelectionPopoverOpen((isOpen) => !isOpen); - const closePopover = () => setIsFieldSelectionPopoverOpen(false); - const { cancel, start, @@ -167,11 +136,6 @@ export const ExplainLogRateSpikesAnalysis: FC () => uniq(significantTerms.map((d) => d.fieldName)).sort(), [significantTerms] ); - const filteredUniqueFieldNames = useMemo(() => { - return uniqueFieldNames.filter( - (d) => d.toLowerCase().indexOf(fieldSearchText.toLowerCase()) !== -1 - ); - }, [fieldSearchText, uniqueFieldNames]); useEffect(() => { if (!isRunning) { @@ -203,8 +167,6 @@ export const ExplainLogRateSpikesAnalysis: FC if (resetGroupButton) { setGroupResults(false); clearAllRowState(); - setSkippedFields([]); - setIsFieldSelectionPopoverOpen(false); } setCurrentAnalysisWindowParameters(windowParameters); @@ -272,111 +234,12 @@ export const ExplainLogRateSpikesAnalysis: FC
- <> - - - - } - isOpen={isFieldSelectionPopoverOpen} - closePopover={closePopover} - > - - ) => - setFieldSearchText(e.currentTarget.value) - } - data-test-subj="dataGridColumnSelectorSearch" - /> - -
- {filteredUniqueFieldNames.map((fieldName) => ( -
- setFieldsFilter([fieldName], e.target.checked)} - checked={!skippedFields.includes(fieldName)} - /> -
- ))} -
- - - {fieldSearchText.length > 0 && ( - <> - - setFieldsFilter(filteredUniqueFieldNames, true)} - data-test-subj="dataGridColumnSelectorShowAllButton" - > - - - - - setFieldsFilter(filteredUniqueFieldNames, false)} - data-test-subj="dataGridColumnSelectorHideAllButton" - > - - - - - )} - - { - startHandler(true, false); - setFieldSearchText(''); - closePopover(); - }} - disabled={isRunning} - > - - - - - -
- +
{errors.length > 0 ? ( diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx new file mode 100644 index 0000000000000..825d012fffc29 --- /dev/null +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx @@ -0,0 +1,187 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect, useMemo, useState, type ChangeEvent, type FC } from 'react'; +import { css } from '@emotion/react'; + +import { + euiYScrollWithShadows, + useEuiTheme, + EuiButton, + EuiButtonEmpty, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiPopoverFooter, + EuiPopoverTitle, + EuiSwitch, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +interface FieldFilterPopoverProps { + disabled?: boolean; + disabledApplyButton?: boolean; + uniqueFieldNames: string[]; + onChange: (skippedFields: string[]) => void; +} + +// This component is mostly inspired by EUI's Data Grid Column Selector (`src/components/datagrid/controls/column_selector.tsx`). +export const FieldFilterPopover: FC = ({ + disabled, + disabledApplyButton, + uniqueFieldNames, + onChange, +}) => { + const euiThemeContext = useEuiTheme(); + // maxHeight: $euiDataGridPopoverMaxHeight + const fieldSelectPopover = useMemo( + () => css` + ${euiYScrollWithShadows(euiThemeContext, {})} + max-height: 400px; + `, + [euiThemeContext] + ); + + const [fieldSearchText, setFieldSearchText] = useState(''); + const [skippedFields, setSkippedFields] = useState([]); + const setFieldsFilter = (fieldNames: string[], checked: boolean) => { + let updatedSkippedFields = [...skippedFields]; + if (!checked) { + updatedSkippedFields.push(...fieldNames); + } else { + updatedSkippedFields = skippedFields.filter((d) => !fieldNames.includes(d)); + } + setSkippedFields(updatedSkippedFields); + }; + + const [isFieldSelectionPopoverOpen, setIsFieldSelectionPopoverOpen] = useState(false); + const onFieldSelectionButtonClick = () => setIsFieldSelectionPopoverOpen((isOpen) => !isOpen); + const closePopover = () => setIsFieldSelectionPopoverOpen(false); + + const filteredUniqueFieldNames = useMemo(() => { + return uniqueFieldNames.filter( + (d) => d.toLowerCase().indexOf(fieldSearchText.toLowerCase()) !== -1 + ); + }, [fieldSearchText, uniqueFieldNames]); + + // If the supplied list of unique field names changes, do a sanity check to only + // keep field names in the list of skipped fields that still are in the list of unique fields. + useEffect(() => { + setSkippedFields((previousSkippedFields) => + previousSkippedFields.filter((d) => uniqueFieldNames.includes(d)) + ); + }, [uniqueFieldNames]); + + return ( + + + + } + isOpen={isFieldSelectionPopoverOpen} + closePopover={closePopover} + > + + ) => setFieldSearchText(e.currentTarget.value)} + data-test-subj="dataGridColumnSelectorSearch" + /> + +
+ {filteredUniqueFieldNames.map((fieldName) => ( +
+ setFieldsFilter([fieldName], e.target.checked)} + checked={!skippedFields.includes(fieldName)} + /> +
+ ))} +
+ + + {fieldSearchText.length > 0 && ( + <> + + setFieldsFilter(filteredUniqueFieldNames, true)} + data-test-subj="dataGridColumnSelectorShowAllButton" + > + + + + + setFieldsFilter(filteredUniqueFieldNames, false)} + data-test-subj="dataGridColumnSelectorHideAllButton" + > + + + + + )} + + { + onChange(skippedFields); + setFieldSearchText(''); + setIsFieldSelectionPopoverOpen(false); + closePopover(); + }} + disabled={disabledApplyButton} + > + + + + + +
+ ); +}; diff --git a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx index 3b33c4a44e806..20be12531e53a 100644 --- a/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx +++ b/x-pack/plugins/aiops/public/components/spike_analysis_table/spike_analysis_table_groups.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import md5 from 'md5'; import React, { FC, useCallback, useMemo, useState } from 'react'; import { sortBy } from 'lodash'; @@ -254,7 +253,7 @@ export const SpikeAnalysisGroupsTable: FC = ({ {(duplicate ?? 0) <= 1 ? '* ' : ''} {`${fieldName}: `} - {`${md5(fieldValue + '')}`} + {`${fieldValue}`} From 280972c0b09d46053cb7373739c8b2263b502ef9 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 29 Mar 2023 09:41:20 +0200 Subject: [PATCH 03/15] fix grouping edge cases --- .../api/explain_log_rate_spikes/actions.ts | 10 +++++ .../api/explain_log_rate_spikes/index.ts | 1 + .../api/explain_log_rate_spikes/schema.ts | 2 +- .../aiops/common/api/stream_reducer.ts | 2 + .../explain_log_rate_spikes_analysis.tsx | 18 +++++--- .../field_filter_apply_button.tsx | 43 +++++++++++++++++++ .../field_filter_popover.tsx | 23 ++++++---- .../server/routes/explain_log_rate_spikes.ts | 8 +++- 8 files changed, 90 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_apply_button.tsx diff --git a/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/actions.ts b/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/actions.ts index cbd2050a5ba8f..79a80c0040e2f 100644 --- a/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/actions.ts +++ b/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/actions.ts @@ -21,6 +21,7 @@ export const API_ACTION_NAME = { PING: 'ping', RESET_ALL: 'reset_all', RESET_ERRORS: 'reset_errors', + RESET_GROUPS: 'reset_groups', UPDATE_LOADING_STATE: 'update_loading_state', } as const; export type ApiActionName = typeof API_ACTION_NAME[keyof typeof API_ACTION_NAME]; @@ -119,6 +120,14 @@ export function resetAllAction(): ApiActionResetAll { return { type: API_ACTION_NAME.RESET_ALL }; } +interface ApiActionResetGroups { + type: typeof API_ACTION_NAME.RESET_GROUPS; +} + +export function resetGroupsAction(): ApiActionResetGroups { + return { type: API_ACTION_NAME.RESET_GROUPS }; +} + interface ApiActionUpdateLoadingState { type: typeof API_ACTION_NAME.UPDATE_LOADING_STATE; payload: { @@ -148,4 +157,5 @@ export type AiopsExplainLogRateSpikesApiAction = | ApiActionPing | ApiActionResetAll | ApiActionResetErrors + | ApiActionResetGroups | ApiActionUpdateLoadingState; diff --git a/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/index.ts b/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/index.ts index 87bcea25810dc..52e26a534baa9 100644 --- a/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/index.ts +++ b/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/index.ts @@ -14,6 +14,7 @@ export { pingAction, resetAllAction, resetErrorsAction, + resetGroupsAction, updateLoadingStateAction, API_ACTION_NAME, } from './actions'; diff --git a/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/schema.ts b/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/schema.ts index 7ca9ecc28f2da..957e5463312f9 100644 --- a/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/schema.ts +++ b/x-pack/plugins/aiops/common/api/explain_log_rate_spikes/schema.ts @@ -31,7 +31,7 @@ export const aiopsExplainLogRateSpikesSchema = schema.object({ remainingFieldCandidates: schema.maybe(schema.arrayOf(schema.string())), // TODO Improve schema significantTerms: schema.maybe(schema.arrayOf(schema.any())), - skipSignificantTermsHistograms: schema.maybe(schema.boolean()), + regroupOnly: schema.maybe(schema.boolean()), }) ), }); diff --git a/x-pack/plugins/aiops/common/api/stream_reducer.ts b/x-pack/plugins/aiops/common/api/stream_reducer.ts index 7043752abd8f1..c78c94987e0ad 100644 --- a/x-pack/plugins/aiops/common/api/stream_reducer.ts +++ b/x-pack/plugins/aiops/common/api/stream_reducer.ts @@ -66,6 +66,8 @@ export function streamReducer( return { ...state, errors: [...state.errors, action.payload] }; case API_ACTION_NAME.RESET_ERRORS: return { ...state, errors: [] }; + case API_ACTION_NAME.RESET_GROUPS: + return { ...state, significantTermsGroups: [] }; case API_ACTION_NAME.RESET_ALL: return initialState; case API_ACTION_NAME.UPDATE_LOADING_STATE: diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx index dc315d819e1c6..e235718769b79 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx @@ -86,12 +86,13 @@ export const ExplainLogRateSpikesAnalysis: FC WindowParameters | undefined >(); const [groupResults, setGroupResults] = useState(false); + const [groupSkipFields, setGroupSkipFields] = useState([]); const [overrides, setOverrides] = useState< ApiExplainLogRateSpikes['body']['overrides'] | undefined >(undefined); const [shouldStart, setShouldStart] = useState(false); - const onSwitchToggle = (e: { target: { checked: React.SetStateAction } }) => { + const onGroupResultsToggle = (e: { target: { checked: React.SetStateAction } }) => { setGroupResults(e.target.checked); // When toggling the group switch, clear all row selections @@ -99,11 +100,12 @@ export const ExplainLogRateSpikesAnalysis: FC }; const onFieldsFilterChange = (skippedFields: string[]) => { + setGroupSkipFields(skippedFields); setOverrides({ loaded: 0, remainingFieldCandidates: [], significantTerms: data.significantTerms.filter((d) => !skippedFields.includes(d.fieldName)), - skipSignificantTermsHistograms: true, + regroupOnly: true, }); startHandler(true, false); }; @@ -208,6 +210,10 @@ export const ExplainLogRateSpikesAnalysis: FC }, 0); const foundGroups = groupTableItems.length > 0 && groupItemCount > 0; + // Disable the grouping switch toggle only if no groups were found, + // the toggle wasn't enabled already and no fields were selected to be skipped. + const disabledGroupResultsSwitch = !foundGroups && !groupResults && groupSkipFields.length === 0; + return (
data-test-subj={`aiopsExplainLogRateSpikesGroupSwitch${ groupResults ? ' checked' : '' }`} - disabled={!foundGroups} + disabled={disabledGroupResultsSwitch} showLabel={true} label={groupResultsMessage} checked={groupResults} - onChange={onSwitchToggle} + onChange={onGroupResultsToggle} compressed /> @@ -309,7 +315,7 @@ export const ExplainLogRateSpikesAnalysis: FC } /> )} - {showSpikeAnalysisTable && groupResults && foundGroups ? ( + {showSpikeAnalysisTable && groupResults ? ( dataViewId={dataView.id} /> ) : null} - {showSpikeAnalysisTable && (!groupResults || !foundGroups) ? ( + {showSpikeAnalysisTable && !groupResults ? ( void; + tooltipContent?: string; +} + +export const FieldFilterApplyButton: FC = ({ + disabled, + onClick, + tooltipContent, +}) => { + const button = ( + + + + ); + + if (tooltipContent) { + return ( + + {button} + + ); + } + + return button; +}; diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx index 825d012fffc29..718b55a04807e 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx @@ -25,6 +25,8 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; +import { FieldFilterApplyButton } from './field_filter_apply_button'; + interface FieldFilterPopoverProps { disabled?: boolean; disabledApplyButton?: boolean; @@ -79,6 +81,8 @@ export const FieldFilterPopover: FC = ({ ); }, [uniqueFieldNames]); + const selectedFieldCount = uniqueFieldNames.length - skippedFields.length; + return ( = ({ )} - { onChange(skippedFields); setFieldSearchText(''); setIsFieldSelectionPopoverOpen(false); closePopover(); }} - disabled={disabledApplyButton} - > - - + disabled={disabledApplyButton || selectedFieldCount < 2} + tooltipContent={ + selectedFieldCount < 2 + ? i18n.translate('xpack.aiops.analysis.fieldSelectorNotEnoughFieldsSelected', { + defaultMessage: 'Grouping requires at least 2 fields to be selected.', + }) + : undefined + } + /> diff --git a/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts b/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts index 24c294628321e..bd21766a63aa0 100644 --- a/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts +++ b/x-pack/plugins/aiops/server/routes/explain_log_rate_spikes.ts @@ -33,6 +33,7 @@ import { pingAction, resetAllAction, resetErrorsAction, + resetGroupsAction, updateLoadingStateAction, AiopsExplainLogRateSpikesApiAction, } from '../../common/api/explain_log_rate_spikes'; @@ -170,6 +171,11 @@ export const defineExplainLogRateSpikesRoute = ( push(resetErrorsAction()); } + if (request.body.overrides?.regroupOnly) { + logDebugMessage('Reset Groups.'); + push(resetGroupsAction()); + } + if (request.body.overrides?.loaded) { logDebugMessage(`Set 'loaded' override to '${request.body.overrides?.loaded}'.`); loaded = request.body.overrides?.loaded; @@ -563,7 +569,7 @@ export const defineExplainLogRateSpikesRoute = ( if ( significantTerms.length > 0 && overallTimeSeries !== undefined && - !request.body.overrides?.skipSignificantTermsHistograms + !request.body.overrides?.regroupOnly ) { const fieldValueHistogramQueue = queue(async function (cp: SignificantTerm) { if (shouldStop) { From b15c3978423350a5dc57217b93627587ed7b86e4 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 29 Mar 2023 10:54:08 +0200 Subject: [PATCH 04/15] cleanup css --- .../explain_log_rate_spikes/field_filter_popover.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx index 718b55a04807e..5c509be43a713 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx @@ -34,7 +34,8 @@ interface FieldFilterPopoverProps { onChange: (skippedFields: string[]) => void; } -// This component is mostly inspired by EUI's Data Grid Column Selector (`src/components/datagrid/controls/column_selector.tsx`). +// This component is mostly inspired by EUI's Data Grid Column Selector +// https://github.com/elastic/eui/blob/main/src/components/datagrid/controls/column_selector.tsx export const FieldFilterPopover: FC = ({ disabled, disabledApplyButton, @@ -42,7 +43,7 @@ export const FieldFilterPopover: FC = ({ onChange, }) => { const euiThemeContext = useEuiTheme(); - // maxHeight: $euiDataGridPopoverMaxHeight + // Inspired by https://github.com/elastic/eui/blob/main/src/components/datagrid/controls/_data_grid_column_selector.scss const fieldSelectPopover = useMemo( () => css` ${euiYScrollWithShadows(euiThemeContext, {})} From a8747ba16aa85a216c32172ec9fa3c29f6794bee Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 30 Mar 2023 09:38:10 +0200 Subject: [PATCH 05/15] jest tests --- .../aiops/common/api/stream_reducer.test.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/aiops/common/api/stream_reducer.test.ts b/x-pack/plugins/aiops/common/api/stream_reducer.test.ts index 3ea239b79ee2e..710f575242b74 100644 --- a/x-pack/plugins/aiops/common/api/stream_reducer.test.ts +++ b/x-pack/plugins/aiops/common/api/stream_reducer.test.ts @@ -5,9 +5,14 @@ * 2.0. */ +import { significantTerms } from '../__mocks__/artificial_logs/significant_terms'; +import { finalSignificantTermGroups } from '../__mocks__/artificial_logs/final_significant_term_groups'; + import { addSignificantTermsAction, + addSignificantTermsGroupAction, resetAllAction, + resetGroupsAction, updateLoadingStateAction, } from './explain_log_rate_spikes'; import { initialState, streamReducer } from './stream_reducer'; @@ -29,7 +34,7 @@ describe('streamReducer', () => { }); }); - it('adds significant term, then resets state again', () => { + it('adds significant term, then resets all state again', () => { const state1 = streamReducer( initialState, addSignificantTermsAction([ @@ -53,4 +58,24 @@ describe('streamReducer', () => { expect(state2.significantTerms).toHaveLength(0); }); + + it('adds significant terms and groups, then resets groups only', () => { + const state1 = streamReducer(initialState, addSignificantTermsAction(significantTerms)); + + expect(state1.significantTerms).toHaveLength(4); + expect(state1.significantTermsGroups).toHaveLength(0); + + const state2 = streamReducer( + state1, + addSignificantTermsGroupAction(finalSignificantTermGroups) + ); + + expect(state2.significantTerms).toHaveLength(4); + expect(state2.significantTermsGroups).toHaveLength(4); + + const state3 = streamReducer(state2, resetGroupsAction()); + + expect(state3.significantTerms).toHaveLength(4); + expect(state3.significantTermsGroups).toHaveLength(0); + }); }); From 67ba5b3d891a0f46b01a1e19a3d38cf2cb5837dc Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 30 Mar 2023 10:19:16 +0200 Subject: [PATCH 06/15] update api integration tests --- .../artificial_logs/significant_terms.ts | 46 ++-- ... explain_log_rate_spikes_full_analysis.ts} | 79 +----- .../explain_log_rate_spikes_groups_only.ts | 224 ++++++++++++++++++ .../aiops/explain_log_rate_spikes_no_index.ts | 62 +++++ .../test/api_integration/apis/aiops/index.ts | 4 +- .../api_integration/apis/aiops/test_data.ts | 30 ++- .../test/api_integration/apis/aiops/types.ts | 2 + 7 files changed, 344 insertions(+), 103 deletions(-) rename x-pack/test/api_integration/apis/aiops/{explain_log_rate_spikes.ts => explain_log_rate_spikes_full_analysis.ts} (75%) create mode 100644 x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes_groups_only.ts create mode 100644 x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes_no_index.ts diff --git a/x-pack/plugins/aiops/common/__mocks__/artificial_logs/significant_terms.ts b/x-pack/plugins/aiops/common/__mocks__/artificial_logs/significant_terms.ts index b1ce53a7df087..1c71932496d78 100644 --- a/x-pack/plugins/aiops/common/__mocks__/artificial_logs/significant_terms.ts +++ b/x-pack/plugins/aiops/common/__mocks__/artificial_logs/significant_terms.ts @@ -6,48 +6,48 @@ */ export const significantTerms = [ + { + fieldName: 'user', + fieldValue: 'Peter', + doc_count: 1981, + bg_count: 553, + total_doc_count: 4669, + total_bg_count: 1975, + score: 47.38899434932384, + pValue: 2.62555579103777e-21, + normalizedScore: 0.8328439168064725, + }, { fieldName: 'response_code', fieldValue: '500', doc_count: 1819, bg_count: 553, - total_doc_count: 4671, + total_doc_count: 4669, total_bg_count: 1975, - score: 26.546201745993947, - pValue: 2.9589053032077285e-12, - normalizedScore: 0.7814127409489161, + score: 26.347710713220195, + pValue: 3.6085657805889595e-12, + normalizedScore: 0.7809229492301661, }, { fieldName: 'url', fieldValue: 'home.php', doc_count: 1744, bg_count: 632, - total_doc_count: 4671, + total_doc_count: 4669, total_bg_count: 1975, - score: 4.53094842981472, - pValue: 0.010770456205312423, - normalizedScore: 0.10333028878375965, + score: 4.631197208465419, + pValue: 0.00974308761016614, + normalizedScore: 0.12006631193078789, }, { fieldName: 'url', fieldValue: 'login.php', doc_count: 1738, bg_count: 632, - total_doc_count: 4671, - total_bg_count: 1975, - score: 4.53094842981472, - pValue: 0.010770456205312423, - normalizedScore: 0.10333028878375965, - }, - { - fieldName: 'user', - fieldValue: 'Peter', - doc_count: 1981, - bg_count: 553, - total_doc_count: 4671, + total_doc_count: 4669, total_bg_count: 1975, - score: 47.34435085428873, - pValue: 2.7454255728359757e-21, - normalizedScore: 0.8327337555873047, + score: 4.359614926663956, + pValue: 0.012783309213417932, + normalizedScore: 0.07472703283204607, }, ]; diff --git a/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes.ts b/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes_full_analysis.ts similarity index 75% rename from x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes.ts rename to x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes_full_analysis.ts index d2bf38ca8532c..5b48214d39bbd 100644 --- a/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes.ts +++ b/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes_full_analysis.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { sortBy } from 'lodash'; +import { orderBy } from 'lodash'; import fetch from 'node-fetch'; import { format as formatUrl } from 'url'; @@ -25,7 +25,7 @@ export default ({ getService }: FtrProviderContext) => { const kibanaServerUrl = formatUrl(config.get('servers.kibana')); const esArchiver = getService('esArchiver'); - describe('POST /internal/aiops/explain_log_rate_spikes', () => { + describe('POST /internal/aiops/explain_log_rate_spikes - full analysis', () => { explainLogRateSpikesTestData.forEach((testData) => { describe(`with ${testData.testName}`, () => { before(async () => { @@ -60,29 +60,16 @@ export default ({ getService }: FtrProviderContext) => { ); expect(addSignificantTermsActions.length).to.greaterThan(0); - const significantTerms = addSignificantTermsActions - .flatMap((d) => d.payload) - .sort(function (a, b) { - if (a.fieldName === b.fieldName) { - return b.fieldValue - a.fieldValue; - } - return a.fieldName > b.fieldName ? 1 : -1; - }); - - expect(significantTerms.length).to.eql( - testData.expected.significantTerms.length, - `Expected 'significantTerms.length' to be ${testData.expected.significantTerms.length}, got ${significantTerms.length}.` + const significantTerms = orderBy( + addSignificantTermsActions.flatMap((d) => d.payload), + ['doc_count'], + ['desc'] + ); + + expect(significantTerms).to.eql( + testData.expected.significantTerms, + 'Significant terms do not match expected values.' ); - significantTerms.forEach((cp, index) => { - const ecp = testData.expected.significantTerms[index]; - expect(cp.fieldName).to.eql(ecp.fieldName); - expect(cp.fieldValue).to.eql(ecp.fieldValue); - expect(cp.doc_count).to.eql( - ecp.doc_count, - `Expected doc_count for '${cp.fieldName}:${cp.fieldValue}' to be ${ecp.doc_count}, got ${cp.doc_count}` - ); - expect(cp.bg_count).to.eql(ecp.bg_count); - }); const histogramActions = data.filter((d) => d.type === testData.expected.histogramFilter); const histograms = histogramActions.flatMap((d) => d.payload); @@ -96,8 +83,8 @@ export default ({ getService }: FtrProviderContext) => { const groupActions = data.filter((d) => d.type === testData.expected.groupFilter); const groups = groupActions.flatMap((d) => d.payload); - expect(sortBy(groups, 'id')).to.eql( - sortBy(testData.expected.groups, 'id'), + expect(orderBy(groups, ['docCount'], ['desc'])).to.eql( + orderBy(testData.expected.groups, ['docCount'], ['desc']), 'Grouping result does not match expected values.' ); @@ -230,46 +217,6 @@ export default ({ getService }: FtrProviderContext) => { flushFix: false, }); }); - - it('should return an error for non existing index without streaming', async () => { - const resp = await supertest - .post(`/internal/aiops/explain_log_rate_spikes`) - .set('kbn-xsrf', 'kibana') - .send({ - ...testData.requestBody, - index: 'does_not_exist', - }) - .expect(200); - - const chunks: string[] = resp.body.toString().split('\n'); - - expect(chunks.length).to.eql( - testData.expected.noIndexChunksLength, - `Expected 'noIndexChunksLength' to be ${testData.expected.noIndexChunksLength}, got ${chunks.length}.` - ); - - const lastChunk = chunks.pop(); - expect(lastChunk).to.be(''); - - let data: any[] = []; - - expect(() => { - data = chunks.map((c) => JSON.parse(c)); - }).not.to.throwError(); - - expect(data.length).to.eql( - testData.expected.noIndexActionsLength, - `Expected 'noIndexActionsLength' to be ${testData.expected.noIndexActionsLength}, got ${data.length}.` - ); - data.forEach((d) => { - expect(typeof d.type).to.be('string'); - }); - - const errorActions = data.filter((d) => d.type === testData.expected.errorFilter); - expect(errorActions.length).to.be(1); - - expect(errorActions[0].payload).to.be('Failed to fetch index information.'); - }); }); }); }); diff --git a/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes_groups_only.ts b/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes_groups_only.ts new file mode 100644 index 0000000000000..b88c07cc0df1c --- /dev/null +++ b/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes_groups_only.ts @@ -0,0 +1,224 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { orderBy } from 'lodash'; +import fetch from 'node-fetch'; +import { format as formatUrl } from 'url'; + +import expect from '@kbn/expect'; + +import type { ApiExplainLogRateSpikes } from '@kbn/aiops-plugin/common/api'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; + +import { parseStream } from './parse_stream'; +import { explainLogRateSpikesTestData } from './test_data'; + +export default ({ getService }: FtrProviderContext) => { + const aiops = getService('aiops'); + const supertest = getService('supertest'); + const config = getService('config'); + const kibanaServerUrl = formatUrl(config.get('servers.kibana')); + const esArchiver = getService('esArchiver'); + + describe('POST /internal/aiops/explain_log_rate_spikes - groups only', () => { + explainLogRateSpikesTestData.forEach((testData) => { + const overrides = { + loaded: 0, + remainingFieldCandidates: [], + significantTerms: testData.expected.significantTerms, + regroupOnly: true, + }; + + describe(`with ${testData.testName}`, () => { + before(async () => { + if (testData.esArchive) { + await esArchiver.loadIfNeeded(testData.esArchive); + } else if (testData.dataGenerator) { + await aiops.explainLogRateSpikesDataGenerator.generateData(testData.dataGenerator); + } + }); + + after(async () => { + if (testData.esArchive) { + await esArchiver.unload(testData.esArchive); + } else if (testData.dataGenerator) { + await aiops.explainLogRateSpikesDataGenerator.removeGeneratedData( + testData.dataGenerator + ); + } + }); + + async function assertAnalysisResult(data: any[]) { + expect(data.length).to.eql( + testData.expected.actionsLengthGroupOnly, + `Expected 'actionsLengthGroupOnly' to be ${testData.expected.actionsLengthGroupOnly}, got ${data.length}.` + ); + data.forEach((d) => { + expect(typeof d.type).to.be('string'); + }); + + const addSignificantTermsActions = data.filter( + (d) => d.type === testData.expected.significantTermFilter + ); + expect(addSignificantTermsActions.length).to.be(0); + + const histogramActions = data.filter((d) => d.type === testData.expected.histogramFilter); + // for each significant term we should get a histogram + expect(histogramActions.length).to.be(0); + + const groupActions = data.filter((d) => d.type === testData.expected.groupFilter); + const groups = groupActions.flatMap((d) => d.payload); + + expect(orderBy(groups, ['docCount'], ['desc'])).to.eql( + orderBy(testData.expected.groups, ['docCount'], ['desc']), + 'Grouping result does not match expected values.' + ); + + const groupHistogramActions = data.filter( + (d) => d.type === testData.expected.groupHistogramFilter + ); + const groupHistograms = groupHistogramActions.flatMap((d) => d.payload); + // for each significant terms group we should get a histogram + expect(groupHistograms.length).to.be(groups.length); + // each histogram should have a length of 20 items. + groupHistograms.forEach((h, index) => { + expect(h.histogram.length).to.be(20); + }); + } + + async function requestWithoutStreaming(body: ApiExplainLogRateSpikes['body']) { + const resp = await supertest + .post(`/internal/aiops/explain_log_rate_spikes`) + .set('kbn-xsrf', 'kibana') + .send(body) + .expect(200); + + // compression is on by default so if the request body is undefined + // the response header should include "gzip" and otherwise be "undefined" + if (body.compressResponse === undefined) { + expect(resp.header['content-encoding']).to.be('gzip'); + } else if (body.compressResponse === false) { + expect(resp.header['content-encoding']).to.be(undefined); + } + + expect(Buffer.isBuffer(resp.body)).to.be(true); + + const chunks: string[] = resp.body.toString().split('\n'); + + expect(chunks.length).to.eql( + testData.expected.chunksLengthGroupOnly, + `Expected 'chunksLength' to be ${testData.expected.chunksLengthGroupOnly}, got ${chunks.length}.` + ); + + const lastChunk = chunks.pop(); + expect(lastChunk).to.be(''); + + let data: any[] = []; + + expect(() => { + data = chunks.map((c) => JSON.parse(c)); + }).not.to.throwError(); + + await assertAnalysisResult(data); + } + + it('should return group only data without streaming with compression with flushFix', async () => { + await requestWithoutStreaming({ ...testData.requestBody, overrides }); + }); + + it('should return group only data without streaming with compression without flushFix', async () => { + await requestWithoutStreaming({ ...testData.requestBody, overrides, flushFix: false }); + }); + + it('should return group only data without streaming without compression with flushFix', async () => { + await requestWithoutStreaming({ + ...testData.requestBody, + overrides, + compressResponse: false, + }); + }); + + it('should return group only data without streaming without compression without flushFix', async () => { + await requestWithoutStreaming({ + ...testData.requestBody, + overrides, + compressResponse: false, + flushFix: false, + }); + }); + + async function requestWithStreaming(body: ApiExplainLogRateSpikes['body']) { + const resp = await fetch(`${kibanaServerUrl}/internal/aiops/explain_log_rate_spikes`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'kbn-xsrf': 'stream', + }, + body: JSON.stringify(body), + }); + + // compression is on by default so if the request body is undefined + // the response header should include "gzip" and otherwise be "null" + if (body.compressResponse === undefined) { + expect(resp.headers.get('content-encoding')).to.be('gzip'); + } else if (body.compressResponse === false) { + expect(resp.headers.get('content-encoding')).to.be(null); + } + + expect(resp.ok).to.be(true); + expect(resp.status).to.be(200); + + const stream = resp.body; + + expect(stream).not.to.be(null); + + if (stream !== null) { + const data: any[] = []; + let chunkCounter = 0; + const parseStreamCallback = (c: number) => (chunkCounter = c); + + for await (const action of parseStream(stream, parseStreamCallback)) { + expect(action.type).not.to.be('error'); + data.push(action); + } + + // If streaming works correctly we should receive more than one chunk. + expect(chunkCounter).to.be.greaterThan(1); + + await assertAnalysisResult(data); + } + } + + it('should return group only in chunks with streaming with compression with flushFix', async () => { + await requestWithStreaming({ ...testData.requestBody, overrides }); + }); + + it('should return group only in chunks with streaming with compression without flushFix', async () => { + await requestWithStreaming({ ...testData.requestBody, overrides, flushFix: false }); + }); + + it('should return group only in chunks with streaming without compression with flushFix', async () => { + await requestWithStreaming({ + ...testData.requestBody, + overrides, + compressResponse: false, + }); + }); + + it('should return group only in chunks with streaming without compression without flushFix', async () => { + await requestWithStreaming({ + ...testData.requestBody, + overrides, + compressResponse: false, + flushFix: false, + }); + }); + }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes_no_index.ts b/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes_no_index.ts new file mode 100644 index 0000000000000..fb421697a98e1 --- /dev/null +++ b/x-pack/test/api_integration/apis/aiops/explain_log_rate_spikes_no_index.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; + +import type { FtrProviderContext } from '../../ftr_provider_context'; + +import { explainLogRateSpikesTestData } from './test_data'; + +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + + describe('POST /internal/aiops/explain_log_rate_spikes - no index', () => { + explainLogRateSpikesTestData.forEach((testData) => { + describe(`with ${testData.testName}`, () => { + it('should return an error for non existing index without streaming', async () => { + const resp = await supertest + .post(`/internal/aiops/explain_log_rate_spikes`) + .set('kbn-xsrf', 'kibana') + .send({ + ...testData.requestBody, + index: 'does_not_exist', + }) + .expect(200); + + const chunks: string[] = resp.body.toString().split('\n'); + + expect(chunks.length).to.eql( + testData.expected.noIndexChunksLength, + `Expected 'noIndexChunksLength' to be ${testData.expected.noIndexChunksLength}, got ${chunks.length}.` + ); + + const lastChunk = chunks.pop(); + expect(lastChunk).to.be(''); + + let data: any[] = []; + + expect(() => { + data = chunks.map((c) => JSON.parse(c)); + }).not.to.throwError(); + + expect(data.length).to.eql( + testData.expected.noIndexActionsLength, + `Expected 'noIndexActionsLength' to be ${testData.expected.noIndexActionsLength}, got ${data.length}.` + ); + data.forEach((d) => { + expect(typeof d.type).to.be('string'); + }); + + const errorActions = data.filter((d) => d.type === testData.expected.errorFilter); + expect(errorActions.length).to.be(1); + + expect(errorActions[0].payload).to.be('Failed to fetch index information.'); + }); + }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/aiops/index.ts b/x-pack/test/api_integration/apis/aiops/index.ts index f311f35a51f87..238484c644a28 100644 --- a/x-pack/test/api_integration/apis/aiops/index.ts +++ b/x-pack/test/api_integration/apis/aiops/index.ts @@ -14,7 +14,9 @@ export default function ({ loadTestFile }: FtrProviderContext) { this.tags(['aiops']); if (AIOPS_ENABLED) { - loadTestFile(require.resolve('./explain_log_rate_spikes')); + loadTestFile(require.resolve('./explain_log_rate_spikes_full_analysis')); + loadTestFile(require.resolve('./explain_log_rate_spikes_groups_only')); + loadTestFile(require.resolve('./explain_log_rate_spikes_no_index')); } }); } diff --git a/x-pack/test/api_integration/apis/aiops/test_data.ts b/x-pack/test/api_integration/apis/aiops/test_data.ts index 001000c29d611..b2bf31cded84a 100644 --- a/x-pack/test/api_integration/apis/aiops/test_data.ts +++ b/x-pack/test/api_integration/apis/aiops/test_data.ts @@ -31,7 +31,9 @@ export const explainLogRateSpikesTestData: TestData[] = [ }, expected: { chunksLength: 35, + chunksLengthGroupOnly: 5, actionsLength: 34, + actionsLengthGroupOnly: 4, noIndexChunksLength: 4, noIndexActionsLength: 3, significantTermFilter: 'add_significant_terms', @@ -40,27 +42,27 @@ export const explainLogRateSpikesTestData: TestData[] = [ histogramFilter: 'add_significant_terms_histogram', errorFilter: 'add_error', significantTerms: [ - { - fieldName: 'day_of_week', - fieldValue: 'Wednesday', - doc_count: 145, - bg_count: 142, - score: 36.31595998561873, - pValue: 1.6911377077437753e-16, - normalizedScore: 0.8055203624020835, - total_doc_count: 0, - total_bg_count: 0, - }, { fieldName: 'day_of_week', fieldValue: 'Thursday', doc_count: 157, bg_count: 224, + total_doc_count: 480, + total_bg_count: 1328, score: 20.366950718358762, pValue: 1.428057484826135e-9, normalizedScore: 0.7661649691018979, - total_doc_count: 0, - total_bg_count: 0, + }, + { + fieldName: 'day_of_week', + fieldValue: 'Wednesday', + doc_count: 145, + bg_count: 142, + total_doc_count: 480, + total_bg_count: 1328, + score: 36.31595998561873, + pValue: 1.6911377077437753e-16, + normalizedScore: 0.8055203624020835, }, ], groups: [], @@ -84,7 +86,9 @@ export const explainLogRateSpikesTestData: TestData[] = [ }, expected: { chunksLength: 27, + chunksLengthGroupOnly: 11, actionsLength: 26, + actionsLengthGroupOnly: 10, noIndexChunksLength: 4, noIndexActionsLength: 3, significantTermFilter: 'add_significant_terms', diff --git a/x-pack/test/api_integration/apis/aiops/types.ts b/x-pack/test/api_integration/apis/aiops/types.ts index b3c6ea166eeac..1cdd173f183f4 100644 --- a/x-pack/test/api_integration/apis/aiops/types.ts +++ b/x-pack/test/api_integration/apis/aiops/types.ts @@ -15,7 +15,9 @@ export interface TestData { requestBody: ApiExplainLogRateSpikes['body']; expected: { chunksLength: number; + chunksLengthGroupOnly: number; actionsLength: number; + actionsLengthGroupOnly: number; noIndexChunksLength: number; noIndexActionsLength: number; significantTermFilter: 'add_significant_terms'; From 19449fa85c87b940f43245385aae3624aefe0c5e Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 30 Mar 2023 10:24:51 +0200 Subject: [PATCH 07/15] fix jest tests --- .../queries/fetch_frequent_item_sets.test.ts | 14 ++++----- .../get_missing_significant_terms.test.ts | 30 +++++++++---------- ...ransform_significant_term_to_group.test.ts | 14 ++++----- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_item_sets.test.ts b/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_item_sets.test.ts index 142a04ed1b373..e2dbeb781b999 100644 --- a/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_item_sets.test.ts +++ b/x-pack/plugins/aiops/server/routes/queries/fetch_frequent_item_sets.test.ts @@ -16,17 +16,17 @@ describe('getShouldClauses', () => { expect(shouldClauses).toEqual([ { terms: { - response_code: ['500'], + user: ['Peter'], }, }, { terms: { - url: ['home.php', 'login.php'], + response_code: ['500'], }, }, { terms: { - user: ['Peter'], + url: ['home.php', 'login.php'], }, }, ]); @@ -38,6 +38,10 @@ describe('getFrequentItemSetsAggFields', () => { const frequentItemSetsAggFields = getFrequentItemSetsAggFields(significantTerms); expect(frequentItemSetsAggFields).toEqual([ + { + field: 'user', + include: ['Peter'], + }, { field: 'response_code', include: ['500'], @@ -46,10 +50,6 @@ describe('getFrequentItemSetsAggFields', () => { field: 'url', include: ['home.php', 'login.php'], }, - { - field: 'user', - include: ['Peter'], - }, ]); }); }); diff --git a/x-pack/plugins/aiops/server/routes/queries/get_missing_significant_terms.test.ts b/x-pack/plugins/aiops/server/routes/queries/get_missing_significant_terms.test.ts index a41f92511e361..5da659dd58631 100644 --- a/x-pack/plugins/aiops/server/routes/queries/get_missing_significant_terms.test.ts +++ b/x-pack/plugins/aiops/server/routes/queries/get_missing_significant_terms.test.ts @@ -34,27 +34,27 @@ describe('getMissingSignificantTerms', () => { ); expect(missingSignificantTerms).toEqual([ - { - bg_count: 632, - doc_count: 1738, - fieldName: 'url', - fieldValue: 'login.php', - normalizedScore: 0.10333028878375965, - pValue: 0.010770456205312423, - score: 4.53094842981472, - total_bg_count: 1975, - total_doc_count: 4671, - }, { bg_count: 553, doc_count: 1981, fieldName: 'user', fieldValue: 'Peter', - normalizedScore: 0.8327337555873047, - pValue: 2.7454255728359757e-21, - score: 47.34435085428873, + normalizedScore: 0.8328439168064725, + pValue: 2.62555579103777e-21, + score: 47.38899434932384, + total_bg_count: 1975, + total_doc_count: 4669, + }, + { + bg_count: 632, + doc_count: 1738, + fieldName: 'url', + fieldValue: 'login.php', + normalizedScore: 0.07472703283204607, + pValue: 0.012783309213417932, + score: 4.359614926663956, total_bg_count: 1975, - total_doc_count: 4671, + total_doc_count: 4669, }, ]); }); diff --git a/x-pack/plugins/aiops/server/routes/queries/transform_significant_term_to_group.test.ts b/x-pack/plugins/aiops/server/routes/queries/transform_significant_term_to_group.test.ts index f3b691f8c4bfb..ec86dbb47d81e 100644 --- a/x-pack/plugins/aiops/server/routes/queries/transform_significant_term_to_group.test.ts +++ b/x-pack/plugins/aiops/server/routes/queries/transform_significant_term_to_group.test.ts @@ -40,18 +40,18 @@ describe('getMissingSignificantTerms', () => { ); expect(transformed).toEqual({ - docCount: 1738, + docCount: 1981, group: [ { + docCount: 1981, duplicate: 1, - fieldName: 'url', - fieldValue: 'login.php', - docCount: 1738, - pValue: 0.010770456205312423, + fieldName: 'user', + fieldValue: 'Peter', + pValue: 2.62555579103777e-21, }, ], - id: '368426784', - pValue: 0.010770456205312423, + id: '817080373', + pValue: 2.62555579103777e-21, }); }); }); From d764d4a16d0453be8de83c86e08bafb6a3d718bd Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 30 Mar 2023 17:17:13 +0200 Subject: [PATCH 08/15] functional tests for field selector popover --- .../field_filter_apply_button.tsx | 7 +- .../field_filter_popover.tsx | 11 +-- .../apps/aiops/explain_log_rate_spikes.ts | 26 +++++++ .../test/functional/apps/aiops/test_data.ts | 10 +++ x-pack/test/functional/apps/aiops/types.ts | 4 ++ .../aiops/explain_log_rate_spikes_page.ts | 71 +++++++++++++++++++ 6 files changed, 124 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_apply_button.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_apply_button.tsx index 7092134177a6b..cbe3c8845e202 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_apply_button.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_apply_button.tsx @@ -23,7 +23,12 @@ export const FieldFilterApplyButton: FC = ({ tooltipContent, }) => { const button = ( - + = ({ })} value={fieldSearchText} onChange={(e: ChangeEvent) => setFieldSearchText(e.currentTarget.value)} - data-test-subj="dataGridColumnSelectorSearch" + data-test-subj="aiopsFieldSelectorSearch" /> -
+
{filteredUniqueFieldNames.map((fieldName) => (
= ({ size="xs" flush="left" onClick={() => setFieldsFilter(filteredUniqueFieldNames, true)} - data-test-subj="dataGridColumnSelectorShowAllButton" + data-test-subj="aiopsFieldSelectorEnableAllSelectedButton" > = ({ size="xs" flush="right" onClick={() => setFieldsFilter(filteredUniqueFieldNames, false)} - data-test-subj="dataGridColumnSelectorHideAllButton" + data-test-subj="aiopsFieldSelectorDisableAllSelectedButton" > ; + filteredAnalysisGroupsTable?: Array<{ group: string; docCount: string }>; analysisTable: Array<{ fieldName: string; fieldValue: string; @@ -25,5 +28,6 @@ export interface TestData { pValue: string; impact: string; }>; + fieldSelectorPopover: string[]; }; } diff --git a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts index 8c46af2076db0..7b8abe75f13a9 100644 --- a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts +++ b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts @@ -12,6 +12,7 @@ import type { FtrProviderContext } from '../../ftr_provider_context'; export function ExplainLogRateSpikesPageProvider({ getService }: FtrProviderContext) { const browser = getService('browser'); const elasticChart = getService('elasticChart'); + const ml = getService('ml'); const testSubjects = getService('testSubjects'); const retry = getService('retry'); @@ -121,6 +122,76 @@ export function ExplainLogRateSpikesPageProvider({ getService }: FtrProviderCont }); }, + async assertFieldFilterPopoverButtonExists(isOpen: boolean) { + await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail('aiopsFieldFilterButton'); + + if (isOpen) { + await testSubjects.existOrFail('aiopsFieldSelectorSearch'); + } else { + await testSubjects.missingOrFail('aiopsFieldSelectorSearch'); + } + }); + }, + + async clickFieldFilterPopoverButton(expectPopoverToBeOpen: boolean) { + await testSubjects.clickWhenNotDisabledWithoutRetry('aiopsFieldFilterButton'); + + await retry.tryForTime(30 * 1000, async () => { + await this.assertFieldFilterPopoverButtonExists(expectPopoverToBeOpen); + }); + }, + + async assertFieldSelectorFieldNameList(expectedFields: string[]) { + const currentFields = await testSubjects.getVisibleText('aiopsFieldSelectorFieldNameList'); + expect(currentFields).to.be(expectedFields.join('\n')); + }, + + async setFieldSelectorSearch(searchText: string) { + await ml.commonUI.setValueWithChecks('aiopsFieldSelectorSearch', searchText, { + clearWithKeyboard: true, + enforceDataTestSubj: true, + }); + await this.assertFieldSelectorSearchValue(searchText); + }, + + async clickFieldSelectorDisableAllSelectedButton() { + await testSubjects.clickWhenNotDisabledWithoutRetry( + 'aiopsFieldSelectorDisableAllSelectedButton' + ); + + await retry.tryForTime(30 * 1000, async () => { + await retry.tryForTime(5000, async () => { + await testSubjects.missingOrFail('aiopsFieldSelectorFieldNameListItem checked'); + }); + }); + }, + + async assertFieldSelectorSearchValue(expectedValue: string) { + const actualSearchValue = await testSubjects.getAttribute( + 'aiopsFieldSelectorSearch', + 'value' + ); + expect(actualSearchValue).to.eql( + expectedValue, + `Field selector search input text should be '${expectedValue}' (got '${actualSearchValue}')` + ); + }, + + async assertFieldFilterApplyButtonExists(disabled: boolean) { + await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail(`aiopsFieldFilterApplyButton${disabled ? ' disabled' : ''}`); + }); + }, + + async clickFieldFilterApplyButton() { + await testSubjects.clickWhenNotDisabledWithoutRetry('aiopsFieldFilterApplyButton'); + + await retry.tryForTime(30 * 1000, async () => { + await this.assertFieldFilterPopoverButtonExists(false); + }); + }, + async assertRerunAnalysisButtonExists(shouldRerun: boolean) { await testSubjects.existOrFail( `aiopsRerunAnalysisButton${shouldRerun ? ' shouldRerun' : ''}` From ea9490cd47ff6037ff616da4678b961ab0e61e30 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Thu, 30 Mar 2023 17:21:19 +0200 Subject: [PATCH 09/15] tweak popover layout --- .../explain_log_rate_spikes/field_filter_popover.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx index 80d14c846bb35..0a18c6f5a0346 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx @@ -140,7 +140,12 @@ export const FieldFilterPopover: FC = ({ ))}
- + {fieldSearchText.length > 0 && ( <> From 16f6714d6d022b83e641e6fddb95832ee0a4be3e Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Fri, 31 Mar 2023 10:22:41 +0200 Subject: [PATCH 10/15] tweak analysis toolbar --- .../progress_controls/progress_controls.tsx | 1 + .../field_filter_popover.tsx | 70 ++++++++++++------- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx b/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx index 205df82d1a819..733f61f5b1b16 100644 --- a/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx +++ b/x-pack/packages/ml/aiops_components/src/progress_controls/progress_controls.tsx @@ -79,6 +79,7 @@ export const ProgressControls: FC = ({ size="s" onClick={onRefresh} color={shouldRerunAnalysis ? 'warning' : 'primary'} + fill > diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx index 0a18c6f5a0346..3e1cb4299685f 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx @@ -19,7 +19,9 @@ import { EuiPopover, EuiPopoverFooter, EuiPopoverTitle, + EuiSpacer, EuiSwitch, + EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -94,7 +96,6 @@ export const FieldFilterPopover: FC = ({ data-test-subj="aiopsFieldFilterButton" onClick={onFieldSelectionButtonClick} disabled={disabled} - // disabled={!groupResults || isRunning} size="s" iconType="arrowDown" iconSide="right" @@ -110,6 +111,13 @@ export const FieldFilterPopover: FC = ({ closePopover={closePopover} > + + + + = ({ justifyContent="spaceBetween" alignItems="center" > - {fieldSearchText.length > 0 && ( - <> - - setFieldsFilter(filteredUniqueFieldNames, true)} - data-test-subj="aiopsFieldSelectorEnableAllSelectedButton" - > + <> + + setFieldsFilter(filteredUniqueFieldNames, true)} + data-test-subj="aiopsFieldSelectorEnableAllSelectedButton" + > + {fieldSearchText.length > 0 ? ( - - - - setFieldsFilter(filteredUniqueFieldNames, false)} - data-test-subj="aiopsFieldSelectorDisableAllSelectedButton" - > + ) : ( + + )} + + + + setFieldsFilter(filteredUniqueFieldNames, false)} + data-test-subj="aiopsFieldSelectorDisableAllSelectedButton" + > + {fieldSearchText.length > 0 ? ( - - - - )} + ) : ( + + )} + + + { From 16a3325443116b763ffc15b9081f9beb383f5b1e Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 11 Apr 2023 10:14:18 +0200 Subject: [PATCH 11/15] tweak popover help text --- .../components/explain_log_rate_spikes/field_filter_popover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx index 3e1cb4299685f..1d3716036675d 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx @@ -114,7 +114,7 @@ export const FieldFilterPopover: FC = ({ From ae9ba6b015d9e327975996044249262c92354a9b Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 11 Apr 2023 11:12:44 +0200 Subject: [PATCH 12/15] tweak button texts --- .../field_filter_popover.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx index 1d3716036675d..707f75bf5f069 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx @@ -160,17 +160,17 @@ export const FieldFilterPopover: FC = ({ size="xs" flush="left" onClick={() => setFieldsFilter(filteredUniqueFieldNames, true)} - data-test-subj="aiopsFieldSelectorEnableAllSelectedButton" + data-test-subj="aiopsFieldSelectorSelectAllFieldsButton" > {fieldSearchText.length > 0 ? ( ) : ( )} @@ -180,17 +180,17 @@ export const FieldFilterPopover: FC = ({ size="xs" flush="right" onClick={() => setFieldsFilter(filteredUniqueFieldNames, false)} - data-test-subj="aiopsFieldSelectorDisableAllSelectedButton" + data-test-subj="aiopsFieldSelectorDeselectAllFieldsButton" > {fieldSearchText.length > 0 ? ( ) : ( )} From 8b9f7993e44226c2ce35d6ddb9fb986b3679ef6b Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 11 Apr 2023 12:41:41 +0200 Subject: [PATCH 13/15] Disable Apply button when form hadn't been touched. Disable select/deselect button if search returns no ofields --- .../explain_log_rate_spikes/field_filter_popover.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx index 707f75bf5f069..38d34983db1a3 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/field_filter_popover.tsx @@ -54,6 +54,7 @@ export const FieldFilterPopover: FC = ({ [euiThemeContext] ); + const [isTouched, setIsTouched] = useState(false); const [fieldSearchText, setFieldSearchText] = useState(''); const [skippedFields, setSkippedFields] = useState([]); const setFieldsFilter = (fieldNames: string[], checked: boolean) => { @@ -64,6 +65,7 @@ export const FieldFilterPopover: FC = ({ updatedSkippedFields = skippedFields.filter((d) => !fieldNames.includes(d)); } setSkippedFields(updatedSkippedFields); + setIsTouched(true); }; const [isFieldSelectionPopoverOpen, setIsFieldSelectionPopoverOpen] = useState(false); @@ -160,6 +162,7 @@ export const FieldFilterPopover: FC = ({ size="xs" flush="left" onClick={() => setFieldsFilter(filteredUniqueFieldNames, true)} + disabled={fieldSearchText.length > 0 && filteredUniqueFieldNames.length === 0} data-test-subj="aiopsFieldSelectorSelectAllFieldsButton" > {fieldSearchText.length > 0 ? ( @@ -180,6 +183,7 @@ export const FieldFilterPopover: FC = ({ size="xs" flush="right" onClick={() => setFieldsFilter(filteredUniqueFieldNames, false)} + disabled={fieldSearchText.length > 0 && filteredUniqueFieldNames.length === 0} data-test-subj="aiopsFieldSelectorDeselectAllFieldsButton" > {fieldSearchText.length > 0 ? ( @@ -204,7 +208,7 @@ export const FieldFilterPopover: FC = ({ setIsFieldSelectionPopoverOpen(false); closePopover(); }} - disabled={disabledApplyButton || selectedFieldCount < 2} + disabled={disabledApplyButton || selectedFieldCount < 2 || !isTouched} tooltipContent={ selectedFieldCount < 2 ? i18n.translate('xpack.aiops.analysis.fieldSelectorNotEnoughFieldsSelected', { From 45e900bface874d6e0262e6060c56a72e9559c64 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 11 Apr 2023 15:48:40 +0200 Subject: [PATCH 14/15] Fix to reset unique field names when analysis is rerun. --- .../explain_log_rate_spikes_analysis.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx index e235718769b79..d3fa5bc22b707 100644 --- a/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx +++ b/x-pack/plugins/aiops/public/components/explain_log_rate_spikes/explain_log_rate_spikes_analysis.tsx @@ -87,6 +87,7 @@ export const ExplainLogRateSpikesAnalysis: FC >(); const [groupResults, setGroupResults] = useState(false); const [groupSkipFields, setGroupSkipFields] = useState([]); + const [uniqueFieldNames, setUniqueFieldNames] = useState([]); const [overrides, setOverrides] = useState< ApiExplainLogRateSpikes['body']['overrides'] | undefined >(undefined); @@ -134,8 +135,8 @@ export const ExplainLogRateSpikesAnalysis: FC ); const { significantTerms } = data; - const uniqueFieldNames = useMemo( - () => uniq(significantTerms.map((d) => d.fieldName)).sort(), + useEffect( + () => setUniqueFieldNames(uniq(significantTerms.map((d) => d.fieldName)).sort()), [significantTerms] ); @@ -163,6 +164,7 @@ export const ExplainLogRateSpikesAnalysis: FC function startHandler(continueAnalysis = false, resetGroupButton = true) { if (!continueAnalysis) { setOverrides(undefined); + setUniqueFieldNames([]); } // Reset grouping to false and clear all row selections when restarting the analysis. From 767eb1cbc6771dfba8c876df9d90d6a1b4d86be1 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 11 Apr 2023 15:57:25 +0200 Subject: [PATCH 15/15] Fix data-test-subj --- .../functional/services/aiops/explain_log_rate_spikes_page.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts index 7b8abe75f13a9..3a921a74ee359 100644 --- a/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts +++ b/x-pack/test/functional/services/aiops/explain_log_rate_spikes_page.ts @@ -157,7 +157,7 @@ export function ExplainLogRateSpikesPageProvider({ getService }: FtrProviderCont async clickFieldSelectorDisableAllSelectedButton() { await testSubjects.clickWhenNotDisabledWithoutRetry( - 'aiopsFieldSelectorDisableAllSelectedButton' + 'aiopsFieldSelectorDeselectAllFieldsButton' ); await retry.tryForTime(30 * 1000, async () => {