[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.
This commit is contained in:
Walter Rafelsberger 2023-11-22 18:03:33 +01:00 committed by GitHub
parent d5fc9b0314
commit 19e97f35a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 162 additions and 54 deletions

View file

@ -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;
}

View file

@ -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>
): LogCategorizationAppState => {
return {
field: undefined,
...getDefaultAiOpsListState(overrides),
};
};

View file

@ -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>
): LogCategorizationAppState => {
return {
field: undefined,
...getDefaultAiOpsListState(overrides),
};
};

View file

@ -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>
): LogRateAnalysisAppState => {
return {
wp: undefined,
...getDefaultAiOpsListState(overrides),
};
};

View file

@ -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';

View file

@ -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';

View file

@ -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';

View file

@ -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 = {

View file

@ -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<LogRateAnalysisContentProps> = ({
barColorOverride,
barHighlightColorOverride,
onAnalysisCompleted,
onWindowParametersChange,
embeddingOrigin,
}) => {
const [windowParameters, setWindowParameters] = useState<WindowParameters | undefined>();
@ -105,6 +108,28 @@ export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
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(

View file

@ -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<Props> = ({ stickyHistogram }) => {
const { currentSelectedSignificantItem, currentSelectedGroup } =
useLogRateAnalysisResultsTableRowContext();
const [aiopsListState, setAiopsListState] = usePageUrlState<AiOpsPageUrlState>(
'AIOPS_INDEX_VIEWER',
getDefaultAiOpsListState()
const [stateFromUrl, setUrlState] = usePageUrlState<LogRateAnalysisPageUrlState>(
'logRateAnalysis',
getDefaultLogRateAnalysisAppState()
);
const [globalState, setGlobalState] = useUrlState('_g');
@ -67,20 +71,20 @@ export const LogRateAnalysisPage: FC<Props> = ({ 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<Props> = ({ stickyHistogram }) => {
});
}, [dataService, searchQueryLanguage, searchString]);
const onWindowParametersHandler = (wp?: WindowParameters) => {
if (!isEqual(wp, stateFromUrl.wp)) {
setUrlState({
wp: windowParametersToAppState(wp),
});
}
};
return (
<EuiPageBody data-test-subj="aiopsLogRateAnalysisPage" paddingSize="none" panelled={false}>
<PageHeader />
@ -148,11 +160,13 @@ export const LogRateAnalysisPage: FC<Props> = ({ stickyHistogram }) => {
/>
</EuiFlexItem>
<LogRateAnalysisContent
initialAnalysisStart={appStateToWindowParameters(stateFromUrl.wp)}
dataView={dataView}
setGlobalState={setGlobalState}
esSearchQuery={searchQuery}
stickyHistogram={stickyHistogram}
embeddingOrigin={AIOPS_TELEMETRY_ID.AIOPS_DEFAULT_SOURCE}
esSearchQuery={searchQuery}
onWindowParametersChange={onWindowParametersHandler}
setGlobalState={setGlobalState}
stickyHistogram={stickyHistogram}
/>
</EuiFlexGroup>
</EuiPageSection>

View file

@ -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 = (

View file

@ -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