[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:
Melissa Alvarez 2022-07-20 14:34:18 -04:00 committed by GitHub
parent d862f6feeb
commit 59082823ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 678 additions and 135 deletions

View file

@ -12,7 +12,9 @@
"requiredPlugins": [
"charts",
"data",
"licensing"
"discover",
"licensing",
"unifiedSearch"
],
"optionalPlugins": [],
"requiredBundles": ["kibanaReact", "fieldFormats"],

View file

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

View file

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

View file

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

View file

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

View 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,
};
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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),
})

View file

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

View file

@ -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() {}
}

View file

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

View file

@ -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" },
]
}

View file

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