diff --git a/src/plugins/kibana_utils/public/state_management/url/errors.test.ts b/src/plugins/kibana_utils/public/state_management/url/errors.test.ts new file mode 100644 index 0000000000000..d70b49471735a --- /dev/null +++ b/src/plugins/kibana_utils/public/state_management/url/errors.test.ts @@ -0,0 +1,34 @@ +/* + * 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 { withNotifyOnErrors } from './errors'; +import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; + +describe('state management URL errors', () => { + const notifications = notificationServiceMock.createStartContract(); + + beforeEach(() => { + notifications.toasts.addError.mockClear(); + }); + + test('notifies on restore error only once', () => { + const { onGetError } = withNotifyOnErrors(notifications.toasts); + const error = new Error(); + onGetError(error); + onGetError(error); + expect(notifications.toasts.addError).toBeCalledTimes(1); + }); + + test('notifies on save error only once', () => { + const { onSetError } = withNotifyOnErrors(notifications.toasts); + const error = new Error(); + onSetError(error); + onSetError(error); + expect(notifications.toasts.addError).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/kibana_utils/public/state_management/url/errors.ts b/src/plugins/kibana_utils/public/state_management/url/errors.ts index 184ac5d128bd1..e64e28bd67a44 100644 --- a/src/plugins/kibana_utils/public/state_management/url/errors.ts +++ b/src/plugins/kibana_utils/public/state_management/url/errors.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { throttle } from 'lodash'; import { i18n } from '@kbn/i18n'; import { NotificationsStart } from '@kbn/core/public'; @@ -23,6 +24,24 @@ export const saveStateInUrlErrorTitle = i18n.translate( } ); +// Prevent toast storms by throttling. See https://github.com/elastic/kibana/issues/153073 +const throttledOnRestoreError = throttle((toasts: NotificationsStart['toasts'], e: Error) => { + toasts.addError(e, { + title: restoreUrlErrorTitle, + }); +}, 10000); +const throttledOnSaveError = throttle((toasts: NotificationsStart['toasts'], e: Error) => { + toasts.addError(e, { + title: saveStateInUrlErrorTitle, + }); +}, 10000); + +// Helper to bypass throttling if consumers need to handle errors right away +export const flushNotifyOnErrors = () => { + throttledOnRestoreError.flush(); + throttledOnSaveError.flush(); +}; + /** * Helper for configuring {@link IKbnUrlStateStorage} to notify about inner errors * @@ -37,15 +56,7 @@ export const saveStateInUrlErrorTitle = i18n.translate( */ export const withNotifyOnErrors = (toasts: NotificationsStart['toasts']) => { return { - onGetError: (error: Error) => { - toasts.addError(error, { - title: restoreUrlErrorTitle, - }); - }, - onSetError: (error: Error) => { - toasts.addError(error, { - title: saveStateInUrlErrorTitle, - }); - }, + onGetError: (e: Error) => throttledOnRestoreError(toasts, e), + onSetError: (e: Error) => throttledOnSaveError(toasts, e), }; }; diff --git a/src/plugins/kibana_utils/public/state_management/url/index.ts b/src/plugins/kibana_utils/public/state_management/url/index.ts index 5f45b5fee0a74..1743777b70100 100644 --- a/src/plugins/kibana_utils/public/state_management/url/index.ts +++ b/src/plugins/kibana_utils/public/state_management/url/index.ts @@ -16,4 +16,9 @@ export { } from './kbn_url_storage'; export { createKbnUrlTracker } from './kbn_url_tracker'; export { createUrlTracker } from './url_tracker'; -export { withNotifyOnErrors, saveStateInUrlErrorTitle, restoreUrlErrorTitle } from './errors'; +export { + withNotifyOnErrors, + flushNotifyOnErrors, + saveStateInUrlErrorTitle, + restoreUrlErrorTitle, +} from './errors'; diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts index f52ed8ee9c525..4ce1e906a9ccc 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.test.ts @@ -12,7 +12,7 @@ import { History, createBrowserHistory } from 'history'; import { takeUntil, toArray } from 'rxjs/operators'; import { Subject } from 'rxjs'; import { CoreScopedHistory } from '@kbn/core/public'; -import { withNotifyOnErrors } from '../../state_management/url'; +import { withNotifyOnErrors, flushNotifyOnErrors } from '../../state_management/url'; import { coreMock } from '@kbn/core/public/mocks'; describe('KbnUrlStateStorage', () => { @@ -123,6 +123,7 @@ describe('KbnUrlStateStorage', () => { const key = '_s'; history.replace(`/#?${key}=(ok:2,test:`); // malformed rison expect(() => urlStateStorage.get(key)).not.toThrow(); + flushNotifyOnErrors(); expect(toasts.addError).toBeCalled(); }); }); @@ -304,6 +305,7 @@ describe('KbnUrlStateStorage', () => { const key = '_s'; history.replace(`/?${key}=(ok:2,test:`); // malformed rison expect(() => urlStateStorage.get(key)).not.toThrow(); + flushNotifyOnErrors(); expect(toasts.addError).toBeCalled(); }); }); diff --git a/src/plugins/kibana_utils/tsconfig.json b/src/plugins/kibana_utils/tsconfig.json index fed20365ae6d3..b711d88dfedf8 100644 --- a/src/plugins/kibana_utils/tsconfig.json +++ b/src/plugins/kibana_utils/tsconfig.json @@ -20,6 +20,7 @@ "@kbn/test-jest-helpers", "@kbn/rison", "@kbn/crypto-browser", + "@kbn/core-notifications-browser-mocks", ], "exclude": [ "target/**/*",