Throttle toast storms related to saving/restoring URL state (#154792)

## Summary

Resolves https://github.com/elastic/kibana/issues/153073.

Throttles toast errors related to restoring/saving URL state.

Before:

<img width="330" alt="image"
src="https://user-images.githubusercontent.com/1178348/231315783-98b966a8-b665-4465-91ae-b88933a56c3b.png">

After:

<img width="333" alt="image"
src="https://user-images.githubusercontent.com/1178348/231315724-7b3109eb-bdc7-405f-865f-36c909b907f4.png">

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Lukas Olson 2023-04-13 12:05:47 -07:00 committed by GitHub
parent 61f212f75c
commit dc1baa9758
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 65 additions and 12 deletions

View file

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

View file

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

View file

@ -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';

View file

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

View file

@ -20,6 +20,7 @@
"@kbn/test-jest-helpers",
"@kbn/rison",
"@kbn/crypto-browser",
"@kbn/core-notifications-browser-mocks",
],
"exclude": [
"target/**/*",