mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[ML] Log rate spike: search (#136571)
* add search bar with filters * create application directory and utils directory * incorporate cache fix and ensure search works * move search above table and fix type errors * refactor to minimize start bundle * update branch with latest changes and fix types * add comments to track duplicate code
This commit is contained in:
parent
d862f6feeb
commit
59082823ed
22 changed files with 678 additions and 135 deletions
|
@ -12,7 +12,9 @@
|
|||
"requiredPlugins": [
|
||||
"charts",
|
||||
"data",
|
||||
"licensing"
|
||||
"discover",
|
||||
"licensing",
|
||||
"unifiedSearch"
|
||||
],
|
||||
"optionalPlugins": [],
|
||||
"requiredBundles": ["kibanaReact", "fieldFormats"],
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// TODO Consolidate with near duplicate service in
|
||||
// `x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/time_field_range.ts`
|
||||
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { getCoreStart } from '../../kibana_services';
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// TODO Consolidate with near duplicate service in
|
||||
// `x-pack/plugins/data_visualizer/public/application/index_data_visualizer/services/timefilter_refresh_service.ts`
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export interface Refresh {
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// TODO Consolidate with duplicate error utils file in
|
||||
// `x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/error_utils.ts`
|
||||
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import Boom from '@hapi/boom';
|
||||
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
|
|
@ -5,6 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// TODO Consolidate with duplicate query utils in
|
||||
// `x-pack/plugins/data_visualizer/common/utils/query_utils.ts`
|
||||
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { Query } from '@kbn/es-query';
|
231
x-pack/plugins/aiops/public/application/utils/search_utils.ts
Normal file
231
x-pack/plugins/aiops/public/application/utils/search_utils.ts
Normal file
|
@ -0,0 +1,231 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// TODO Consolidate with duplicate saved search utils file in
|
||||
// `x-pack/plugins/data_visualizer/public/application/index_data_visualizer/utils/saved_search_utils.ts`
|
||||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { IUiSettingsClient } from '@kbn/core/public';
|
||||
import { SearchSource } from '@kbn/data-plugin/common';
|
||||
import { SavedSearch } from '@kbn/discover-plugin/public';
|
||||
import { FilterManager } from '@kbn/data-plugin/public';
|
||||
import {
|
||||
fromKueryExpression,
|
||||
toElasticsearchQuery,
|
||||
buildQueryFromFilters,
|
||||
buildEsQuery,
|
||||
Query,
|
||||
Filter,
|
||||
} from '@kbn/es-query';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { getEsQueryConfig } from '@kbn/data-plugin/common';
|
||||
import type { SimpleSavedObject } from '@kbn/core/public';
|
||||
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
|
||||
|
||||
const DEFAULT_QUERY = {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
match_all: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const SEARCH_QUERY_LANGUAGE = {
|
||||
KUERY: 'kuery',
|
||||
LUCENE: 'lucene',
|
||||
} as const;
|
||||
|
||||
export type SearchQueryLanguage = typeof SEARCH_QUERY_LANGUAGE[keyof typeof SEARCH_QUERY_LANGUAGE];
|
||||
|
||||
export function getDefaultQuery() {
|
||||
return cloneDeep(DEFAULT_QUERY);
|
||||
}
|
||||
|
||||
export type SavedSearchSavedObject = SimpleSavedObject<any>;
|
||||
|
||||
export function isSavedSearchSavedObject(arg: unknown): arg is SavedSearchSavedObject {
|
||||
return isPopulatedObject(arg, ['id', 'type', 'attributes']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the stringified searchSourceJSON
|
||||
* from a saved search or saved search object
|
||||
*/
|
||||
export function getQueryFromSavedSearchObject(savedSearch: SavedSearchSavedObject | SavedSearch) {
|
||||
const search = isSavedSearchSavedObject(savedSearch)
|
||||
? savedSearch?.attributes?.kibanaSavedObjectMeta
|
||||
: // @ts-ignore
|
||||
savedSearch?.kibanaSavedObjectMeta;
|
||||
|
||||
const parsed =
|
||||
typeof search?.searchSourceJSON === 'string'
|
||||
? (JSON.parse(search.searchSourceJSON) as {
|
||||
query: Query;
|
||||
filter: Filter[];
|
||||
})
|
||||
: undefined;
|
||||
|
||||
// Remove indexRefName because saved search might no longer be relevant
|
||||
// if user modifies the query or filter
|
||||
// after opening a saved search
|
||||
if (parsed && Array.isArray(parsed.filter)) {
|
||||
parsed.filter.forEach((f) => {
|
||||
// @ts-expect-error indexRefName does appear in meta for newly created saved search
|
||||
f.meta.indexRefName = undefined;
|
||||
});
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Elasticsearch query that combines both lucene/kql query string and filters
|
||||
* Should also form a valid query if only the query or filters is provided
|
||||
*/
|
||||
export function createMergedEsQuery(
|
||||
query?: Query,
|
||||
filters?: Filter[],
|
||||
dataView?: DataView,
|
||||
uiSettings?: IUiSettingsClient
|
||||
) {
|
||||
let combinedQuery: QueryDslQueryContainer = getDefaultQuery();
|
||||
|
||||
if (query && query.language === SEARCH_QUERY_LANGUAGE.KUERY) {
|
||||
const ast = fromKueryExpression(query.query);
|
||||
if (query.query !== '') {
|
||||
combinedQuery = toElasticsearchQuery(ast, dataView);
|
||||
}
|
||||
if (combinedQuery.bool !== undefined) {
|
||||
const filterQuery = buildQueryFromFilters(filters, dataView);
|
||||
|
||||
if (!Array.isArray(combinedQuery.bool.filter)) {
|
||||
combinedQuery.bool.filter =
|
||||
combinedQuery.bool.filter === undefined ? [] : [combinedQuery.bool.filter];
|
||||
}
|
||||
|
||||
if (!Array.isArray(combinedQuery.bool.must_not)) {
|
||||
combinedQuery.bool.must_not =
|
||||
combinedQuery.bool.must_not === undefined ? [] : [combinedQuery.bool.must_not];
|
||||
}
|
||||
|
||||
combinedQuery.bool.filter = [...combinedQuery.bool.filter, ...filterQuery.filter];
|
||||
combinedQuery.bool.must_not = [...combinedQuery.bool.must_not, ...filterQuery.must_not];
|
||||
}
|
||||
} else {
|
||||
combinedQuery = buildEsQuery(
|
||||
dataView,
|
||||
query ? [query] : [],
|
||||
filters ? filters : [],
|
||||
uiSettings ? getEsQueryConfig(uiSettings) : undefined
|
||||
);
|
||||
}
|
||||
return combinedQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract query data from the saved search object
|
||||
* with overrides from the provided query data and/or filters
|
||||
*/
|
||||
export function getEsQueryFromSavedSearch({
|
||||
dataView,
|
||||
uiSettings,
|
||||
savedSearch,
|
||||
query,
|
||||
filters,
|
||||
filterManager,
|
||||
}: {
|
||||
dataView: DataView;
|
||||
uiSettings: IUiSettingsClient;
|
||||
savedSearch: SavedSearchSavedObject | SavedSearch | null | undefined;
|
||||
query?: Query;
|
||||
filters?: Filter[];
|
||||
filterManager?: FilterManager;
|
||||
}) {
|
||||
if (!dataView || !savedSearch) return;
|
||||
|
||||
const userQuery = query;
|
||||
const userFilters = filters;
|
||||
|
||||
// If saved search has a search source with nested parent
|
||||
// e.g. a search coming from Dashboard saved search embeddable
|
||||
// which already combines both the saved search's original query/filters and the Dashboard's
|
||||
// then no need to process any further
|
||||
if (
|
||||
savedSearch &&
|
||||
'searchSource' in savedSearch &&
|
||||
savedSearch?.searchSource instanceof SearchSource &&
|
||||
savedSearch.searchSource.getParent() !== undefined &&
|
||||
userQuery
|
||||
) {
|
||||
// Flattened query from search source may contain a clause that narrows the time range
|
||||
// which might interfere with global time pickers so we need to remove
|
||||
const savedQuery =
|
||||
cloneDeep(savedSearch.searchSource.getSearchRequestBody()?.query) ?? getDefaultQuery();
|
||||
const timeField = savedSearch.searchSource.getField('index')?.timeFieldName;
|
||||
|
||||
if (Array.isArray(savedQuery.bool.filter) && timeField !== undefined) {
|
||||
savedQuery.bool.filter = savedQuery.bool.filter.filter(
|
||||
(c: QueryDslQueryContainer) =>
|
||||
!(c.hasOwnProperty('range') && c.range?.hasOwnProperty(timeField))
|
||||
);
|
||||
}
|
||||
return {
|
||||
searchQuery: savedQuery,
|
||||
searchString: userQuery.query,
|
||||
queryLanguage: userQuery.language as SearchQueryLanguage,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: support saved search
|
||||
// If saved search is an json object with the original query and filter
|
||||
// retrieve the parsed query and filter
|
||||
const savedSearchData = undefined; // getQueryFromSavedSearchObject(savedSearch);
|
||||
|
||||
// If no saved search available, use user's query and filters
|
||||
if (!savedSearchData && userQuery) {
|
||||
if (filterManager && userFilters) filterManager.setFilters(userFilters);
|
||||
|
||||
const combinedQuery = createMergedEsQuery(
|
||||
userQuery,
|
||||
Array.isArray(userFilters) ? userFilters : [],
|
||||
dataView,
|
||||
uiSettings
|
||||
);
|
||||
|
||||
return {
|
||||
searchQuery: combinedQuery,
|
||||
searchString: userQuery.query,
|
||||
queryLanguage: userQuery.language as SearchQueryLanguage,
|
||||
};
|
||||
}
|
||||
|
||||
// If saved search available, merge saved search with latest user query or filters
|
||||
// which might differ from extracted saved search data
|
||||
if (savedSearchData) {
|
||||
// @ts-ignore property does not exist on type never
|
||||
const currentQuery = userQuery ?? savedSearchData?.query;
|
||||
// @ts-ignore property does not exist on type never
|
||||
const currentFilters = userFilters ?? savedSearchData?.filter;
|
||||
|
||||
if (filterManager) filterManager.setFilters(currentFilters);
|
||||
|
||||
const combinedQuery = createMergedEsQuery(
|
||||
currentQuery,
|
||||
Array.isArray(currentFilters) ? currentFilters : [],
|
||||
dataView,
|
||||
uiSettings
|
||||
);
|
||||
|
||||
return {
|
||||
searchQuery: combinedQuery,
|
||||
searchString: currentQuery.query,
|
||||
queryLanguage: currentQuery.language as SearchQueryLanguage,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -5,6 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// TODO Consolidate with duplicate component `DatePickerWrapper` in
|
||||
// `x-pack/plugins/data_visualizer/public/application/common/components/date_picker_wrapper/date_picker_wrapper.tsx`
|
||||
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { debounce } from 'lodash';
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// TODO Consolidate with duplicate component `TotalCountHeader` in
|
||||
// `x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/total_count_header.tsx`
|
||||
|
||||
import { EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React from 'react';
|
||||
|
|
|
@ -5,17 +5,36 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, FC } from 'react';
|
||||
import React, { useCallback, useEffect, useState, FC } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiPageContentBody,
|
||||
EuiPageContentHeader,
|
||||
EuiPageContentHeaderSection,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { ProgressControls } from '@kbn/aiops-components';
|
||||
import { useFetchStream } from '@kbn/aiops-utils';
|
||||
import type { WindowParameters } from '@kbn/aiops-utils';
|
||||
import { Filter, Query } from '@kbn/es-query';
|
||||
|
||||
import { useAiOpsKibana } from '../../kibana_context';
|
||||
import { initialState, streamReducer } from '../../../common/api/stream_reducer';
|
||||
import type { ApiExplainLogRateSpikes } from '../../../common/api';
|
||||
import { SearchQueryLanguage } from '../../application/utils/search_utils';
|
||||
import { useUrlState, usePageUrlState, AppStateKey } from '../../hooks/url_state';
|
||||
import { useData } from '../../hooks/use_data';
|
||||
import { SpikeAnalysisTable } from '../spike_analysis_table';
|
||||
import { restorableDefaults } from './explain_log_rate_spikes_wrapper';
|
||||
import { FullTimeRangeSelector } from '../full_time_range_selector';
|
||||
import { DocumentCountContent } from '../document_count_content/document_count_content';
|
||||
import { DatePickerWrapper } from '../date_picker_wrapper';
|
||||
import { SearchPanel } from '../search_panel';
|
||||
|
||||
/**
|
||||
* ExplainLogRateSpikes props require a data view.
|
||||
|
@ -23,22 +42,72 @@ import { SpikeAnalysisTable } from '../spike_analysis_table';
|
|||
interface ExplainLogRateSpikesProps {
|
||||
/** The data view to analyze. */
|
||||
dataView: DataView;
|
||||
/** Start timestamp filter */
|
||||
earliest: number;
|
||||
/** End timestamp filter */
|
||||
latest: number;
|
||||
/** Window parameters for the analysis */
|
||||
windowParameters: WindowParameters;
|
||||
}
|
||||
|
||||
export const ExplainLogRateSpikes: FC<ExplainLogRateSpikesProps> = ({
|
||||
dataView,
|
||||
earliest,
|
||||
latest,
|
||||
windowParameters,
|
||||
}) => {
|
||||
export const ExplainLogRateSpikes: FC<ExplainLogRateSpikesProps> = ({ dataView }) => {
|
||||
const { services } = useAiOpsKibana();
|
||||
const basePath = services.http?.basePath.get() ?? '';
|
||||
const { http, data: dataService } = services;
|
||||
const basePath = http?.basePath.get() ?? '';
|
||||
|
||||
const [aiopsListState, setAiopsListState] = usePageUrlState(AppStateKey, restorableDefaults);
|
||||
const [globalState, setGlobalState] = useUrlState('_g');
|
||||
|
||||
const setSearchParams = useCallback(
|
||||
(searchParams: {
|
||||
searchQuery: Query['query'];
|
||||
searchString: Query['query'];
|
||||
queryLanguage: SearchQueryLanguage;
|
||||
filters: Filter[];
|
||||
}) => {
|
||||
setAiopsListState({
|
||||
...aiopsListState,
|
||||
searchQuery: searchParams.searchQuery,
|
||||
searchString: searchParams.searchString,
|
||||
searchQueryLanguage: searchParams.queryLanguage,
|
||||
filters: searchParams.filters,
|
||||
});
|
||||
},
|
||||
[aiopsListState, setAiopsListState]
|
||||
);
|
||||
|
||||
const { docStats, timefilter, earliest, latest, searchQueryLanguage, searchString, searchQuery } =
|
||||
useData(dataView, aiopsListState, setGlobalState);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// When navigating away from the index pattern
|
||||
// Reset all previously set filters
|
||||
// to make sure new page doesn't have unrelated filters
|
||||
dataService.query.filterManager.removeAll();
|
||||
};
|
||||
}, [dataView.id, dataService.query.filterManager]);
|
||||
|
||||
const [windowParameters, setWindowParameters] = useState<WindowParameters | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (globalState?.time !== undefined) {
|
||||
timefilter.setTime({
|
||||
from: globalState.time.from,
|
||||
to: globalState.time.to,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(globalState?.time), timefilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (globalState?.refreshInterval !== undefined) {
|
||||
timefilter.setRefreshInterval(globalState.refreshInterval);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(globalState?.refreshInterval), timefilter]);
|
||||
|
||||
useEffect(() => {
|
||||
// Update data query manager if input string is updated
|
||||
dataService?.query.queryString.setQuery({
|
||||
query: searchString,
|
||||
language: searchQueryLanguage,
|
||||
});
|
||||
}, [dataService, searchQueryLanguage, searchString]);
|
||||
|
||||
const { cancel, start, data, isRunning, error } = useFetchStream<
|
||||
ApiExplainLogRateSpikes,
|
||||
|
@ -46,7 +115,9 @@ export const ExplainLogRateSpikes: FC<ExplainLogRateSpikesProps> = ({
|
|||
>(
|
||||
`${basePath}/internal/aiops/explain_log_rate_spikes`,
|
||||
{
|
||||
// @ts-ignore unexpected type
|
||||
start: earliest,
|
||||
// @ts-ignore unexpected type
|
||||
end: latest,
|
||||
// TODO Consider an optional Kuery.
|
||||
kuery: '',
|
||||
|
@ -65,16 +136,82 @@ export const ExplainLogRateSpikes: FC<ExplainLogRateSpikesProps> = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
<ProgressControls
|
||||
progress={data.loaded}
|
||||
progressMessage={data.loadingState ?? ''}
|
||||
isRunning={isRunning}
|
||||
onRefresh={start}
|
||||
onCancel={cancel}
|
||||
/>
|
||||
{data?.changePoints ? (
|
||||
<SpikeAnalysisTable changePointData={data.changePoints} loading={isRunning} error={error} />
|
||||
) : null}
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiPageContentHeader className="aiopsPageHeader">
|
||||
<EuiPageContentHeaderSection>
|
||||
<div className="aiopsTitleHeader">
|
||||
<EuiTitle size={'s'}>
|
||||
<h2>{dataView.title}</h2>
|
||||
</EuiTitle>
|
||||
</div>
|
||||
</EuiPageContentHeaderSection>
|
||||
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
gutterSize="s"
|
||||
data-test-subj="aiopsTimeRangeSelectorSection"
|
||||
>
|
||||
{dataView.timeFieldName !== undefined && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<FullTimeRangeSelector
|
||||
dataView={dataView}
|
||||
query={undefined}
|
||||
disabled={false}
|
||||
timefilter={timefilter}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<DatePickerWrapper />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageContentHeader>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule />
|
||||
<EuiPageContentBody>
|
||||
<EuiFlexGroup gutterSize="m" direction="column">
|
||||
<EuiFlexItem>
|
||||
<SearchPanel
|
||||
dataView={dataView}
|
||||
searchString={searchString}
|
||||
searchQuery={searchQuery}
|
||||
searchQueryLanguage={searchQueryLanguage}
|
||||
setSearchParams={setSearchParams}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{docStats?.totalCount !== undefined && (
|
||||
<EuiFlexItem>
|
||||
<DocumentCountContent
|
||||
brushSelectionUpdateHandler={setWindowParameters}
|
||||
documentCountStats={docStats.documentCountStats}
|
||||
totalCount={docStats.totalCount}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiSpacer size="m" />
|
||||
{earliest !== undefined && latest !== undefined && windowParameters !== undefined && (
|
||||
<EuiFlexItem>
|
||||
<ProgressControls
|
||||
progress={data.loaded}
|
||||
progressMessage={data.loadingState ?? ''}
|
||||
isRunning={isRunning}
|
||||
onRefresh={start}
|
||||
onCancel={cancel}
|
||||
/>
|
||||
{data?.changePoints ? (
|
||||
<SpikeAnalysisTable
|
||||
changePointData={data.changePoints}
|
||||
loading={isRunning}
|
||||
error={error}
|
||||
/>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiPageContentBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,27 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
import React, { FC, useCallback, useEffect } from 'react';
|
||||
import { Filter, Query } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { parse, stringify } from 'query-string';
|
||||
import { isEqual } from 'lodash';
|
||||
import { encode } from 'rison-node';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiPageBody,
|
||||
EuiPageContentBody,
|
||||
EuiPageContentHeader,
|
||||
EuiPageContentHeaderSection,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import type { WindowParameters } from '@kbn/aiops-utils';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { EuiPageBody } from '@elastic/eui';
|
||||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
|
||||
import { ExplainLogRateSpikes } from './explain_log_rate_spikes';
|
||||
import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../../application/utils/search_utils';
|
||||
import { useAiOpsKibana } from '../../kibana_context';
|
||||
import {
|
||||
Accessor,
|
||||
Dictionary,
|
||||
|
@ -35,46 +28,60 @@ import {
|
|||
getNestedProperty,
|
||||
SetUrlState,
|
||||
} from '../../hooks/url_state';
|
||||
import { useData } from '../../hooks/use_data';
|
||||
import { useUrlState } from '../../hooks/url_state';
|
||||
|
||||
import { FullTimeRangeSelector } from '../full_time_range_selector';
|
||||
import { DocumentCountContent } from '../document_count_content/document_count_content';
|
||||
import { DatePickerWrapper } from '../date_picker_wrapper';
|
||||
|
||||
import { ExplainLogRateSpikes } from './explain_log_rate_spikes';
|
||||
|
||||
export interface ExplainLogRateSpikesWrapperProps {
|
||||
/** The data view to analyze. */
|
||||
dataView: DataView;
|
||||
}
|
||||
|
||||
const defaultSearchQuery = {
|
||||
match_all: {},
|
||||
};
|
||||
|
||||
export interface AiOpsIndexBasedAppState {
|
||||
searchString?: Query['query'];
|
||||
searchQuery?: Query['query'];
|
||||
searchQueryLanguage: SearchQueryLanguage;
|
||||
filters?: Filter[];
|
||||
}
|
||||
|
||||
export const getDefaultAiOpsListState = (
|
||||
overrides?: Partial<AiOpsIndexBasedAppState>
|
||||
): Required<AiOpsIndexBasedAppState> => ({
|
||||
searchString: '',
|
||||
searchQuery: defaultSearchQuery,
|
||||
searchQueryLanguage: SEARCH_QUERY_LANGUAGE.KUERY,
|
||||
filters: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const restorableDefaults = getDefaultAiOpsListState();
|
||||
|
||||
export const ExplainLogRateSpikesWrapper: FC<ExplainLogRateSpikesWrapperProps> = ({ dataView }) => {
|
||||
const [globalState, setGlobalState] = useUrlState('_g');
|
||||
|
||||
const { docStats, timefilter, earliest, latest } = useData(dataView, setGlobalState);
|
||||
const [windowParameters, setWindowParameters] = useState<WindowParameters | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
if (globalState?.time !== undefined) {
|
||||
timefilter.setTime({
|
||||
from: globalState.time.from,
|
||||
to: globalState.time.to,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(globalState?.time), timefilter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (globalState?.refreshInterval !== undefined) {
|
||||
timefilter.setRefreshInterval(globalState.refreshInterval);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(globalState?.refreshInterval), timefilter]);
|
||||
const { services } = useAiOpsKibana();
|
||||
const { notifications } = services;
|
||||
const { toasts } = notifications;
|
||||
|
||||
const history = useHistory();
|
||||
const { search: urlSearchString } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!dataView.isTimeBased()) {
|
||||
toasts.addWarning({
|
||||
title: i18n.translate('xpack.aiops.index.dataViewNotBasedOnTimeSeriesNotificationTitle', {
|
||||
defaultMessage: 'The data view {dataViewTitle} is not based on a time series',
|
||||
values: { dataViewTitle: dataView.title },
|
||||
}),
|
||||
text: i18n.translate(
|
||||
'xpack.aiops.index.dataViewNotBasedOnTimeSeriesNotificationDescription',
|
||||
{
|
||||
defaultMessage: 'Log rate spike analysis only runs over time-based indices',
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
}, [dataView, toasts]);
|
||||
|
||||
const setUrlState: SetUrlState = useCallback(
|
||||
(
|
||||
accessor: Accessor,
|
||||
|
@ -137,64 +144,12 @@ export const ExplainLogRateSpikesWrapper: FC<ExplainLogRateSpikesWrapperProps> =
|
|||
[history, urlSearchString]
|
||||
);
|
||||
|
||||
if (!dataView || !timefilter) return null;
|
||||
if (!dataView) return null;
|
||||
|
||||
return (
|
||||
<UrlStateContextProvider value={{ searchString: urlSearchString, setUrlState }}>
|
||||
<EuiPageBody data-test-subj="aiopsIndexPage" paddingSize="none" panelled={false}>
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
<EuiFlexItem>
|
||||
<EuiPageContentHeader className="aiopsPageHeader">
|
||||
<EuiPageContentHeaderSection>
|
||||
<div className="aiopsTitleHeader">
|
||||
<EuiTitle size={'s'}>
|
||||
<h2>{dataView.title}</h2>
|
||||
</EuiTitle>
|
||||
</div>
|
||||
</EuiPageContentHeaderSection>
|
||||
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
gutterSize="s"
|
||||
data-test-subj="aiopsTimeRangeSelectorSection"
|
||||
>
|
||||
{dataView.timeFieldName !== undefined && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<FullTimeRangeSelector
|
||||
dataView={dataView}
|
||||
query={undefined}
|
||||
disabled={false}
|
||||
timefilter={timefilter}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<DatePickerWrapper />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageContentHeader>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiHorizontalRule />
|
||||
<EuiPageContentBody>
|
||||
{docStats?.totalCount !== undefined && (
|
||||
<DocumentCountContent
|
||||
brushSelectionUpdateHandler={setWindowParameters}
|
||||
documentCountStats={docStats.documentCountStats}
|
||||
totalCount={docStats.totalCount}
|
||||
/>
|
||||
)}
|
||||
<EuiSpacer size="m" />
|
||||
{earliest !== undefined && latest !== undefined && windowParameters !== undefined && (
|
||||
<ExplainLogRateSpikes
|
||||
dataView={dataView}
|
||||
earliest={earliest}
|
||||
latest={latest}
|
||||
windowParameters={windowParameters}
|
||||
/>
|
||||
)}
|
||||
</EuiPageContentBody>
|
||||
<ExplainLogRateSpikes dataView={dataView} />
|
||||
</EuiPageBody>
|
||||
</UrlStateContextProvider>
|
||||
);
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// TODO Consolidate with near duplicate component `FullTimeRangeSelector` in
|
||||
// `x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector.tsx`
|
||||
|
||||
import React, { FC, useCallback, useMemo, useState } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// TODO Consolidate with duplicate service in
|
||||
// `x-pack/plugins/data_visualizer/public/application/index_data_visualizer/components/full_time_range_selector/full_time_range_selector_service.ts`
|
||||
|
||||
import moment from 'moment';
|
||||
import { TimefilterContract } from '@kbn/data-plugin/public';
|
||||
import dateMath from '@kbn/datemath';
|
||||
|
@ -14,7 +17,7 @@ import type { ToastsStart } from '@kbn/core/public';
|
|||
import { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
|
||||
import { getTimeFieldRange } from '../../application/services/time_field_range';
|
||||
import { addExcludeFrozenToQuery } from '../../query_utils';
|
||||
import { addExcludeFrozenToQuery } from '../../application/utils/query_utils';
|
||||
|
||||
export interface GetTimeFieldRangeResponse {
|
||||
success: boolean;
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { SearchPanel } from './search_panel';
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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, { FC, useEffect, useState } from 'react';
|
||||
import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Query, Filter } from '@kbn/es-query';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
|
||||
import { SearchQueryLanguage } from '../../application/utils/search_utils';
|
||||
import { useAiOpsKibana } from '../../kibana_context';
|
||||
import { getPluginsStart } from '../../kibana_services';
|
||||
import { createMergedEsQuery } from '../../application/utils/search_utils';
|
||||
interface Props {
|
||||
dataView: DataView;
|
||||
searchString: Query['query'];
|
||||
searchQuery: Query['query'];
|
||||
searchQueryLanguage: SearchQueryLanguage;
|
||||
setSearchParams({
|
||||
searchQuery,
|
||||
searchString,
|
||||
queryLanguage,
|
||||
filters,
|
||||
}: {
|
||||
searchQuery: Query['query'];
|
||||
searchString: Query['query'];
|
||||
queryLanguage: SearchQueryLanguage;
|
||||
filters: Filter[];
|
||||
}): void;
|
||||
onAddFilter?: (field: DataViewField | string, value: string, type: '+' | '-') => void;
|
||||
}
|
||||
|
||||
export const SearchPanel: FC<Props> = ({
|
||||
dataView,
|
||||
searchString,
|
||||
searchQueryLanguage,
|
||||
setSearchParams,
|
||||
}) => {
|
||||
const { unifiedSearch } = getPluginsStart();
|
||||
const { SearchBar } = unifiedSearch?.ui;
|
||||
const {
|
||||
services: {
|
||||
uiSettings,
|
||||
notifications: { toasts },
|
||||
data: { query: queryManager },
|
||||
},
|
||||
} = useAiOpsKibana();
|
||||
// The internal state of the input query bar updated on every key stroke.
|
||||
const [searchInput, setSearchInput] = useState<Query>({
|
||||
query: searchString || '',
|
||||
language: searchQueryLanguage,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setSearchInput({
|
||||
query: searchString || '',
|
||||
language: searchQueryLanguage,
|
||||
});
|
||||
}, [searchQueryLanguage, searchString, queryManager.filterManager]);
|
||||
|
||||
const searchHandler = ({ query, filters }: { query?: Query; filters?: Filter[] }) => {
|
||||
const mergedQuery = query ?? searchInput;
|
||||
const mergedFilters = filters ?? queryManager.filterManager.getFilters();
|
||||
try {
|
||||
if (mergedFilters) {
|
||||
queryManager.filterManager.setFilters(mergedFilters);
|
||||
}
|
||||
|
||||
const combinedQuery = createMergedEsQuery(
|
||||
mergedQuery,
|
||||
queryManager.filterManager.getFilters() ?? [],
|
||||
dataView,
|
||||
uiSettings
|
||||
);
|
||||
|
||||
setSearchParams({
|
||||
searchQuery: combinedQuery,
|
||||
searchString: mergedQuery.query,
|
||||
queryLanguage: mergedQuery.language as SearchQueryLanguage,
|
||||
filters: mergedFilters,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log('Invalid syntax', JSON.stringify(e, null, 2)); // eslint-disable-line no-console
|
||||
toasts.addError(e, {
|
||||
title: i18n.translate('xpack.aiops.searchPanel.invalidSyntax', {
|
||||
defaultMessage: 'Invalid syntax',
|
||||
}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
data-test-subj="aiopsSearchPanel"
|
||||
className={'aiopsSearchPanel__container'}
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={9} className={'aiopsSearchBar'}>
|
||||
<SearchBar
|
||||
dataTestSubj="aiopsQueryInput"
|
||||
appName={'aiops'}
|
||||
showFilterBar={true}
|
||||
showDatePicker={false}
|
||||
showQueryInput={true}
|
||||
query={searchInput}
|
||||
onQuerySubmit={(params: { dateRange: TimeRange; query?: Query | undefined }) =>
|
||||
searchHandler({ query: params.query })
|
||||
}
|
||||
indexPatterns={[dataView]}
|
||||
placeholder={i18n.translate('xpack.aiops.searchPanel.queryBarPlaceholderText', {
|
||||
defaultMessage: 'Search… (e.g. status:200 AND extension:"PHP")',
|
||||
})}
|
||||
displayStyle={'inPage'}
|
||||
isClearable={true}
|
||||
customSubmitButton={<div />}
|
||||
// @ts-expect-error onFiltersUpdated is a valid prop on SearchBar
|
||||
onFiltersUpdated={(filters: Filter[]) => searchHandler({ filters })}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -8,7 +8,8 @@
|
|||
import { each, get } from 'lodash';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
|
||||
import { buildBaseFilterCriteria } from './query_utils';
|
||||
import { Query } from '@kbn/es-query';
|
||||
import { buildBaseFilterCriteria } from './application/utils/query_utils';
|
||||
|
||||
export interface DocumentCountStats {
|
||||
interval?: number;
|
||||
|
@ -23,6 +24,7 @@ export interface DocumentStatsSearchStrategyParams {
|
|||
latest?: number;
|
||||
intervalMs?: number;
|
||||
index: string;
|
||||
searchQuery: Query['query'];
|
||||
timeFieldName?: string;
|
||||
runtimeFieldMap?: estypes.MappingRuntimeFields;
|
||||
fieldsToFetch?: string[];
|
||||
|
@ -35,15 +37,13 @@ export const getDocumentCountStatsRequest = (params: DocumentStatsSearchStrategy
|
|||
earliest: earliestMs,
|
||||
latest: latestMs,
|
||||
runtimeFieldMap,
|
||||
// searchQuery,
|
||||
searchQuery,
|
||||
intervalMs,
|
||||
fieldsToFetch,
|
||||
} = params;
|
||||
|
||||
const size = 0;
|
||||
const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, {
|
||||
match_all: {},
|
||||
});
|
||||
const filterCriteria = buildBaseFilterCriteria(timeFieldName, earliestMs, latestMs, searchQuery);
|
||||
|
||||
// Don't use the sampler aggregation as this can lead to some potentially
|
||||
// confusing date histogram results depending on the date range of data amongst shards.
|
||||
|
|
|
@ -16,9 +16,12 @@ import { TimeBuckets } from '../../common/time_buckets';
|
|||
import { useDocumentCountStats } from './use_document_count_stats';
|
||||
import { Dictionary } from './url_state';
|
||||
import { DocumentStatsSearchStrategyParams } from '../get_document_stats';
|
||||
import { getEsQueryFromSavedSearch } from '../application/utils/search_utils';
|
||||
import { AiOpsIndexBasedAppState } from '../components/explain_log_rate_spikes/explain_log_rate_spikes_wrapper';
|
||||
|
||||
export const useData = (
|
||||
currentDataView: DataView,
|
||||
aiopsListState: AiOpsIndexBasedAppState,
|
||||
onUpdate: (params: Dictionary<unknown>) => void
|
||||
) => {
|
||||
const { services } = useAiOpsKibana();
|
||||
|
@ -28,6 +31,42 @@ export const useData = (
|
|||
DocumentStatsSearchStrategyParams | undefined
|
||||
>();
|
||||
|
||||
/** Prepare required params to pass to search strategy **/
|
||||
const { searchQueryLanguage, searchString, searchQuery } = useMemo(() => {
|
||||
const searchData = getEsQueryFromSavedSearch({
|
||||
dataView: currentDataView,
|
||||
uiSettings,
|
||||
savedSearch: undefined,
|
||||
});
|
||||
|
||||
if (searchData === undefined || aiopsListState.searchString !== '') {
|
||||
if (aiopsListState.filters) {
|
||||
services.data.query.filterManager.setFilters(aiopsListState.filters);
|
||||
}
|
||||
return {
|
||||
searchQuery: aiopsListState.searchQuery,
|
||||
searchString: aiopsListState.searchString,
|
||||
searchQueryLanguage: aiopsListState.searchQueryLanguage,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
searchQuery: searchData.searchQuery,
|
||||
searchString: searchData.searchString,
|
||||
searchQueryLanguage: searchData.queryLanguage,
|
||||
};
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
currentDataView.id,
|
||||
aiopsListState.searchString,
|
||||
aiopsListState.searchQueryLanguage,
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
JSON.stringify({
|
||||
searchQuery: aiopsListState.searchQuery,
|
||||
}),
|
||||
lastRefresh,
|
||||
]);
|
||||
|
||||
const _timeBuckets = useMemo(() => {
|
||||
return new TimeBuckets({
|
||||
[UI_SETTINGS.HISTOGRAM_MAX_BARS]: uiSettings.get(UI_SETTINGS.HISTOGRAM_MAX_BARS),
|
||||
|
@ -56,6 +95,7 @@ export const useData = (
|
|||
latest: timefilterActiveBounds.max?.valueOf(),
|
||||
intervalMs: _timeBuckets.getInterval()?.asMilliseconds(),
|
||||
index: currentDataView.title,
|
||||
searchQuery,
|
||||
timeFieldName: currentDataView.timeFieldName,
|
||||
runtimeFieldMap: currentDataView.getRuntimeMappings(),
|
||||
});
|
||||
|
@ -94,10 +134,21 @@ export const useData = (
|
|||
};
|
||||
});
|
||||
|
||||
// Ensure request is updated when search changes
|
||||
useEffect(() => {
|
||||
updateFieldStatsRequest();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [searchString, JSON.stringify(searchQuery)]);
|
||||
|
||||
return {
|
||||
docStats,
|
||||
timefilter,
|
||||
/** Start timestamp filter */
|
||||
earliest: fieldStatsRequest?.earliest,
|
||||
/** End timestamp filter */
|
||||
latest: fieldStatsRequest?.latest,
|
||||
searchQueryLanguage,
|
||||
searchString,
|
||||
searchQuery,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -10,7 +10,7 @@ import { lastValueFrom } from 'rxjs';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import type { ToastsStart } from '@kbn/core/public';
|
||||
import { useAiOpsKibana } from '../kibana_context';
|
||||
import { extractErrorProperties } from '../../common/error_utils';
|
||||
import { extractErrorProperties } from '../application/utils/error_utils';
|
||||
import {
|
||||
DocumentCountStats,
|
||||
getDocumentCountStatsRequest,
|
||||
|
@ -70,7 +70,7 @@ export function useDocumentCountStats<TParams extends DocumentStatsSearchStrateg
|
|||
if (!searchParams) return;
|
||||
|
||||
try {
|
||||
const resp: any = await lastValueFrom(
|
||||
const resp = await lastValueFrom(
|
||||
data.search.search({
|
||||
params: getDocumentCountStatsRequest(searchParams),
|
||||
})
|
||||
|
|
|
@ -6,10 +6,14 @@
|
|||
*/
|
||||
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { AiOpsStartDependencies } from './plugin';
|
||||
|
||||
let coreStart: CoreStart;
|
||||
export function setStartServices(core: CoreStart) {
|
||||
let pluginsStart: AiOpsStartDependencies;
|
||||
export function setStartServices(core: CoreStart, plugins: AiOpsStartDependencies) {
|
||||
coreStart = core;
|
||||
pluginsStart = plugins;
|
||||
}
|
||||
|
||||
export const getCoreStart = () => coreStart;
|
||||
export const getPluginsStart = () => pluginsStart;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
|
||||
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
|
||||
|
@ -17,12 +18,13 @@ export interface AiOpsStartDependencies {
|
|||
data: DataPublicPluginStart;
|
||||
charts: ChartsPluginStart;
|
||||
fieldFormats: FieldFormatsStart;
|
||||
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||
}
|
||||
|
||||
export class AiopsPlugin implements Plugin<AiopsPluginSetup, AiopsPluginStart> {
|
||||
public setup(core: CoreSetup) {}
|
||||
public start(core: CoreStart) {
|
||||
setStartServices(core);
|
||||
public start(core: CoreStart, plugins: AiOpsStartDependencies) {
|
||||
setStartServices(core, plugins);
|
||||
}
|
||||
public stop() {}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import React, { FC, Suspense } from 'react';
|
||||
|
||||
import { EuiErrorBoundary, EuiLoadingContent } from '@elastic/eui';
|
||||
|
||||
import type { ExplainLogRateSpikesWrapperProps } from './components/explain_log_rate_spikes';
|
||||
|
||||
const ExplainLogRateSpikesWrapperLazy = React.lazy(
|
||||
|
|
|
@ -25,5 +25,6 @@
|
|||
{ "path": "../../../src/plugins/unified_search/tsconfig.json" },
|
||||
{ "path": "../security/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/charts/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/discover/tsconfig.json" },
|
||||
]
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// TODO Consolidate with duplicate component `CorrelationsProgressControls` in
|
||||
// `x-pack/plugins/apm/public/components/app/correlations/progress_controls.tsx`
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { IUiSettingsClient } from '@kbn/core/public';
|
||||
import {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue