Report telemetry for autocomplete (#91428)
* Report telemetry about autocomplete * error cound * position of selected suggestion * length of query used to generate the suggestions Also added a couple if handly telemetry events: * query language switch * filter bar bulk actions * Fix ts * Report the value suggestions funnel * docs * docs * fix jest * test * code review Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
edc11d9680
commit
98bd8e0211
|
@ -21,4 +21,5 @@ export interface IDataPluginServices extends Partial<CoreStart>
|
|||
| [savedObjects](./kibana-plugin-plugins-data-public.idatapluginservices.savedobjects.md) | <code>CoreStart['savedObjects']</code> | |
|
||||
| [storage](./kibana-plugin-plugins-data-public.idatapluginservices.storage.md) | <code>IStorageWrapper</code> | |
|
||||
| [uiSettings](./kibana-plugin-plugins-data-public.idatapluginservices.uisettings.md) | <code>CoreStart['uiSettings']</code> | |
|
||||
| [usageCollection](./kibana-plugin-plugins-data-public.idatapluginservices.usagecollection.md) | <code>UsageCollectionStart</code> | |
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IDataPluginServices](./kibana-plugin-plugins-data-public.idatapluginservices.md) > [usageCollection](./kibana-plugin-plugins-data-public.idatapluginservices.usagecollection.md)
|
||||
|
||||
## IDataPluginServices.usageCollection property
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
usageCollection?: UsageCollectionStart;
|
||||
```
|
|
@ -7,7 +7,7 @@
|
|||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
SearchBar: React.ComponentClass<Pick<Pick<SearchBarProps, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "intl" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & {
|
||||
WrappedComponent: React.ComponentType<Pick<SearchBarProps, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "intl" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated"> & ReactIntl.InjectedIntlProps>;
|
||||
SearchBar: React.ComponentClass<Pick<Pick<SearchBarProps, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "intl" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & {
|
||||
WrappedComponent: React.ComponentType<Pick<SearchBarProps, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "intl" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated"> & ReactIntl.InjectedIntlProps>;
|
||||
}
|
||||
```
|
||||
|
|
|
@ -16,6 +16,8 @@ import {
|
|||
} from './providers/value_suggestion_provider';
|
||||
|
||||
import { ConfigSchema } from '../../config';
|
||||
import { UsageCollectionSetup } from '../../../usage_collection/public';
|
||||
import { createUsageCollector } from './collectors';
|
||||
|
||||
export class AutocompleteService {
|
||||
autocompleteConfig: ConfigSchema['autocomplete'];
|
||||
|
@ -47,9 +49,17 @@ export class AutocompleteService {
|
|||
private hasQuerySuggestions = (language: string) => this.querySuggestionProviders.has(language);
|
||||
|
||||
/** @public **/
|
||||
public setup(core: CoreSetup, { timefilter }: { timefilter: TimefilterSetup }) {
|
||||
public setup(
|
||||
core: CoreSetup,
|
||||
{
|
||||
timefilter,
|
||||
usageCollection,
|
||||
}: { timefilter: TimefilterSetup; usageCollection?: UsageCollectionSetup }
|
||||
) {
|
||||
const usageCollector = createUsageCollector(core.getStartServices, usageCollection);
|
||||
|
||||
this.getValueSuggestions = this.autocompleteConfig.valueSuggestions.enabled
|
||||
? setupValueSuggestionProvider(core, { timefilter })
|
||||
? setupValueSuggestionProvider(core, { timefilter, usageCollector })
|
||||
: getEmptyValueSuggestions;
|
||||
|
||||
return {
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { first } from 'rxjs/operators';
|
||||
import { StartServicesAccessor } from '../../../../../core/public';
|
||||
import { METRIC_TYPE, UsageCollectionSetup } from '../../../../usage_collection/public';
|
||||
import { AUTOCOMPLETE_EVENT_TYPE, AutocompleteUsageCollector } from './types';
|
||||
|
||||
export const createUsageCollector = (
|
||||
getStartServices: StartServicesAccessor,
|
||||
usageCollection?: UsageCollectionSetup
|
||||
): AutocompleteUsageCollector => {
|
||||
const getCurrentApp = async () => {
|
||||
const [{ application }] = await getStartServices();
|
||||
return application.currentAppId$.pipe(first()).toPromise();
|
||||
};
|
||||
|
||||
return {
|
||||
trackCall: async () => {
|
||||
const currentApp = await getCurrentApp();
|
||||
return usageCollection?.reportUiCounter(
|
||||
currentApp!,
|
||||
METRIC_TYPE.LOADED,
|
||||
AUTOCOMPLETE_EVENT_TYPE.CALL
|
||||
);
|
||||
},
|
||||
trackRequest: async () => {
|
||||
const currentApp = await getCurrentApp();
|
||||
return usageCollection?.reportUiCounter(
|
||||
currentApp!,
|
||||
METRIC_TYPE.LOADED,
|
||||
AUTOCOMPLETE_EVENT_TYPE.REQUEST
|
||||
);
|
||||
},
|
||||
trackResult: async () => {
|
||||
const currentApp = await getCurrentApp();
|
||||
return usageCollection?.reportUiCounter(
|
||||
currentApp!,
|
||||
METRIC_TYPE.LOADED,
|
||||
AUTOCOMPLETE_EVENT_TYPE.RESULT
|
||||
);
|
||||
},
|
||||
trackError: async () => {
|
||||
const currentApp = await getCurrentApp();
|
||||
return usageCollection?.reportUiCounter(
|
||||
currentApp!,
|
||||
METRIC_TYPE.LOADED,
|
||||
AUTOCOMPLETE_EVENT_TYPE.ERROR
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
10
src/plugins/data/public/autocomplete/collectors/index.ts
Normal file
10
src/plugins/data/public/autocomplete/collectors/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { createUsageCollector } from './create_usage_collector';
|
||||
export { AUTOCOMPLETE_EVENT_TYPE, AutocompleteUsageCollector } from './types';
|
21
src/plugins/data/public/autocomplete/collectors/types.ts
Normal file
21
src/plugins/data/public/autocomplete/collectors/types.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export enum AUTOCOMPLETE_EVENT_TYPE {
|
||||
CALL = 'autocomplete:call',
|
||||
REQUEST = 'autocomplete:req',
|
||||
RESULT = 'autocomplete:res',
|
||||
ERROR = 'autocomplete:err',
|
||||
}
|
||||
|
||||
export interface AutocompleteUsageCollector {
|
||||
trackCall: () => Promise<void>;
|
||||
trackRequest: () => Promise<void>;
|
||||
trackResult: () => Promise<void>;
|
||||
trackError: () => Promise<void>;
|
||||
}
|
|
@ -18,7 +18,7 @@ describe('FieldSuggestions', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
const uiSettings = { get: (key: string) => shouldSuggestValues } as IUiSettingsClient;
|
||||
http = { fetch: jest.fn() };
|
||||
http = { fetch: jest.fn().mockResolvedValue([]) };
|
||||
|
||||
getValueSuggestions = setupValueSuggestionProvider({ http, uiSettings } as CoreSetup, {
|
||||
timefilter: ({
|
||||
|
|
|
@ -11,12 +11,7 @@ import { memoize } from 'lodash';
|
|||
import { CoreSetup } from 'src/core/public';
|
||||
import { IIndexPattern, IFieldType, UI_SETTINGS, buildQueryFromFilters } from '../../../common';
|
||||
import { TimefilterSetup } from '../../query';
|
||||
|
||||
function resolver(title: string, field: IFieldType, query: string, filters: any[]) {
|
||||
// Only cache results for a minute
|
||||
const ttl = Math.floor(Date.now() / 1000 / 60);
|
||||
return [ttl, query, title, field.name, JSON.stringify(filters)].join('|');
|
||||
}
|
||||
import { AutocompleteUsageCollector } from '../collectors';
|
||||
|
||||
export type ValueSuggestionsGetFn = (args: ValueSuggestionsGetFnArgs) => Promise<any[]>;
|
||||
|
||||
|
@ -47,15 +42,31 @@ export const getEmptyValueSuggestions = (() => Promise.resolve([])) as ValueSugg
|
|||
|
||||
export const setupValueSuggestionProvider = (
|
||||
core: CoreSetup,
|
||||
{ timefilter }: { timefilter: TimefilterSetup }
|
||||
{
|
||||
timefilter,
|
||||
usageCollector,
|
||||
}: { timefilter: TimefilterSetup; usageCollector?: AutocompleteUsageCollector }
|
||||
): ValueSuggestionsGetFn => {
|
||||
function resolver(title: string, field: IFieldType, query: string, filters: any[]) {
|
||||
// Only cache results for a minute
|
||||
const ttl = Math.floor(Date.now() / 1000 / 60);
|
||||
return [ttl, query, title, field.name, JSON.stringify(filters)].join('|');
|
||||
}
|
||||
|
||||
const requestSuggestions = memoize(
|
||||
(index: string, field: IFieldType, query: string, filters: any = [], signal?: AbortSignal) =>
|
||||
core.http.fetch(`/api/kibana/suggestions/values/${index}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ query, field: field.name, filters }),
|
||||
signal,
|
||||
}),
|
||||
(index: string, field: IFieldType, query: string, filters: any = [], signal?: AbortSignal) => {
|
||||
usageCollector?.trackRequest();
|
||||
return core.http
|
||||
.fetch(`/api/kibana/suggestions/values/${index}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ query, field: field.name, filters }),
|
||||
signal,
|
||||
})
|
||||
.then((r) => {
|
||||
usageCollector?.trackResult();
|
||||
return r;
|
||||
});
|
||||
},
|
||||
resolver
|
||||
);
|
||||
|
||||
|
@ -85,6 +96,16 @@ export const setupValueSuggestionProvider = (
|
|||
: undefined;
|
||||
const filterQuery = timeFilter ? buildQueryFromFilters([timeFilter], indexPattern).filter : [];
|
||||
const filters = [...(boolFilter ? boolFilter : []), ...filterQuery];
|
||||
return await requestSuggestions(title, field, query, filters, signal);
|
||||
try {
|
||||
usageCollector?.trackCall();
|
||||
return await requestSuggestions(title, field, query, filters, signal);
|
||||
} catch (e) {
|
||||
if (!signal?.aborted) {
|
||||
usageCollector?.trackError();
|
||||
}
|
||||
// Remove rejected results from memoize cache
|
||||
requestSuggestions.cache.delete(resolver(title, field, query, filters));
|
||||
return [];
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -115,7 +115,10 @@ export class DataPublicPlugin
|
|||
);
|
||||
|
||||
return {
|
||||
autocomplete: this.autocomplete.setup(core, { timefilter: queryService.timefilter }),
|
||||
autocomplete: this.autocomplete.setup(core, {
|
||||
timefilter: queryService.timefilter,
|
||||
usageCollection,
|
||||
}),
|
||||
search: searchService,
|
||||
fieldFormats: this.fieldFormatsService.setup(core),
|
||||
query: queryService,
|
||||
|
@ -195,10 +198,7 @@ export class DataPublicPlugin
|
|||
core,
|
||||
data: dataServices,
|
||||
storage: this.storage,
|
||||
trackUiMetric: this.usageCollection?.reportUiCounter.bind(
|
||||
this.usageCollection,
|
||||
'data_plugin'
|
||||
),
|
||||
usageCollection: this.usageCollection,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -105,7 +105,6 @@ import { Type } from '@kbn/config-schema';
|
|||
import { TypeOf } from '@kbn/config-schema';
|
||||
import { UiActionsSetup } from 'src/plugins/ui_actions/public';
|
||||
import { UiActionsStart } from 'src/plugins/ui_actions/public';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import { Unit } from '@elastic/datemath';
|
||||
import { UnregisterCallback } from 'history';
|
||||
import { URL } from 'url';
|
||||
|
@ -1094,6 +1093,10 @@ export interface IDataPluginServices extends Partial<CoreStart_2> {
|
|||
storage: IStorageWrapper;
|
||||
// (undocumented)
|
||||
uiSettings: CoreStart_2['uiSettings'];
|
||||
// Warning: (ae-forgotten-export) The symbol "UsageCollectionStart" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// (undocumented)
|
||||
usageCollection?: UsageCollectionStart;
|
||||
}
|
||||
|
||||
// Warning: (ae-missing-release-tag) "IEsSearchRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
|
@ -2250,8 +2253,8 @@ export const SEARCH_SESSIONS_MANAGEMENT_ID = "search_sessions";
|
|||
// Warning: (ae-missing-release-tag) "SearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const SearchBar: React.ComponentClass<Pick<Pick<SearchBarProps, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "intl" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & {
|
||||
WrappedComponent: React.ComponentType<Pick<SearchBarProps, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "intl" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated"> & ReactIntl.InjectedIntlProps>;
|
||||
export const SearchBar: React.ComponentClass<Pick<Pick<SearchBarProps, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "intl" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & {
|
||||
WrappedComponent: React.ComponentType<Pick<SearchBarProps, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "intl" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated"> & ReactIntl.InjectedIntlProps>;
|
||||
};
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "SearchBarOwnProps" needs to be exported by the entry point index.d.ts
|
||||
|
|
|
@ -19,7 +19,7 @@ import { ISearchSetup, ISearchStart, SearchEnhancements } from './search';
|
|||
import { QuerySetup, QueryStart } from './query';
|
||||
import { IndexPatternsContract } from './index_patterns';
|
||||
import { IndexPatternSelectProps, StatefulSearchBarProps } from './ui';
|
||||
import { UsageCollectionSetup } from '../../usage_collection/public';
|
||||
import { UsageCollectionSetup, UsageCollectionStart } from '../../usage_collection/public';
|
||||
import { Setup as InspectorSetup } from '../../inspector/public';
|
||||
import { NowProviderPublicContract } from './now_provider';
|
||||
|
||||
|
@ -120,4 +120,5 @@ export interface IDataPluginServices extends Partial<CoreStart> {
|
|||
http: CoreStart['http'];
|
||||
storage: IStorageWrapper;
|
||||
data: DataPublicPluginStart;
|
||||
usageCollection?: UsageCollectionStart;
|
||||
}
|
||||
|
|
|
@ -11,12 +11,12 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
|||
import classNames from 'classnames';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { FilterEditor } from './filter_editor';
|
||||
import { FILTER_EDITOR_WIDTH, FilterItem } from './filter_item';
|
||||
import { FilterOptions } from './filter_options';
|
||||
import { useKibana } from '../../../../kibana_react/public';
|
||||
import { IIndexPattern } from '../..';
|
||||
import { IDataPluginServices, IIndexPattern } from '../..';
|
||||
import {
|
||||
buildEmptyFilter,
|
||||
Filter,
|
||||
|
@ -36,17 +36,16 @@ interface Props {
|
|||
indexPatterns: IIndexPattern[];
|
||||
intl: InjectedIntl;
|
||||
appName: string;
|
||||
// Track UI Metrics
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
}
|
||||
|
||||
function FilterBarUI(props: Props) {
|
||||
const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false);
|
||||
const kibana = useKibana();
|
||||
|
||||
const uiSettings = kibana.services.uiSettings;
|
||||
const kibana = useKibana<IDataPluginServices>();
|
||||
const { appName, usageCollection, uiSettings } = kibana.services;
|
||||
if (!uiSettings) return null;
|
||||
|
||||
const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName);
|
||||
|
||||
function onFiltersUpdated(filters: Filter[]) {
|
||||
if (props.onFiltersUpdated) {
|
||||
props.onFiltersUpdated(filters);
|
||||
|
@ -119,66 +118,65 @@ function FilterBarUI(props: Props) {
|
|||
}
|
||||
|
||||
function onAdd(filter: Filter) {
|
||||
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:added`);
|
||||
setIsAddFilterPopoverOpen(false);
|
||||
if (props.trackUiMetric) {
|
||||
props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_added`);
|
||||
}
|
||||
|
||||
const filters = [...props.filters, filter];
|
||||
onFiltersUpdated(filters);
|
||||
}
|
||||
|
||||
function onRemove(i: number) {
|
||||
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:removed`);
|
||||
const filters = [...props.filters];
|
||||
filters.splice(i, 1);
|
||||
onFiltersUpdated(filters);
|
||||
}
|
||||
|
||||
function onUpdate(i: number, filter: Filter) {
|
||||
if (props.trackUiMetric) {
|
||||
props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_edited`);
|
||||
}
|
||||
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:edited`);
|
||||
const filters = [...props.filters];
|
||||
filters[i] = filter;
|
||||
onFiltersUpdated(filters);
|
||||
}
|
||||
|
||||
function onEnableAll() {
|
||||
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:enable_all`);
|
||||
const filters = props.filters.map(enableFilter);
|
||||
onFiltersUpdated(filters);
|
||||
}
|
||||
|
||||
function onDisableAll() {
|
||||
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:disable_all`);
|
||||
const filters = props.filters.map(disableFilter);
|
||||
onFiltersUpdated(filters);
|
||||
}
|
||||
|
||||
function onPinAll() {
|
||||
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:pin_all`);
|
||||
const filters = props.filters.map(pinFilter);
|
||||
onFiltersUpdated(filters);
|
||||
}
|
||||
|
||||
function onUnpinAll() {
|
||||
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:unpin_all`);
|
||||
const filters = props.filters.map(unpinFilter);
|
||||
onFiltersUpdated(filters);
|
||||
}
|
||||
|
||||
function onToggleAllNegated() {
|
||||
if (props.trackUiMetric) {
|
||||
props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_invertInclusion`);
|
||||
}
|
||||
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:invert_all`);
|
||||
const filters = props.filters.map(toggleFilterNegated);
|
||||
onFiltersUpdated(filters);
|
||||
}
|
||||
|
||||
function onToggleAllDisabled() {
|
||||
if (props.trackUiMetric) {
|
||||
props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_toggleAllDisabled`);
|
||||
}
|
||||
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:toggle_all`);
|
||||
const filters = props.filters.map(toggleFilterDisabled);
|
||||
onFiltersUpdated(filters);
|
||||
}
|
||||
|
||||
function onRemoveAll() {
|
||||
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:remove_all`);
|
||||
onFiltersUpdated([]);
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { debounce, compact, isEqual, isFunction } from 'lodash';
|
||||
import { Toast } from 'src/core/public';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { IDataPluginServices, IIndexPattern, Query } from '../..';
|
||||
import { QuerySuggestion, QuerySuggestionTypes } from '../../autocomplete';
|
||||
|
||||
|
@ -105,6 +106,10 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
private abortController?: AbortController;
|
||||
private fetchIndexPatternsAbortController?: AbortController;
|
||||
private services = this.props.kibana.services;
|
||||
private reportUiCounter = this.services.usageCollection?.reportUiCounter.bind(
|
||||
this.services.usageCollection,
|
||||
this.services.appName
|
||||
);
|
||||
private componentIsUnmounting = false;
|
||||
private queryBarInputDivRefInstance: RefObject<HTMLDivElement> = createRef();
|
||||
|
||||
|
@ -178,12 +183,14 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
selectionEnd,
|
||||
signal: this.abortController.signal,
|
||||
})) || [];
|
||||
|
||||
return [...suggestions, ...recentSearchSuggestions];
|
||||
} catch (e) {
|
||||
// TODO: Waiting on https://github.com/elastic/kibana/issues/51406 for a properly typed error
|
||||
// Ignore aborted requests
|
||||
if (e.message === 'The user aborted a request.') return;
|
||||
|
||||
this.reportUiCounter?.(METRIC_TYPE.LOADED, `query_string:suggestions_error`);
|
||||
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
@ -302,7 +309,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
}
|
||||
if (isSuggestionsVisible && index !== null && this.state.suggestions[index]) {
|
||||
event.preventDefault();
|
||||
this.selectSuggestion(this.state.suggestions[index]);
|
||||
this.selectSuggestion(this.state.suggestions[index], index);
|
||||
} else {
|
||||
this.onSubmit(this.props.query);
|
||||
this.setState({
|
||||
|
@ -335,7 +342,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
}
|
||||
};
|
||||
|
||||
private selectSuggestion = (suggestion: QuerySuggestion) => {
|
||||
private selectSuggestion = (suggestion: QuerySuggestion, listIndex: number) => {
|
||||
if (!this.inputRef) {
|
||||
return;
|
||||
}
|
||||
|
@ -352,6 +359,17 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
const value = query.substr(0, selectionStart) + query.substr(selectionEnd);
|
||||
const newQueryString = value.substr(0, start) + text + value.substr(end);
|
||||
|
||||
this.reportUiCounter?.(
|
||||
METRIC_TYPE.LOADED,
|
||||
`query_string:${type}:suggestions_select_position`,
|
||||
listIndex
|
||||
);
|
||||
this.reportUiCounter?.(
|
||||
METRIC_TYPE.LOADED,
|
||||
`query_string:${type}:suggestions_select_q_length`,
|
||||
end - start
|
||||
);
|
||||
|
||||
this.onQueryStringChange(newQueryString);
|
||||
|
||||
this.setState({
|
||||
|
@ -458,6 +476,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
const newQuery = { query: '', language };
|
||||
this.onChange(newQuery);
|
||||
this.onSubmit(newQuery);
|
||||
this.reportUiCounter?.(METRIC_TYPE.LOADED, `query_string:language:${language}`);
|
||||
};
|
||||
|
||||
private onOutsideClick = () => {
|
||||
|
@ -480,11 +499,11 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
}
|
||||
};
|
||||
|
||||
private onClickSuggestion = (suggestion: QuerySuggestion) => {
|
||||
private onClickSuggestion = (suggestion: QuerySuggestion, index: number) => {
|
||||
if (!this.inputRef) {
|
||||
return;
|
||||
}
|
||||
this.selectSuggestion(suggestion);
|
||||
this.selectSuggestion(suggestion, index);
|
||||
this.inputRef.focus();
|
||||
};
|
||||
|
||||
|
@ -588,6 +607,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
|||
if (this.props.onChangeQueryInputFocus) {
|
||||
this.props.onChangeQueryInputFocus(true);
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.handleAutoHeight();
|
||||
});
|
||||
|
|
|
@ -10,7 +10,6 @@ import _ from 'lodash';
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import { CoreStart } from 'src/core/public';
|
||||
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
|
||||
import { UiCounterMetricType } from '@kbn/analytics';
|
||||
import { KibanaContextProvider } from '../../../../kibana_react/public';
|
||||
import { QueryStart, SavedQuery } from '../../query';
|
||||
import { SearchBar, SearchBarOwnProps } from './';
|
||||
|
@ -20,12 +19,13 @@ import { useSavedQuery } from './lib/use_saved_query';
|
|||
import { DataPublicPluginStart } from '../../types';
|
||||
import { Filter, Query, TimeRange } from '../../../common';
|
||||
import { useQueryStringManager } from './lib/use_query_string_manager';
|
||||
import { UsageCollectionSetup } from '../../../../usage_collection/public';
|
||||
|
||||
interface StatefulSearchBarDeps {
|
||||
core: CoreStart;
|
||||
data: Omit<DataPublicPluginStart, 'ui'>;
|
||||
storage: IStorageWrapper;
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
}
|
||||
|
||||
export type StatefulSearchBarProps = SearchBarOwnProps & {
|
||||
|
@ -110,7 +110,7 @@ const overrideDefaultBehaviors = (props: StatefulSearchBarProps) => {
|
|||
return props.useDefaultBehaviors ? {} : props;
|
||||
};
|
||||
|
||||
export function createSearchBar({ core, storage, data, trackUiMetric }: StatefulSearchBarDeps) {
|
||||
export function createSearchBar({ core, storage, data, usageCollection }: StatefulSearchBarDeps) {
|
||||
// App name should come from the core application service.
|
||||
// Until it's available, we'll ask the user to provide it for the pre-wired component.
|
||||
return (props: StatefulSearchBarProps) => {
|
||||
|
@ -161,6 +161,7 @@ export function createSearchBar({ core, storage, data, trackUiMetric }: Stateful
|
|||
appName: props.appName,
|
||||
data,
|
||||
storage,
|
||||
usageCollection,
|
||||
...core,
|
||||
}}
|
||||
>
|
||||
|
@ -188,7 +189,6 @@ export function createSearchBar({ core, storage, data, trackUiMetric }: Stateful
|
|||
onClearSavedQuery={defaultOnClearSavedQuery(props, clearSavedQuery)}
|
||||
onSavedQueryUpdated={defaultOnSavedQueryUpdated(props, setSavedQuery)}
|
||||
onSaved={defaultOnSavedQueryUpdated(props, setSavedQuery)}
|
||||
trackUiMetric={trackUiMetric}
|
||||
{...overrideDefaultBehaviors(props)}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
|
|
|
@ -13,7 +13,7 @@ import React, { Component } from 'react';
|
|||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
import { get, isEqual } from 'lodash';
|
||||
|
||||
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { withKibana, KibanaReactContextValue } from '../../../../kibana_react/public';
|
||||
|
||||
import QueryBarTopRow from '../query_string_input/query_bar_top_row';
|
||||
|
@ -68,8 +68,6 @@ export interface SearchBarOwnProps {
|
|||
|
||||
onRefresh?: (payload: { dateRange: TimeRange }) => void;
|
||||
indicateNoData?: boolean;
|
||||
// Track UI Metrics
|
||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
||||
}
|
||||
|
||||
export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps;
|
||||
|
@ -323,9 +321,11 @@ class SearchBarUI extends Component<SearchBarProps, State> {
|
|||
},
|
||||
});
|
||||
}
|
||||
if (this.props.trackUiMetric) {
|
||||
this.props.trackUiMetric(METRIC_TYPE.CLICK, `${this.services.appName}:query_submitted`);
|
||||
}
|
||||
this.services.usageCollection?.reportUiCounter(
|
||||
this.services.appName,
|
||||
METRIC_TYPE.CLICK,
|
||||
'query_submitted'
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -428,7 +428,6 @@ class SearchBarUI extends Component<SearchBarProps, State> {
|
|||
onFiltersUpdated={this.props.onFiltersUpdated}
|
||||
indexPatterns={this.props.indexPatterns!}
|
||||
appName={this.services.appName}
|
||||
trackUiMetric={this.props.trackUiMetric}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -22,6 +22,7 @@ exports[`SuggestionsComponent Passing the index should control which suggestion
|
|||
>
|
||||
<SuggestionComponent
|
||||
ariaId="suggestion-0"
|
||||
index={0}
|
||||
innerRef={[Function]}
|
||||
key="value - as promised, not helpful"
|
||||
onClick={[Function]}
|
||||
|
@ -40,6 +41,7 @@ exports[`SuggestionsComponent Passing the index should control which suggestion
|
|||
/>
|
||||
<SuggestionComponent
|
||||
ariaId="suggestion-1"
|
||||
index={1}
|
||||
innerRef={[Function]}
|
||||
key="field - yep"
|
||||
onClick={[Function]}
|
||||
|
@ -84,6 +86,7 @@ exports[`SuggestionsComponent Should display given suggestions if the show prop
|
|||
>
|
||||
<SuggestionComponent
|
||||
ariaId="suggestion-0"
|
||||
index={0}
|
||||
innerRef={[Function]}
|
||||
key="value - as promised, not helpful"
|
||||
onClick={[Function]}
|
||||
|
@ -102,6 +105,7 @@ exports[`SuggestionsComponent Should display given suggestions if the show prop
|
|||
/>
|
||||
<SuggestionComponent
|
||||
ariaId="suggestion-1"
|
||||
index={1}
|
||||
innerRef={[Function]}
|
||||
key="field - yep"
|
||||
onClick={[Function]}
|
||||
|
|
|
@ -27,6 +27,7 @@ describe('SuggestionComponent', () => {
|
|||
it('Should display the suggestion and use the provided ariaId', () => {
|
||||
const component = shallow(
|
||||
<SuggestionComponent
|
||||
index={0}
|
||||
onClick={noop}
|
||||
onMouseEnter={noop}
|
||||
selected={false}
|
||||
|
@ -43,6 +44,7 @@ describe('SuggestionComponent', () => {
|
|||
it('Should make the element active if the selected prop is true', () => {
|
||||
const component = shallow(
|
||||
<SuggestionComponent
|
||||
index={0}
|
||||
onClick={noop}
|
||||
onMouseEnter={noop}
|
||||
selected={true}
|
||||
|
@ -64,6 +66,7 @@ describe('SuggestionComponent', () => {
|
|||
|
||||
mount(
|
||||
<SuggestionComponent
|
||||
index={0}
|
||||
onClick={noop}
|
||||
onMouseEnter={noop}
|
||||
selected={false}
|
||||
|
@ -80,6 +83,7 @@ describe('SuggestionComponent', () => {
|
|||
|
||||
const component = shallow(
|
||||
<SuggestionComponent
|
||||
index={0}
|
||||
onClick={mockHandler}
|
||||
onMouseEnter={noop}
|
||||
selected={false}
|
||||
|
@ -92,7 +96,7 @@ describe('SuggestionComponent', () => {
|
|||
|
||||
component.simulate('click');
|
||||
expect(mockHandler).toHaveBeenCalledTimes(1);
|
||||
expect(mockHandler).toHaveBeenCalledWith(mockSuggestion);
|
||||
expect(mockHandler).toHaveBeenCalledWith(mockSuggestion, 0);
|
||||
});
|
||||
|
||||
it('Should call onMouseEnter when user mouses over the element', () => {
|
||||
|
@ -100,6 +104,7 @@ describe('SuggestionComponent', () => {
|
|||
|
||||
const component = shallow(
|
||||
<SuggestionComponent
|
||||
index={0}
|
||||
onClick={noop}
|
||||
onMouseEnter={mockHandler}
|
||||
selected={false}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { EuiIcon } from '@elastic/eui';
|
|||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { QuerySuggestion } from '../../autocomplete';
|
||||
import { SuggestionOnClick } from './types';
|
||||
|
||||
function getEuiIconType(type: string) {
|
||||
switch (type) {
|
||||
|
@ -29,9 +30,10 @@ function getEuiIconType(type: string) {
|
|||
}
|
||||
|
||||
interface Props {
|
||||
onClick: (suggestion: QuerySuggestion) => void;
|
||||
onClick: SuggestionOnClick;
|
||||
onMouseEnter: () => void;
|
||||
selected: boolean;
|
||||
index: number;
|
||||
suggestion: QuerySuggestion;
|
||||
innerRef: (node: HTMLDivElement) => void;
|
||||
ariaId: string;
|
||||
|
@ -48,7 +50,7 @@ export function SuggestionComponent(props: Props) {
|
|||
active: props.selected,
|
||||
})}
|
||||
role="option"
|
||||
onClick={() => props.onClick(props.suggestion)}
|
||||
onClick={() => props.onClick(props.suggestion, props.index)}
|
||||
onMouseEnter={props.onMouseEnter}
|
||||
ref={props.innerRef}
|
||||
id={props.ariaId}
|
||||
|
|
|
@ -115,7 +115,7 @@ describe('SuggestionsComponent', () => {
|
|||
|
||||
component.find(SuggestionComponent).at(1).simulate('click');
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
expect(mockCallback).toHaveBeenCalledWith(mockSuggestions[1]);
|
||||
expect(mockCallback).toHaveBeenCalledWith(mockSuggestions[1], 1);
|
||||
});
|
||||
|
||||
it('Should call onMouseEnter with the index of the suggestion that was entered', () => {
|
||||
|
|
|
@ -17,11 +17,12 @@ import {
|
|||
SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET,
|
||||
SUGGESTIONS_LIST_REQUIRED_WIDTH,
|
||||
} from './constants';
|
||||
import { SuggestionOnClick } from './types';
|
||||
|
||||
// @internal
|
||||
export interface SuggestionsComponentProps {
|
||||
index: number | null;
|
||||
onClick: (suggestion: QuerySuggestion) => void;
|
||||
onClick: SuggestionOnClick;
|
||||
onMouseEnter: (index: number) => void;
|
||||
show: boolean;
|
||||
suggestions: QuerySuggestion[];
|
||||
|
@ -50,6 +51,7 @@ export default class SuggestionsComponent extends Component<SuggestionsComponent
|
|||
<SuggestionComponent
|
||||
innerRef={(node) => (this.childNodes[index] = node)}
|
||||
selected={index === this.props.index}
|
||||
index={index}
|
||||
suggestion={suggestion}
|
||||
onClick={this.props.onClick}
|
||||
onMouseEnter={() => this.props.onMouseEnter(index)}
|
||||
|
|
11
src/plugins/data/public/ui/typeahead/types.ts
Normal file
11
src/plugins/data/public/ui/typeahead/types.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { QuerySuggestion } from '../../autocomplete';
|
||||
|
||||
export type SuggestionOnClick = (suggestion: QuerySuggestion, index: number) => void;
|
Loading…
Reference in a new issue