From 19e97f35a731eb9c9252cfd0f7ec400172d2a126 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Wed, 22 Nov 2023 18:03:33 +0100 Subject: [PATCH] [ML] [AIOps] Log Rate Analysis: Adds support to restore baseline/deviation from url state on page refresh. (#171398) Support to restore baseline/deviation time ranges from url state on full page refresh. Also updates functional tests to include a full page refresh after the first analysis run for each dataset. --- .../ml/aiops_utils/window_parameters.ts | 20 ++---- .../url_state.ts => url_state/common.ts} | 18 ----- .../url_state/log_pattern_analysis.ts | 26 ++++++++ .../url_state/log_rate_analysis.ts | 66 +++++++++++++++++++ .../category_table/category_table.tsx | 2 +- .../log_categorization_for_flyout.tsx | 2 +- .../log_categorization_page.tsx | 2 +- .../log_categorization/use_discover_links.ts | 2 +- .../log_rate_analysis_content.tsx | 27 +++++++- .../log_rate_analysis_page.tsx | 40 +++++++---- .../plugins/aiops/public/hooks/use_search.ts | 2 +- .../apps/aiops/log_rate_analysis.ts | 7 ++ 12 files changed, 161 insertions(+), 53 deletions(-) rename x-pack/plugins/aiops/public/application/{utils/url_state.ts => url_state/common.ts} (71%) create mode 100644 x-pack/plugins/aiops/public/application/url_state/log_pattern_analysis.ts create mode 100644 x-pack/plugins/aiops/public/application/url_state/log_rate_analysis.ts diff --git a/x-pack/packages/ml/aiops_utils/window_parameters.ts b/x-pack/packages/ml/aiops_utils/window_parameters.ts index 806d224defdf4..91ecee5f841a2 100644 --- a/x-pack/packages/ml/aiops_utils/window_parameters.ts +++ b/x-pack/packages/ml/aiops_utils/window_parameters.ts @@ -15,25 +15,13 @@ import { isPopulatedObject } from '@kbn/ml-is-populated-object'; * @typedef {WindowParameters} */ export interface WindowParameters { - /** - * Baseline minimum value - * @type {number} - */ + /** Baseline minimum value */ baselineMin: number; - /** - * Baseline maximum value - * @type {number} - */ + /** Baseline maximum value */ baselineMax: number; - /** - * Deviation minimum value - * @type {number} - */ + /** Deviation minimum value */ deviationMin: number; - /** - * Deviation maximum value - * @type {number} - */ + /** Deviation maximum value */ deviationMax: number; } diff --git a/x-pack/plugins/aiops/public/application/utils/url_state.ts b/x-pack/plugins/aiops/public/application/url_state/common.ts similarity index 71% rename from x-pack/plugins/aiops/public/application/utils/url_state.ts rename to x-pack/plugins/aiops/public/application/url_state/common.ts index 22a32a3d610ed..8ca9ec848150c 100644 --- a/x-pack/plugins/aiops/public/application/utils/url_state.ts +++ b/x-pack/plugins/aiops/public/application/url_state/common.ts @@ -38,21 +38,3 @@ export const getDefaultAiOpsListState = ( filters: [], ...overrides, }); - -export interface LogCategorizationPageUrlState { - pageKey: 'logCategorization'; - pageUrlState: LogCategorizationAppState; -} - -export interface LogCategorizationAppState extends AiOpsFullIndexBasedAppState { - field: string | undefined; -} - -export const getDefaultLogCategorizationAppState = ( - overrides?: Partial -): LogCategorizationAppState => { - return { - field: undefined, - ...getDefaultAiOpsListState(overrides), - }; -}; diff --git a/x-pack/plugins/aiops/public/application/url_state/log_pattern_analysis.ts b/x-pack/plugins/aiops/public/application/url_state/log_pattern_analysis.ts new file mode 100644 index 0000000000000..7ea9e1a398e40 --- /dev/null +++ b/x-pack/plugins/aiops/public/application/url_state/log_pattern_analysis.ts @@ -0,0 +1,26 @@ +/* + * 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 { getDefaultAiOpsListState, type AiOpsFullIndexBasedAppState } from './common'; + +export interface LogCategorizationPageUrlState { + pageKey: 'logCategorization'; + pageUrlState: LogCategorizationAppState; +} + +export interface LogCategorizationAppState extends AiOpsFullIndexBasedAppState { + field: string | undefined; +} + +export const getDefaultLogCategorizationAppState = ( + overrides?: Partial +): LogCategorizationAppState => { + return { + field: undefined, + ...getDefaultAiOpsListState(overrides), + }; +}; diff --git a/x-pack/plugins/aiops/public/application/url_state/log_rate_analysis.ts b/x-pack/plugins/aiops/public/application/url_state/log_rate_analysis.ts new file mode 100644 index 0000000000000..f7691bc2b9d79 --- /dev/null +++ b/x-pack/plugins/aiops/public/application/url_state/log_rate_analysis.ts @@ -0,0 +1,66 @@ +/* + * 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 type { WindowParameters } from '@kbn/aiops-utils'; + +import { getDefaultAiOpsListState, type AiOpsFullIndexBasedAppState } from './common'; + +export interface LogRateAnalysisPageUrlState { + pageKey: 'logRateAnalysis'; + pageUrlState: LogRateAnalysisAppState; +} +/** + * To avoid long urls, we store the window parameters in the url state not with + * their full parameters names but with abbrevations. `windowParametersToAppState` and + * `appStateToWindowParameters` are used to transform the data structure. + */ +export interface LogRateAnalysisAppState extends AiOpsFullIndexBasedAppState { + /** Window parameters */ + wp?: { + /** Baseline minimum value */ + bMin: number; + /** Baseline maximum value */ + bMax: number; + /** Deviation minimum value */ + dMin: number; + /** Deviation maximum value */ + dMax: number; + }; +} + +/** + * Transforms a full window parameters object to the abbreviated url state version. + */ +export const windowParametersToAppState = (wp?: WindowParameters): LogRateAnalysisAppState['wp'] => + wp && { + bMin: wp.baselineMin, + bMax: wp.baselineMax, + dMin: wp.deviationMin, + dMax: wp.deviationMax, + }; + +/** + * Transforms an abbreviated url state version of window parameters to its full version. + */ +export const appStateToWindowParameters = ( + wp: LogRateAnalysisAppState['wp'] +): WindowParameters | undefined => + wp && { + baselineMin: wp.bMin, + baselineMax: wp.bMax, + deviationMin: wp.dMin, + deviationMax: wp.dMax, + }; + +export const getDefaultLogRateAnalysisAppState = ( + overrides?: Partial +): LogRateAnalysisAppState => { + return { + wp: undefined, + ...getDefaultAiOpsListState(overrides), + }; +}; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx b/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx index 3796af67f8cd2..05cca08d22227 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/category_table/category_table.tsx @@ -30,7 +30,7 @@ import type { } from '../../../../common/api/log_categorization/types'; import { useEuiTheme } from '../../../hooks/use_eui_theme'; -import type { LogCategorizationAppState } from '../../../application/utils/url_state'; +import type { LogCategorizationAppState } from '../../../application/url_state/log_pattern_analysis'; import { MiniHistogram } from '../../mini_histogram'; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_for_flyout.tsx b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_for_flyout.tsx index 5b37fd019c013..1f3eda09132eb 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_for_flyout.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_for_flyout.tsx @@ -29,7 +29,7 @@ import type { Category, SparkLinesPerCategory } from '../../../common/api/log_ca import { type LogCategorizationPageUrlState, getDefaultLogCategorizationAppState, -} from '../../application/utils/url_state'; +} from '../../application/url_state/log_pattern_analysis'; import { createMergedEsQuery } from '../../application/utils/search_utils'; import { useData } from '../../hooks/use_data'; import { useSearch } from '../../hooks/use_search'; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx index af3a636be27e8..b07a10a4924f1 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx +++ b/x-pack/plugins/aiops/public/components/log_categorization/log_categorization_page.tsx @@ -37,7 +37,7 @@ import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { getDefaultLogCategorizationAppState, type LogCategorizationPageUrlState, -} from '../../application/utils/url_state'; +} from '../../application/url_state/log_pattern_analysis'; import { SearchPanel } from '../search_panel'; import { PageHeader } from '../page_header'; diff --git a/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts b/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts index 22e8e50ebcf19..36d5eae310a52 100644 --- a/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts +++ b/x-pack/plugins/aiops/public/components/log_categorization/use_discover_links.ts @@ -15,7 +15,7 @@ import type { Filter } from '@kbn/es-query'; import { getCategoryQuery } from '../../../common/api/log_categorization/get_category_query'; import type { Category } from '../../../common/api/log_categorization/types'; -import type { AiOpsIndexBasedAppState } from '../../application/utils/url_state'; +import type { AiOpsIndexBasedAppState } from '../../application/url_state/common'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; export const QUERY_MODE = { diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_content/log_rate_analysis_content.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_content/log_rate_analysis_content.tsx index 0ce422a0fb872..51244a2300634 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_content/log_rate_analysis_content.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_content/log_rate_analysis_content.tsx @@ -6,7 +6,7 @@ */ import { isEqual } from 'lodash'; -import React, { useEffect, useMemo, useState, type FC } from 'react'; +import React, { useEffect, useMemo, useRef, useState, type FC } from 'react'; import { EuiEmptyPrompt, EuiHorizontalRule, EuiPanel } from '@elastic/eui'; import type { Moment } from 'moment'; @@ -76,6 +76,8 @@ export interface LogRateAnalysisContentProps { barHighlightColorOverride?: string; /** Optional callback that exposes data of the completed analysis */ onAnalysisCompleted?: (d: LogRateAnalysisResultsData) => void; + /** Optional callback that exposes current window parameters */ + onWindowParametersChange?: (wp?: WindowParameters) => void; /** Identifier to indicate the plugin utilizing the component */ embeddingOrigin: string; } @@ -90,6 +92,7 @@ export const LogRateAnalysisContent: FC = ({ barColorOverride, barHighlightColorOverride, onAnalysisCompleted, + onWindowParametersChange, embeddingOrigin, }) => { const [windowParameters, setWindowParameters] = useState(); @@ -105,6 +108,28 @@ export const LogRateAnalysisContent: FC = ({ setIsBrushCleared(windowParameters === undefined); }, [windowParameters]); + // Window parameters stored in the url state use this components + // `initialAnalysisStart` prop to set the initial params restore from url state. + // To avoid a loop with window parameters being passed around on load, + // the following ref and useEffect are used to check wether it's safe to call + // the `onWindowParametersChange` callback. + const windowParametersTouched = useRef(false); + useEffect(() => { + // Don't continue if window parameters were not touched yet. + // Because they can be reset to `undefined` at a later stage again when a user + // clears the selections, we cannot rely solely on checking if they are + // `undefined`, we need the additional ref to update on the first change. + if (!windowParametersTouched.current && windowParameters === undefined) { + return; + } + + windowParametersTouched.current = true; + + if (onWindowParametersChange) { + onWindowParametersChange(windowParameters); + } + }, [onWindowParametersChange, windowParameters]); + // Checks if `esSearchQuery` is the default empty query passed on from the search bar // and if that's the case fall back to a simpler match all query. const searchQuery = useMemo( diff --git a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_page.tsx b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_page.tsx index b3c9f256fb2ea..4931503b7366e 100644 --- a/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_page.tsx +++ b/x-pack/plugins/aiops/public/components/log_rate_analysis/log_rate_analysis_page.tsx @@ -6,22 +6,26 @@ */ import React, { useCallback, useEffect, useState, FC } from 'react'; +import { isEqual } from 'lodash'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { EuiFlexGroup, EuiFlexItem, EuiPageBody, EuiPageSection, EuiSpacer } from '@elastic/eui'; import { Filter, FilterStateStore, Query } from '@kbn/es-query'; import { useUrlState, usePageUrlState } from '@kbn/ml-url-state'; - import type { SearchQueryLanguage } from '@kbn/ml-query-utils'; +import type { WindowParameters } from '@kbn/aiops-utils'; + import { useDataSource } from '../../hooks/use_data_source'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; import { useData } from '../../hooks/use_data'; import { useSearch } from '../../hooks/use_search'; import { - getDefaultAiOpsListState, - type AiOpsPageUrlState, -} from '../../application/utils/url_state'; + getDefaultLogRateAnalysisAppState, + appStateToWindowParameters, + windowParametersToAppState, + type LogRateAnalysisPageUrlState, +} from '../../application/url_state/log_rate_analysis'; import { AIOPS_TELEMETRY_ID } from '../../../common/constants'; import { SearchPanel } from '../search_panel'; @@ -40,9 +44,9 @@ export const LogRateAnalysisPage: FC = ({ stickyHistogram }) => { const { currentSelectedSignificantItem, currentSelectedGroup } = useLogRateAnalysisResultsTableRowContext(); - const [aiopsListState, setAiopsListState] = usePageUrlState( - 'AIOPS_INDEX_VIEWER', - getDefaultAiOpsListState() + const [stateFromUrl, setUrlState] = usePageUrlState( + 'logRateAnalysis', + getDefaultLogRateAnalysisAppState() ); const [globalState, setGlobalState] = useUrlState('_g'); @@ -67,20 +71,20 @@ export const LogRateAnalysisPage: FC = ({ stickyHistogram }) => { setSelectedSavedSearch(null); } - setAiopsListState({ - ...aiopsListState, + setUrlState({ + ...stateFromUrl, searchQuery: searchParams.searchQuery, searchString: searchParams.searchString, searchQueryLanguage: searchParams.queryLanguage, filters: searchParams.filters, }); }, - [selectedSavedSearch, aiopsListState, setAiopsListState] + [selectedSavedSearch, stateFromUrl, setUrlState] ); const { searchQueryLanguage, searchString, searchQuery } = useSearch( { dataView, savedSearch }, - aiopsListState + stateFromUrl ); const { timefilter } = useData( @@ -132,6 +136,14 @@ export const LogRateAnalysisPage: FC = ({ stickyHistogram }) => { }); }, [dataService, searchQueryLanguage, searchString]); + const onWindowParametersHandler = (wp?: WindowParameters) => { + if (!isEqual(wp, stateFromUrl.wp)) { + setUrlState({ + wp: windowParametersToAppState(wp), + }); + } + }; + return ( @@ -148,11 +160,13 @@ export const LogRateAnalysisPage: FC = ({ stickyHistogram }) => { /> diff --git a/x-pack/plugins/aiops/public/hooks/use_search.ts b/x-pack/plugins/aiops/public/hooks/use_search.ts index 609beb6774bc9..8c62db36289fd 100644 --- a/x-pack/plugins/aiops/public/hooks/use_search.ts +++ b/x-pack/plugins/aiops/public/hooks/use_search.ts @@ -11,7 +11,7 @@ import type { DataView } from '@kbn/data-views-plugin/public'; import type { SavedSearch } from '@kbn/saved-search-plugin/public'; import { getEsQueryFromSavedSearch } from '../application/utils/search_utils'; -import type { AiOpsIndexBasedAppState } from '../application/utils/url_state'; +import type { AiOpsIndexBasedAppState } from '../application/url_state/common'; import { useAiopsAppContext } from './use_aiops_app_context'; export const useSearch = ( diff --git a/x-pack/test/functional/apps/aiops/log_rate_analysis.ts b/x-pack/test/functional/apps/aiops/log_rate_analysis.ts index 8e33b4b1c8e4a..c9551367bc5a9 100644 --- a/x-pack/test/functional/apps/aiops/log_rate_analysis.ts +++ b/x-pack/test/functional/apps/aiops/log_rate_analysis.ts @@ -15,6 +15,7 @@ import { logRateAnalysisTestData } from './log_rate_analysis_test_data'; export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'console', 'header', 'home', 'security']); + const browser = getService('browser'); const elasticChart = getService('elasticChart'); const aiops = getService('aiops'); @@ -147,6 +148,12 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await aiops.logRateAnalysisPage.clickRerunAnalysisButton(true); } + // Wait for the analysis to finish + await aiops.logRateAnalysisPage.assertAnalysisComplete(testData.analysisType); + + // At this stage the baseline and deviation brush position should be stored in + // the url state and a full browser refresh should restore the analysis. + await browser.refresh(); await aiops.logRateAnalysisPage.assertAnalysisComplete(testData.analysisType); // The group switch should be disabled by default