[dataViews] dedupe data view loading toasts (#131779)

* debounce data view toasts

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* more debounce

* fix debounce

* debounce tdata view toast usage

* debounce tdata view toast u

* lets try adding some code comments

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Matthew Kime 2022-05-18 00:12:23 -05:00 committed by GitHub
parent 605020d79c
commit 0f3af54f6e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 134 additions and 31 deletions

View file

@ -113,7 +113,17 @@ export class DataViewsService {
private savedObjectsCache?: Array<SavedObject<IndexPatternSavedObjectAttrs>> | 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<typeof createDataViewCache>;
public getCanSave: () => Promise<boolean>;
@ -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;
}

View file

@ -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: <T = any>(key: string) => Promise<T>;

View file

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

View file

@ -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 = <F extends (...args: any) => any>(
fn: F,
waitInMs: number
): ((key: string) => F) => {
const debouncerCollector: Record<string, F> = {};
return (key: string) => {
if (!debouncerCollector[key]) {
debouncerCollector[key] = debounce(fn, waitInMs, {
leading: true,
});
}
return debouncerCollector[key];
};
};

View file

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