diff --git a/src/plugins/data_views/common/data_views/data_views.ts b/src/plugins/data_views/common/data_views/data_views.ts index 098e895324aa7..b263c7d77fe3b 100644 --- a/src/plugins/data_views/common/data_views/data_views.ts +++ b/src/plugins/data_views/common/data_views/data_views.ts @@ -113,7 +113,17 @@ export class DataViewsService { private savedObjectsCache?: Array> | null; private apiClient: IDataViewsApiClient; private fieldFormats: FieldFormatsStartCommon; + /** + * Handler for service notifications + * @param toastInputFields notification content in toast format + * @param key used to indicate uniqueness of the notification + */ private onNotification: OnNotification; + /* + * Handler for service errors + * @param error notification content in toast format + * @param key used to indicate uniqueness of the error + */ private onError: OnError; private dataViewCache: ReturnType; public getCanSave: () => Promise; @@ -333,15 +343,22 @@ export class DataViewsService { indexPattern.fields.replaceAll(fieldsWithSavedAttrs); } catch (err) { if (err instanceof DataViewMissingIndices) { - this.onNotification({ title: err.message, color: 'danger', iconType: 'alert' }); + this.onNotification( + { title: err.message, color: 'danger', iconType: 'alert' }, + `refreshFields:${indexPattern.title}` + ); } - this.onError(err, { - title: i18n.translate('dataViews.fetchFieldErrorTitle', { - defaultMessage: 'Error fetching fields for data view {title} (ID: {id})', - values: { id: indexPattern.id, title: indexPattern.title }, - }), - }); + this.onError( + err, + { + title: i18n.translate('dataViews.fetchFieldErrorTitle', { + defaultMessage: 'Error fetching fields for data view {title} (ID: {id})', + values: { id: indexPattern.id, title: indexPattern.title }, + }), + }, + indexPattern.title + ); } }; @@ -378,16 +395,23 @@ export class DataViewsService { return this.fieldArrayToMap(updatedFieldList, fieldAttrs); } catch (err) { if (err instanceof DataViewMissingIndices) { - this.onNotification({ title: err.message, color: 'danger', iconType: 'alert' }); + this.onNotification( + { title: err.message, color: 'danger', iconType: 'alert' }, + `refreshFieldSpecMap:${title}` + ); return {}; } - this.onError(err, { - title: i18n.translate('dataViews.fetchFieldErrorTitle', { - defaultMessage: 'Error fetching fields for data view {title} (ID: {id})', - values: { id, title }, - }), - }); + this.onError( + err, + { + title: i18n.translate('dataViews.fetchFieldErrorTitle', { + defaultMessage: 'Error fetching fields for data view {title} (ID: {id})', + values: { id, title }, + }), + }, + title + ); throw err; } }; @@ -530,18 +554,25 @@ export class DataViewsService { } } catch (err) { if (err instanceof DataViewMissingIndices) { - this.onNotification({ - title: err.message, - color: 'danger', - iconType: 'alert', - }); + this.onNotification( + { + title: err.message, + color: 'danger', + iconType: 'alert', + }, + `initFromSavedObject:${title}` + ); } else { - this.onError(err, { - title: i18n.translate('dataViews.fetchFieldErrorTitle', { - defaultMessage: 'Error fetching fields for data view {title} (ID: {id})', - values: { id: savedObject.id, title }, - }), - }); + this.onError( + err, + { + title: i18n.translate('dataViews.fetchFieldErrorTitle', { + defaultMessage: 'Error fetching fields for data view {title} (ID: {id})', + values: { id: savedObject.id, title }, + }), + }, + title || '' + ); } } @@ -718,7 +749,10 @@ export class DataViewsService { 'Unable to write data view! Refresh the page to get the most up to date changes for this data view.', }); - this.onNotification({ title, color: 'danger' }); + this.onNotification( + { title, color: 'danger' }, + `updateSavedObject:${indexPattern.title}` + ); throw err; } diff --git a/src/plugins/data_views/common/types.ts b/src/plugins/data_views/common/types.ts index 4032d83b24c5a..f4bed383d8447 100644 --- a/src/plugins/data_views/common/types.ts +++ b/src/plugins/data_views/common/types.ts @@ -123,8 +123,8 @@ export interface FieldAttrSet { count?: number; } -export type OnNotification = (toastInputFields: ToastInputFields) => void; -export type OnError = (error: Error, toastInputFields: ErrorToastOptions) => void; +export type OnNotification = (toastInputFields: ToastInputFields, key: string) => void; +export type OnError = (error: Error, toastInputFields: ErrorToastOptions, key: string) => void; export interface UiSettingsCommon { get: (key: string) => Promise; diff --git a/src/plugins/data_views/public/debounce_by_key.test.ts b/src/plugins/data_views/public/debounce_by_key.test.ts new file mode 100644 index 0000000000000..c5fba82fdcfdf --- /dev/null +++ b/src/plugins/data_views/public/debounce_by_key.test.ts @@ -0,0 +1,31 @@ +/* + * 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 { debounceByKey } from './debounce_by_key'; + +describe('debounceByKey', () => { + test('debounce, confirm params', async () => { + const fn = jest.fn(); + const fn2 = jest.fn(); + + const debouncedFn = debounceByKey(fn, 1000); + const debouncedFn2 = debounceByKey(fn2, 1000); + + // debounces based on key, not params + debouncedFn('a')(1); + debouncedFn('a')(2); + + debouncedFn2('b')(2); + debouncedFn2('b')(1); + + expect(fn).toBeCalledTimes(1); + expect(fn).toBeCalledWith(1); + expect(fn2).toBeCalledTimes(1); + expect(fn2).toBeCalledWith(2); + }); +}); diff --git a/src/plugins/data_views/public/debounce_by_key.ts b/src/plugins/data_views/public/debounce_by_key.ts new file mode 100644 index 0000000000000..c8ae7094a6437 --- /dev/null +++ b/src/plugins/data_views/public/debounce_by_key.ts @@ -0,0 +1,24 @@ +/* + * 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 { debounce } from 'lodash'; + +export const debounceByKey = any>( + fn: F, + waitInMs: number +): ((key: string) => F) => { + const debouncerCollector: Record = {}; + return (key: string) => { + if (!debouncerCollector[key]) { + debouncerCollector[key] = debounce(fn, waitInMs, { + leading: true, + }); + } + return debouncerCollector[key]; + }; +}; diff --git a/src/plugins/data_views/public/plugin.ts b/src/plugins/data_views/public/plugin.ts index 95378df130c46..5c3ad2c33307d 100644 --- a/src/plugins/data_views/public/plugin.ts +++ b/src/plugins/data_views/public/plugin.ts @@ -25,6 +25,8 @@ import { import { DataViewsServicePublic } from './data_views_service_public'; import { HasData } from './services'; +import { debounceByKey } from './debounce_by_key'; + export class DataViewsPublicPlugin implements Plugin< @@ -50,16 +52,28 @@ export class DataViewsPublicPlugin { fieldFormats }: DataViewsPublicStartDependencies ): DataViewsPublicPluginStart { const { uiSettings, http, notifications, savedObjects, theme, overlays, application } = core; + + const onNotifDebounced = debounceByKey( + notifications.toasts.add.bind(notifications.toasts), + 10000 + ); + const onErrorDebounced = debounceByKey( + notifications.toasts.addError.bind(notifications.toasts), + 10000 + ); + return new DataViewsServicePublic({ hasData: this.hasData.start(core), uiSettings: new UiSettingsPublicToCommon(uiSettings), savedObjectsClient: new SavedObjectsClientPublicToCommon(savedObjects.client), apiClient: new DataViewsApiClient(http), fieldFormats, - onNotification: (toastInputFields) => { - notifications.toasts.add(toastInputFields); + onNotification: (toastInputFields, key) => { + onNotifDebounced(key)(toastInputFields); + }, + onError: (error, toastInputFields, key) => { + onErrorDebounced(key)(error, toastInputFields); }, - onError: notifications.toasts.addError.bind(notifications.toasts), onRedirectNoIndexPattern: onRedirectNoIndexPattern( application.capabilities, application.navigateToApp,