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> | |
|
| [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> | |
|
| [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> | |
|
| [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>
|
<b>Signature:</b>
|
||||||
|
|
||||||
```typescript
|
```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> & {
|
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" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated"> & ReactIntl.InjectedIntlProps>;
|
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';
|
} from './providers/value_suggestion_provider';
|
||||||
|
|
||||||
import { ConfigSchema } from '../../config';
|
import { ConfigSchema } from '../../config';
|
||||||
|
import { UsageCollectionSetup } from '../../../usage_collection/public';
|
||||||
|
import { createUsageCollector } from './collectors';
|
||||||
|
|
||||||
export class AutocompleteService {
|
export class AutocompleteService {
|
||||||
autocompleteConfig: ConfigSchema['autocomplete'];
|
autocompleteConfig: ConfigSchema['autocomplete'];
|
||||||
|
@ -47,9 +49,17 @@ export class AutocompleteService {
|
||||||
private hasQuerySuggestions = (language: string) => this.querySuggestionProviders.has(language);
|
private hasQuerySuggestions = (language: string) => this.querySuggestionProviders.has(language);
|
||||||
|
|
||||||
/** @public **/
|
/** @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
|
this.getValueSuggestions = this.autocompleteConfig.valueSuggestions.enabled
|
||||||
? setupValueSuggestionProvider(core, { timefilter })
|
? setupValueSuggestionProvider(core, { timefilter, usageCollector })
|
||||||
: getEmptyValueSuggestions;
|
: getEmptyValueSuggestions;
|
||||||
|
|
||||||
return {
|
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(() => {
|
beforeEach(() => {
|
||||||
const uiSettings = { get: (key: string) => shouldSuggestValues } as IUiSettingsClient;
|
const uiSettings = { get: (key: string) => shouldSuggestValues } as IUiSettingsClient;
|
||||||
http = { fetch: jest.fn() };
|
http = { fetch: jest.fn().mockResolvedValue([]) };
|
||||||
|
|
||||||
getValueSuggestions = setupValueSuggestionProvider({ http, uiSettings } as CoreSetup, {
|
getValueSuggestions = setupValueSuggestionProvider({ http, uiSettings } as CoreSetup, {
|
||||||
timefilter: ({
|
timefilter: ({
|
||||||
|
|
|
@ -11,12 +11,7 @@ import { memoize } from 'lodash';
|
||||||
import { CoreSetup } from 'src/core/public';
|
import { CoreSetup } from 'src/core/public';
|
||||||
import { IIndexPattern, IFieldType, UI_SETTINGS, buildQueryFromFilters } from '../../../common';
|
import { IIndexPattern, IFieldType, UI_SETTINGS, buildQueryFromFilters } from '../../../common';
|
||||||
import { TimefilterSetup } from '../../query';
|
import { TimefilterSetup } from '../../query';
|
||||||
|
import { AutocompleteUsageCollector } from '../collectors';
|
||||||
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('|');
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ValueSuggestionsGetFn = (args: ValueSuggestionsGetFnArgs) => Promise<any[]>;
|
export type ValueSuggestionsGetFn = (args: ValueSuggestionsGetFnArgs) => Promise<any[]>;
|
||||||
|
|
||||||
|
@ -47,15 +42,31 @@ export const getEmptyValueSuggestions = (() => Promise.resolve([])) as ValueSugg
|
||||||
|
|
||||||
export const setupValueSuggestionProvider = (
|
export const setupValueSuggestionProvider = (
|
||||||
core: CoreSetup,
|
core: CoreSetup,
|
||||||
{ timefilter }: { timefilter: TimefilterSetup }
|
{
|
||||||
|
timefilter,
|
||||||
|
usageCollector,
|
||||||
|
}: { timefilter: TimefilterSetup; usageCollector?: AutocompleteUsageCollector }
|
||||||
): ValueSuggestionsGetFn => {
|
): 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(
|
const requestSuggestions = memoize(
|
||||||
(index: string, field: IFieldType, query: string, filters: any = [], signal?: AbortSignal) =>
|
(index: string, field: IFieldType, query: string, filters: any = [], signal?: AbortSignal) => {
|
||||||
core.http.fetch(`/api/kibana/suggestions/values/${index}`, {
|
usageCollector?.trackRequest();
|
||||||
method: 'POST',
|
return core.http
|
||||||
body: JSON.stringify({ query, field: field.name, filters }),
|
.fetch(`/api/kibana/suggestions/values/${index}`, {
|
||||||
signal,
|
method: 'POST',
|
||||||
}),
|
body: JSON.stringify({ query, field: field.name, filters }),
|
||||||
|
signal,
|
||||||
|
})
|
||||||
|
.then((r) => {
|
||||||
|
usageCollector?.trackResult();
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
},
|
||||||
resolver
|
resolver
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -85,6 +96,16 @@ export const setupValueSuggestionProvider = (
|
||||||
: undefined;
|
: undefined;
|
||||||
const filterQuery = timeFilter ? buildQueryFromFilters([timeFilter], indexPattern).filter : [];
|
const filterQuery = timeFilter ? buildQueryFromFilters([timeFilter], indexPattern).filter : [];
|
||||||
const filters = [...(boolFilter ? boolFilter : []), ...filterQuery];
|
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 {
|
return {
|
||||||
autocomplete: this.autocomplete.setup(core, { timefilter: queryService.timefilter }),
|
autocomplete: this.autocomplete.setup(core, {
|
||||||
|
timefilter: queryService.timefilter,
|
||||||
|
usageCollection,
|
||||||
|
}),
|
||||||
search: searchService,
|
search: searchService,
|
||||||
fieldFormats: this.fieldFormatsService.setup(core),
|
fieldFormats: this.fieldFormatsService.setup(core),
|
||||||
query: queryService,
|
query: queryService,
|
||||||
|
@ -195,10 +198,7 @@ export class DataPublicPlugin
|
||||||
core,
|
core,
|
||||||
data: dataServices,
|
data: dataServices,
|
||||||
storage: this.storage,
|
storage: this.storage,
|
||||||
trackUiMetric: this.usageCollection?.reportUiCounter.bind(
|
usageCollection: this.usageCollection,
|
||||||
this.usageCollection,
|
|
||||||
'data_plugin'
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -105,7 +105,6 @@ import { Type } from '@kbn/config-schema';
|
||||||
import { TypeOf } from '@kbn/config-schema';
|
import { TypeOf } from '@kbn/config-schema';
|
||||||
import { UiActionsSetup } from 'src/plugins/ui_actions/public';
|
import { UiActionsSetup } from 'src/plugins/ui_actions/public';
|
||||||
import { UiActionsStart } 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 { Unit } from '@elastic/datemath';
|
||||||
import { UnregisterCallback } from 'history';
|
import { UnregisterCallback } from 'history';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
|
@ -1094,6 +1093,10 @@ export interface IDataPluginServices extends Partial<CoreStart_2> {
|
||||||
storage: IStorageWrapper;
|
storage: IStorageWrapper;
|
||||||
// (undocumented)
|
// (undocumented)
|
||||||
uiSettings: CoreStart_2['uiSettings'];
|
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)
|
// 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)
|
// 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)
|
// @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> & {
|
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" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated"> & ReactIntl.InjectedIntlProps>;
|
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
|
// 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 { QuerySetup, QueryStart } from './query';
|
||||||
import { IndexPatternsContract } from './index_patterns';
|
import { IndexPatternsContract } from './index_patterns';
|
||||||
import { IndexPatternSelectProps, StatefulSearchBarProps } from './ui';
|
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 { Setup as InspectorSetup } from '../../inspector/public';
|
||||||
import { NowProviderPublicContract } from './now_provider';
|
import { NowProviderPublicContract } from './now_provider';
|
||||||
|
|
||||||
|
@ -120,4 +120,5 @@ export interface IDataPluginServices extends Partial<CoreStart> {
|
||||||
http: CoreStart['http'];
|
http: CoreStart['http'];
|
||||||
storage: IStorageWrapper;
|
storage: IStorageWrapper;
|
||||||
data: DataPublicPluginStart;
|
data: DataPublicPluginStart;
|
||||||
|
usageCollection?: UsageCollectionStart;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,12 +11,12 @@ import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { METRIC_TYPE, UiCounterMetricType } from '@kbn/analytics';
|
import { METRIC_TYPE } from '@kbn/analytics';
|
||||||
import { FilterEditor } from './filter_editor';
|
import { FilterEditor } from './filter_editor';
|
||||||
import { FILTER_EDITOR_WIDTH, FilterItem } from './filter_item';
|
import { FILTER_EDITOR_WIDTH, FilterItem } from './filter_item';
|
||||||
import { FilterOptions } from './filter_options';
|
import { FilterOptions } from './filter_options';
|
||||||
import { useKibana } from '../../../../kibana_react/public';
|
import { useKibana } from '../../../../kibana_react/public';
|
||||||
import { IIndexPattern } from '../..';
|
import { IDataPluginServices, IIndexPattern } from '../..';
|
||||||
import {
|
import {
|
||||||
buildEmptyFilter,
|
buildEmptyFilter,
|
||||||
Filter,
|
Filter,
|
||||||
|
@ -36,17 +36,16 @@ interface Props {
|
||||||
indexPatterns: IIndexPattern[];
|
indexPatterns: IIndexPattern[];
|
||||||
intl: InjectedIntl;
|
intl: InjectedIntl;
|
||||||
appName: string;
|
appName: string;
|
||||||
// Track UI Metrics
|
|
||||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function FilterBarUI(props: Props) {
|
function FilterBarUI(props: Props) {
|
||||||
const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false);
|
const [isAddFilterPopoverOpen, setIsAddFilterPopoverOpen] = useState(false);
|
||||||
const kibana = useKibana();
|
const kibana = useKibana<IDataPluginServices>();
|
||||||
|
const { appName, usageCollection, uiSettings } = kibana.services;
|
||||||
const uiSettings = kibana.services.uiSettings;
|
|
||||||
if (!uiSettings) return null;
|
if (!uiSettings) return null;
|
||||||
|
|
||||||
|
const reportUiCounter = usageCollection?.reportUiCounter.bind(usageCollection, appName);
|
||||||
|
|
||||||
function onFiltersUpdated(filters: Filter[]) {
|
function onFiltersUpdated(filters: Filter[]) {
|
||||||
if (props.onFiltersUpdated) {
|
if (props.onFiltersUpdated) {
|
||||||
props.onFiltersUpdated(filters);
|
props.onFiltersUpdated(filters);
|
||||||
|
@ -119,66 +118,65 @@ function FilterBarUI(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAdd(filter: Filter) {
|
function onAdd(filter: Filter) {
|
||||||
|
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:added`);
|
||||||
setIsAddFilterPopoverOpen(false);
|
setIsAddFilterPopoverOpen(false);
|
||||||
if (props.trackUiMetric) {
|
|
||||||
props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_added`);
|
|
||||||
}
|
|
||||||
const filters = [...props.filters, filter];
|
const filters = [...props.filters, filter];
|
||||||
onFiltersUpdated(filters);
|
onFiltersUpdated(filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRemove(i: number) {
|
function onRemove(i: number) {
|
||||||
|
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:removed`);
|
||||||
const filters = [...props.filters];
|
const filters = [...props.filters];
|
||||||
filters.splice(i, 1);
|
filters.splice(i, 1);
|
||||||
onFiltersUpdated(filters);
|
onFiltersUpdated(filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUpdate(i: number, filter: Filter) {
|
function onUpdate(i: number, filter: Filter) {
|
||||||
if (props.trackUiMetric) {
|
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:edited`);
|
||||||
props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_edited`);
|
|
||||||
}
|
|
||||||
const filters = [...props.filters];
|
const filters = [...props.filters];
|
||||||
filters[i] = filter;
|
filters[i] = filter;
|
||||||
onFiltersUpdated(filters);
|
onFiltersUpdated(filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onEnableAll() {
|
function onEnableAll() {
|
||||||
|
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:enable_all`);
|
||||||
const filters = props.filters.map(enableFilter);
|
const filters = props.filters.map(enableFilter);
|
||||||
onFiltersUpdated(filters);
|
onFiltersUpdated(filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDisableAll() {
|
function onDisableAll() {
|
||||||
|
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:disable_all`);
|
||||||
const filters = props.filters.map(disableFilter);
|
const filters = props.filters.map(disableFilter);
|
||||||
onFiltersUpdated(filters);
|
onFiltersUpdated(filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPinAll() {
|
function onPinAll() {
|
||||||
|
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:pin_all`);
|
||||||
const filters = props.filters.map(pinFilter);
|
const filters = props.filters.map(pinFilter);
|
||||||
onFiltersUpdated(filters);
|
onFiltersUpdated(filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUnpinAll() {
|
function onUnpinAll() {
|
||||||
|
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:unpin_all`);
|
||||||
const filters = props.filters.map(unpinFilter);
|
const filters = props.filters.map(unpinFilter);
|
||||||
onFiltersUpdated(filters);
|
onFiltersUpdated(filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onToggleAllNegated() {
|
function onToggleAllNegated() {
|
||||||
if (props.trackUiMetric) {
|
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:invert_all`);
|
||||||
props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_invertInclusion`);
|
|
||||||
}
|
|
||||||
const filters = props.filters.map(toggleFilterNegated);
|
const filters = props.filters.map(toggleFilterNegated);
|
||||||
onFiltersUpdated(filters);
|
onFiltersUpdated(filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onToggleAllDisabled() {
|
function onToggleAllDisabled() {
|
||||||
if (props.trackUiMetric) {
|
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:toggle_all`);
|
||||||
props.trackUiMetric(METRIC_TYPE.CLICK, `${props.appName}:filter_toggleAllDisabled`);
|
|
||||||
}
|
|
||||||
const filters = props.filters.map(toggleFilterDisabled);
|
const filters = props.filters.map(toggleFilterDisabled);
|
||||||
onFiltersUpdated(filters);
|
onFiltersUpdated(filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRemoveAll() {
|
function onRemoveAll() {
|
||||||
|
reportUiCounter?.(METRIC_TYPE.CLICK, `filter:remove_all`);
|
||||||
onFiltersUpdated([]);
|
onFiltersUpdated([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { debounce, compact, isEqual, isFunction } from 'lodash';
|
import { debounce, compact, isEqual, isFunction } from 'lodash';
|
||||||
import { Toast } from 'src/core/public';
|
import { Toast } from 'src/core/public';
|
||||||
|
import { METRIC_TYPE } from '@kbn/analytics';
|
||||||
import { IDataPluginServices, IIndexPattern, Query } from '../..';
|
import { IDataPluginServices, IIndexPattern, Query } from '../..';
|
||||||
import { QuerySuggestion, QuerySuggestionTypes } from '../../autocomplete';
|
import { QuerySuggestion, QuerySuggestionTypes } from '../../autocomplete';
|
||||||
|
|
||||||
|
@ -105,6 +106,10 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
||||||
private abortController?: AbortController;
|
private abortController?: AbortController;
|
||||||
private fetchIndexPatternsAbortController?: AbortController;
|
private fetchIndexPatternsAbortController?: AbortController;
|
||||||
private services = this.props.kibana.services;
|
private services = this.props.kibana.services;
|
||||||
|
private reportUiCounter = this.services.usageCollection?.reportUiCounter.bind(
|
||||||
|
this.services.usageCollection,
|
||||||
|
this.services.appName
|
||||||
|
);
|
||||||
private componentIsUnmounting = false;
|
private componentIsUnmounting = false;
|
||||||
private queryBarInputDivRefInstance: RefObject<HTMLDivElement> = createRef();
|
private queryBarInputDivRefInstance: RefObject<HTMLDivElement> = createRef();
|
||||||
|
|
||||||
|
@ -178,12 +183,14 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
||||||
selectionEnd,
|
selectionEnd,
|
||||||
signal: this.abortController.signal,
|
signal: this.abortController.signal,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
return [...suggestions, ...recentSearchSuggestions];
|
return [...suggestions, ...recentSearchSuggestions];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// TODO: Waiting on https://github.com/elastic/kibana/issues/51406 for a properly typed error
|
// TODO: Waiting on https://github.com/elastic/kibana/issues/51406 for a properly typed error
|
||||||
// Ignore aborted requests
|
// Ignore aborted requests
|
||||||
if (e.message === 'The user aborted a request.') return;
|
if (e.message === 'The user aborted a request.') return;
|
||||||
|
|
||||||
|
this.reportUiCounter?.(METRIC_TYPE.LOADED, `query_string:suggestions_error`);
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -302,7 +309,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
||||||
}
|
}
|
||||||
if (isSuggestionsVisible && index !== null && this.state.suggestions[index]) {
|
if (isSuggestionsVisible && index !== null && this.state.suggestions[index]) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.selectSuggestion(this.state.suggestions[index]);
|
this.selectSuggestion(this.state.suggestions[index], index);
|
||||||
} else {
|
} else {
|
||||||
this.onSubmit(this.props.query);
|
this.onSubmit(this.props.query);
|
||||||
this.setState({
|
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) {
|
if (!this.inputRef) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -352,6 +359,17 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
||||||
const value = query.substr(0, selectionStart) + query.substr(selectionEnd);
|
const value = query.substr(0, selectionStart) + query.substr(selectionEnd);
|
||||||
const newQueryString = value.substr(0, start) + text + value.substr(end);
|
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.onQueryStringChange(newQueryString);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -458,6 +476,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
||||||
const newQuery = { query: '', language };
|
const newQuery = { query: '', language };
|
||||||
this.onChange(newQuery);
|
this.onChange(newQuery);
|
||||||
this.onSubmit(newQuery);
|
this.onSubmit(newQuery);
|
||||||
|
this.reportUiCounter?.(METRIC_TYPE.LOADED, `query_string:language:${language}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onOutsideClick = () => {
|
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) {
|
if (!this.inputRef) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.selectSuggestion(suggestion);
|
this.selectSuggestion(suggestion, index);
|
||||||
this.inputRef.focus();
|
this.inputRef.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -588,6 +607,7 @@ export default class QueryStringInputUI extends Component<Props, State> {
|
||||||
if (this.props.onChangeQueryInputFocus) {
|
if (this.props.onChangeQueryInputFocus) {
|
||||||
this.props.onChangeQueryInputFocus(true);
|
this.props.onChangeQueryInputFocus(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this.handleAutoHeight();
|
this.handleAutoHeight();
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,7 +10,6 @@ import _ from 'lodash';
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { CoreStart } from 'src/core/public';
|
import { CoreStart } from 'src/core/public';
|
||||||
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
|
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
|
||||||
import { UiCounterMetricType } from '@kbn/analytics';
|
|
||||||
import { KibanaContextProvider } from '../../../../kibana_react/public';
|
import { KibanaContextProvider } from '../../../../kibana_react/public';
|
||||||
import { QueryStart, SavedQuery } from '../../query';
|
import { QueryStart, SavedQuery } from '../../query';
|
||||||
import { SearchBar, SearchBarOwnProps } from './';
|
import { SearchBar, SearchBarOwnProps } from './';
|
||||||
|
@ -20,12 +19,13 @@ import { useSavedQuery } from './lib/use_saved_query';
|
||||||
import { DataPublicPluginStart } from '../../types';
|
import { DataPublicPluginStart } from '../../types';
|
||||||
import { Filter, Query, TimeRange } from '../../../common';
|
import { Filter, Query, TimeRange } from '../../../common';
|
||||||
import { useQueryStringManager } from './lib/use_query_string_manager';
|
import { useQueryStringManager } from './lib/use_query_string_manager';
|
||||||
|
import { UsageCollectionSetup } from '../../../../usage_collection/public';
|
||||||
|
|
||||||
interface StatefulSearchBarDeps {
|
interface StatefulSearchBarDeps {
|
||||||
core: CoreStart;
|
core: CoreStart;
|
||||||
data: Omit<DataPublicPluginStart, 'ui'>;
|
data: Omit<DataPublicPluginStart, 'ui'>;
|
||||||
storage: IStorageWrapper;
|
storage: IStorageWrapper;
|
||||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
usageCollection?: UsageCollectionSetup;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StatefulSearchBarProps = SearchBarOwnProps & {
|
export type StatefulSearchBarProps = SearchBarOwnProps & {
|
||||||
|
@ -110,7 +110,7 @@ const overrideDefaultBehaviors = (props: StatefulSearchBarProps) => {
|
||||||
return props.useDefaultBehaviors ? {} : props;
|
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.
|
// 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.
|
// Until it's available, we'll ask the user to provide it for the pre-wired component.
|
||||||
return (props: StatefulSearchBarProps) => {
|
return (props: StatefulSearchBarProps) => {
|
||||||
|
@ -161,6 +161,7 @@ export function createSearchBar({ core, storage, data, trackUiMetric }: Stateful
|
||||||
appName: props.appName,
|
appName: props.appName,
|
||||||
data,
|
data,
|
||||||
storage,
|
storage,
|
||||||
|
usageCollection,
|
||||||
...core,
|
...core,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -188,7 +189,6 @@ export function createSearchBar({ core, storage, data, trackUiMetric }: Stateful
|
||||||
onClearSavedQuery={defaultOnClearSavedQuery(props, clearSavedQuery)}
|
onClearSavedQuery={defaultOnClearSavedQuery(props, clearSavedQuery)}
|
||||||
onSavedQueryUpdated={defaultOnSavedQueryUpdated(props, setSavedQuery)}
|
onSavedQueryUpdated={defaultOnSavedQueryUpdated(props, setSavedQuery)}
|
||||||
onSaved={defaultOnSavedQueryUpdated(props, setSavedQuery)}
|
onSaved={defaultOnSavedQueryUpdated(props, setSavedQuery)}
|
||||||
trackUiMetric={trackUiMetric}
|
|
||||||
{...overrideDefaultBehaviors(props)}
|
{...overrideDefaultBehaviors(props)}
|
||||||
/>
|
/>
|
||||||
</KibanaContextProvider>
|
</KibanaContextProvider>
|
||||||
|
|
|
@ -13,7 +13,7 @@ import React, { Component } from 'react';
|
||||||
import ResizeObserver from 'resize-observer-polyfill';
|
import ResizeObserver from 'resize-observer-polyfill';
|
||||||
import { get, isEqual } from 'lodash';
|
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 { withKibana, KibanaReactContextValue } from '../../../../kibana_react/public';
|
||||||
|
|
||||||
import QueryBarTopRow from '../query_string_input/query_bar_top_row';
|
import QueryBarTopRow from '../query_string_input/query_bar_top_row';
|
||||||
|
@ -68,8 +68,6 @@ export interface SearchBarOwnProps {
|
||||||
|
|
||||||
onRefresh?: (payload: { dateRange: TimeRange }) => void;
|
onRefresh?: (payload: { dateRange: TimeRange }) => void;
|
||||||
indicateNoData?: boolean;
|
indicateNoData?: boolean;
|
||||||
// Track UI Metrics
|
|
||||||
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps;
|
export type SearchBarProps = SearchBarOwnProps & SearchBarInjectedDeps;
|
||||||
|
@ -323,9 +321,11 @@ class SearchBarUI extends Component<SearchBarProps, State> {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (this.props.trackUiMetric) {
|
this.services.usageCollection?.reportUiCounter(
|
||||||
this.props.trackUiMetric(METRIC_TYPE.CLICK, `${this.services.appName}:query_submitted`);
|
this.services.appName,
|
||||||
}
|
METRIC_TYPE.CLICK,
|
||||||
|
'query_submitted'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -428,7 +428,6 @@ class SearchBarUI extends Component<SearchBarProps, State> {
|
||||||
onFiltersUpdated={this.props.onFiltersUpdated}
|
onFiltersUpdated={this.props.onFiltersUpdated}
|
||||||
indexPatterns={this.props.indexPatterns!}
|
indexPatterns={this.props.indexPatterns!}
|
||||||
appName={this.services.appName}
|
appName={this.services.appName}
|
||||||
trackUiMetric={this.props.trackUiMetric}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -22,6 +22,7 @@ exports[`SuggestionsComponent Passing the index should control which suggestion
|
||||||
>
|
>
|
||||||
<SuggestionComponent
|
<SuggestionComponent
|
||||||
ariaId="suggestion-0"
|
ariaId="suggestion-0"
|
||||||
|
index={0}
|
||||||
innerRef={[Function]}
|
innerRef={[Function]}
|
||||||
key="value - as promised, not helpful"
|
key="value - as promised, not helpful"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
@ -40,6 +41,7 @@ exports[`SuggestionsComponent Passing the index should control which suggestion
|
||||||
/>
|
/>
|
||||||
<SuggestionComponent
|
<SuggestionComponent
|
||||||
ariaId="suggestion-1"
|
ariaId="suggestion-1"
|
||||||
|
index={1}
|
||||||
innerRef={[Function]}
|
innerRef={[Function]}
|
||||||
key="field - yep"
|
key="field - yep"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
@ -84,6 +86,7 @@ exports[`SuggestionsComponent Should display given suggestions if the show prop
|
||||||
>
|
>
|
||||||
<SuggestionComponent
|
<SuggestionComponent
|
||||||
ariaId="suggestion-0"
|
ariaId="suggestion-0"
|
||||||
|
index={0}
|
||||||
innerRef={[Function]}
|
innerRef={[Function]}
|
||||||
key="value - as promised, not helpful"
|
key="value - as promised, not helpful"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
@ -102,6 +105,7 @@ exports[`SuggestionsComponent Should display given suggestions if the show prop
|
||||||
/>
|
/>
|
||||||
<SuggestionComponent
|
<SuggestionComponent
|
||||||
ariaId="suggestion-1"
|
ariaId="suggestion-1"
|
||||||
|
index={1}
|
||||||
innerRef={[Function]}
|
innerRef={[Function]}
|
||||||
key="field - yep"
|
key="field - yep"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
|
|
|
@ -27,6 +27,7 @@ describe('SuggestionComponent', () => {
|
||||||
it('Should display the suggestion and use the provided ariaId', () => {
|
it('Should display the suggestion and use the provided ariaId', () => {
|
||||||
const component = shallow(
|
const component = shallow(
|
||||||
<SuggestionComponent
|
<SuggestionComponent
|
||||||
|
index={0}
|
||||||
onClick={noop}
|
onClick={noop}
|
||||||
onMouseEnter={noop}
|
onMouseEnter={noop}
|
||||||
selected={false}
|
selected={false}
|
||||||
|
@ -43,6 +44,7 @@ describe('SuggestionComponent', () => {
|
||||||
it('Should make the element active if the selected prop is true', () => {
|
it('Should make the element active if the selected prop is true', () => {
|
||||||
const component = shallow(
|
const component = shallow(
|
||||||
<SuggestionComponent
|
<SuggestionComponent
|
||||||
|
index={0}
|
||||||
onClick={noop}
|
onClick={noop}
|
||||||
onMouseEnter={noop}
|
onMouseEnter={noop}
|
||||||
selected={true}
|
selected={true}
|
||||||
|
@ -64,6 +66,7 @@ describe('SuggestionComponent', () => {
|
||||||
|
|
||||||
mount(
|
mount(
|
||||||
<SuggestionComponent
|
<SuggestionComponent
|
||||||
|
index={0}
|
||||||
onClick={noop}
|
onClick={noop}
|
||||||
onMouseEnter={noop}
|
onMouseEnter={noop}
|
||||||
selected={false}
|
selected={false}
|
||||||
|
@ -80,6 +83,7 @@ describe('SuggestionComponent', () => {
|
||||||
|
|
||||||
const component = shallow(
|
const component = shallow(
|
||||||
<SuggestionComponent
|
<SuggestionComponent
|
||||||
|
index={0}
|
||||||
onClick={mockHandler}
|
onClick={mockHandler}
|
||||||
onMouseEnter={noop}
|
onMouseEnter={noop}
|
||||||
selected={false}
|
selected={false}
|
||||||
|
@ -92,7 +96,7 @@ describe('SuggestionComponent', () => {
|
||||||
|
|
||||||
component.simulate('click');
|
component.simulate('click');
|
||||||
expect(mockHandler).toHaveBeenCalledTimes(1);
|
expect(mockHandler).toHaveBeenCalledTimes(1);
|
||||||
expect(mockHandler).toHaveBeenCalledWith(mockSuggestion);
|
expect(mockHandler).toHaveBeenCalledWith(mockSuggestion, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should call onMouseEnter when user mouses over the element', () => {
|
it('Should call onMouseEnter when user mouses over the element', () => {
|
||||||
|
@ -100,6 +104,7 @@ describe('SuggestionComponent', () => {
|
||||||
|
|
||||||
const component = shallow(
|
const component = shallow(
|
||||||
<SuggestionComponent
|
<SuggestionComponent
|
||||||
|
index={0}
|
||||||
onClick={noop}
|
onClick={noop}
|
||||||
onMouseEnter={mockHandler}
|
onMouseEnter={mockHandler}
|
||||||
selected={false}
|
selected={false}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { EuiIcon } from '@elastic/eui';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { QuerySuggestion } from '../../autocomplete';
|
import { QuerySuggestion } from '../../autocomplete';
|
||||||
|
import { SuggestionOnClick } from './types';
|
||||||
|
|
||||||
function getEuiIconType(type: string) {
|
function getEuiIconType(type: string) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
@ -29,9 +30,10 @@ function getEuiIconType(type: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClick: (suggestion: QuerySuggestion) => void;
|
onClick: SuggestionOnClick;
|
||||||
onMouseEnter: () => void;
|
onMouseEnter: () => void;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
|
index: number;
|
||||||
suggestion: QuerySuggestion;
|
suggestion: QuerySuggestion;
|
||||||
innerRef: (node: HTMLDivElement) => void;
|
innerRef: (node: HTMLDivElement) => void;
|
||||||
ariaId: string;
|
ariaId: string;
|
||||||
|
@ -48,7 +50,7 @@ export function SuggestionComponent(props: Props) {
|
||||||
active: props.selected,
|
active: props.selected,
|
||||||
})}
|
})}
|
||||||
role="option"
|
role="option"
|
||||||
onClick={() => props.onClick(props.suggestion)}
|
onClick={() => props.onClick(props.suggestion, props.index)}
|
||||||
onMouseEnter={props.onMouseEnter}
|
onMouseEnter={props.onMouseEnter}
|
||||||
ref={props.innerRef}
|
ref={props.innerRef}
|
||||||
id={props.ariaId}
|
id={props.ariaId}
|
||||||
|
|
|
@ -115,7 +115,7 @@ describe('SuggestionsComponent', () => {
|
||||||
|
|
||||||
component.find(SuggestionComponent).at(1).simulate('click');
|
component.find(SuggestionComponent).at(1).simulate('click');
|
||||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
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', () => {
|
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_TOP_OFFSET,
|
||||||
SUGGESTIONS_LIST_REQUIRED_WIDTH,
|
SUGGESTIONS_LIST_REQUIRED_WIDTH,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
import { SuggestionOnClick } from './types';
|
||||||
|
|
||||||
// @internal
|
// @internal
|
||||||
export interface SuggestionsComponentProps {
|
export interface SuggestionsComponentProps {
|
||||||
index: number | null;
|
index: number | null;
|
||||||
onClick: (suggestion: QuerySuggestion) => void;
|
onClick: SuggestionOnClick;
|
||||||
onMouseEnter: (index: number) => void;
|
onMouseEnter: (index: number) => void;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
suggestions: QuerySuggestion[];
|
suggestions: QuerySuggestion[];
|
||||||
|
@ -50,6 +51,7 @@ export default class SuggestionsComponent extends Component<SuggestionsComponent
|
||||||
<SuggestionComponent
|
<SuggestionComponent
|
||||||
innerRef={(node) => (this.childNodes[index] = node)}
|
innerRef={(node) => (this.childNodes[index] = node)}
|
||||||
selected={index === this.props.index}
|
selected={index === this.props.index}
|
||||||
|
index={index}
|
||||||
suggestion={suggestion}
|
suggestion={suggestion}
|
||||||
onClick={this.props.onClick}
|
onClick={this.props.onClick}
|
||||||
onMouseEnter={() => this.props.onMouseEnter(index)}
|
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