mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Enterprise Search] Add toasts support to FlashMessages component (#95981)
* Add toasts to FlashMessagesLogic + Tests cleanup: - Group actions by their reducer blocks (since flashMessages has such specific logic) - recommend viewing with whitespace changes off for this - Do not reset context between each test, but instead by mount(), which allows tests to maintain state between adding/removing/resetting - Remove '()' from test names (feedback from previous PRs) * Add toast message helpers + refactor FLASH_MESSAGE_TYPES to constants, so that both callouts & toasts can use it effectively * Update FlashMessages to display toasts as well as callouts - This means we can automatically use toasts alongside callouts in all views that already have FlashMessages + a11y enhancement! update callouts to also announce new messages to screenreaders * [Example] Update ApiLogsLogic to flash an error toast on poll + update copy to better match EUI guidelines (shorter) * Fix test caused by new FlashMessages structure * PR suggestion - destructure Co-authored-by: Scotty Bollinger <scotty.bollinger@elastic.co> * PR feedback: implicit return * Fix color types - adding our own string enum fixes the typescript errors that both EuiCallout & EuiToast emit when passing color props to the base EUI types * PR feedback: Update flashToast API to match callout helper API - accepts a string title with optional args, creates a unique ID automatically if missing Co-authored-by: Scotty Bollinger <scotty.bollinger@elastic.co>
This commit is contained in:
parent
fb681d9062
commit
1d58266559
14 changed files with 356 additions and 104 deletions
|
@ -24,6 +24,8 @@ export const mockFlashMessageHelpers = {
|
|||
setQueuedSuccessMessage: jest.fn(),
|
||||
setQueuedErrorMessage: jest.fn(),
|
||||
clearFlashMessages: jest.fn(),
|
||||
flashSuccessToast: jest.fn(),
|
||||
flashErrorToast: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('../shared/flash_messages', () => ({
|
||||
|
|
|
@ -12,14 +12,12 @@ import { nextTick } from '@kbn/test/jest';
|
|||
|
||||
import { DEFAULT_META } from '../../../shared/constants';
|
||||
|
||||
import { POLLING_ERROR_MESSAGE } from './constants';
|
||||
|
||||
import { ApiLogsLogic } from './';
|
||||
|
||||
describe('ApiLogsLogic', () => {
|
||||
const { mount, unmount } = new LogicMounter(ApiLogsLogic);
|
||||
const { http } = mockHttpValues;
|
||||
const { flashAPIErrors, setErrorMessage } = mockFlashMessageHelpers;
|
||||
const { flashAPIErrors, flashErrorToast } = mockFlashMessageHelpers;
|
||||
|
||||
const DEFAULT_VALUES = {
|
||||
dataLoading: true,
|
||||
|
@ -213,7 +211,10 @@ describe('ApiLogsLogic', () => {
|
|||
ApiLogsLogic.actions.fetchApiLogs({ isPoll: true });
|
||||
await nextTick();
|
||||
|
||||
expect(setErrorMessage).toHaveBeenCalledWith(POLLING_ERROR_MESSAGE);
|
||||
expect(flashErrorToast).toHaveBeenCalledWith('Could not refresh API log data', {
|
||||
text: expect.stringContaining('Please check your connection'),
|
||||
toastLifeTimeMs: 3750,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
import { kea, MakeLogicType } from 'kea';
|
||||
|
||||
import { DEFAULT_META } from '../../../shared/constants';
|
||||
import { flashAPIErrors, setErrorMessage } from '../../../shared/flash_messages';
|
||||
import { flashAPIErrors, flashErrorToast } from '../../../shared/flash_messages';
|
||||
import { HttpLogic } from '../../../shared/http';
|
||||
import { updateMetaPageIndex } from '../../../shared/table_pagination';
|
||||
import { EngineLogic } from '../engine';
|
||||
|
||||
import { POLLING_DURATION, POLLING_ERROR_MESSAGE } from './constants';
|
||||
import { POLLING_DURATION, POLLING_ERROR_TITLE, POLLING_ERROR_TEXT } from './constants';
|
||||
import { ApiLogsData, ApiLog } from './types';
|
||||
import { getDateString } from './utils';
|
||||
|
||||
|
@ -117,14 +117,21 @@ export const ApiLogsLogic = kea<MakeLogicType<ApiLogsValues, ApiLogsActions>>({
|
|||
// while polls are stored in-state until the user manually triggers the 'Refresh' action
|
||||
if (isPoll) {
|
||||
actions.onPollInterval(response);
|
||||
flashErrorToast(POLLING_ERROR_TITLE, {
|
||||
text: POLLING_ERROR_TEXT,
|
||||
toastLifeTimeMs: POLLING_DURATION * 0.75,
|
||||
});
|
||||
} else {
|
||||
actions.updateView(response);
|
||||
}
|
||||
} catch (e) {
|
||||
if (isPoll) {
|
||||
// If polling fails, it will typically be due due to http connection -
|
||||
// If polling fails, it will typically be due to http connection -
|
||||
// we should send a more human-readable message if so
|
||||
setErrorMessage(POLLING_ERROR_MESSAGE);
|
||||
flashErrorToast(POLLING_ERROR_TITLE, {
|
||||
text: POLLING_ERROR_TEXT,
|
||||
toastLifeTimeMs: POLLING_DURATION * 0.75,
|
||||
});
|
||||
} else {
|
||||
flashAPIErrors(e);
|
||||
}
|
||||
|
|
|
@ -19,10 +19,11 @@ export const RECENT_API_EVENTS = i18n.translate(
|
|||
|
||||
export const POLLING_DURATION = 5000;
|
||||
|
||||
export const POLLING_ERROR_MESSAGE = i18n.translate(
|
||||
export const POLLING_ERROR_TITLE = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.apiLogs.pollingErrorMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'Could not automatically refresh API logs data. Please check your connection or manually refresh the page.',
|
||||
}
|
||||
{ defaultMessage: 'Could not refresh API log data' }
|
||||
);
|
||||
export const POLLING_ERROR_TEXT = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engine.apiLogs.pollingErrorDescription',
|
||||
{ defaultMessage: 'Please check your connection or manually reload the page.' }
|
||||
);
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FlashMessageColors } from './types';
|
||||
|
||||
export const FLASH_MESSAGE_TYPES = {
|
||||
success: { color: 'success' as FlashMessageColors, iconType: 'check' },
|
||||
info: { color: 'primary' as FlashMessageColors, iconType: 'iInCircle' },
|
||||
warning: { color: 'warning' as FlashMessageColors, iconType: 'alert' },
|
||||
error: { color: 'danger' as FlashMessageColors, iconType: 'alert' },
|
||||
};
|
||||
|
||||
// This is the default amount of time (5 seconds) a toast will last before disappearing
|
||||
// It can be overridden per-toast by passing the `toastLifetimeMs` property - @see types.ts
|
||||
export const DEFAULT_TOAST_TIMEOUT = 5000;
|
|
@ -5,62 +5,96 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { setMockValues } from '../../__mocks__/kea.mock';
|
||||
import { setMockValues, setMockActions } from '../../__mocks__/kea.mock';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
import { EuiCallOut, EuiGlobalToastList } from '@elastic/eui';
|
||||
|
||||
import { FlashMessages } from './flash_messages';
|
||||
import { FlashMessages, Callouts, Toasts } from './flash_messages';
|
||||
|
||||
describe('FlashMessages', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('does not render if no messages exist', () => {
|
||||
setMockValues({ messages: [] });
|
||||
|
||||
it('renders callout and toast flash messages', () => {
|
||||
const wrapper = shallow(<FlashMessages />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
expect(wrapper.find(Callouts)).toHaveLength(1);
|
||||
expect(wrapper.find(Toasts)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('renders an array of flash messages & types', () => {
|
||||
const mockMessages = [
|
||||
{ type: 'success', message: 'Hello world!!' },
|
||||
{
|
||||
type: 'error',
|
||||
message: 'Whoa nelly!',
|
||||
description: <div data-test-subj="error">Something went wrong</div>,
|
||||
},
|
||||
{ type: 'info', message: 'Everything is fine, nothing is ruined' },
|
||||
{ type: 'warning', message: 'Uh oh' },
|
||||
{ type: 'info', message: 'Testing multiples of same type' },
|
||||
];
|
||||
setMockValues({ messages: mockMessages });
|
||||
describe('callouts', () => {
|
||||
it('renders an array of flash messages & types', () => {
|
||||
const mockMessages = [
|
||||
{ type: 'success', message: 'Hello world!!' },
|
||||
{
|
||||
type: 'error',
|
||||
message: 'Whoa nelly!',
|
||||
description: <div data-test-subj="error">Something went wrong</div>,
|
||||
},
|
||||
{ type: 'info', message: 'Everything is fine, nothing is ruined' },
|
||||
{ type: 'warning', message: 'Uh oh' },
|
||||
{ type: 'info', message: 'Testing multiples of same type' },
|
||||
];
|
||||
setMockValues({ messages: mockMessages });
|
||||
|
||||
const wrapper = shallow(<FlashMessages />);
|
||||
const wrapper = shallow(<Callouts />);
|
||||
|
||||
expect(wrapper.find(EuiCallOut)).toHaveLength(5);
|
||||
expect(wrapper.find(EuiCallOut).first().prop('color')).toEqual('success');
|
||||
expect(wrapper.find('[data-test-subj="error"]')).toHaveLength(1);
|
||||
expect(wrapper.find(EuiCallOut).last().prop('iconType')).toEqual('iInCircle');
|
||||
expect(wrapper.find(EuiCallOut)).toHaveLength(5);
|
||||
expect(wrapper.find(EuiCallOut).first().prop('color')).toEqual('success');
|
||||
expect(wrapper.find('[data-test-subj="error"]')).toHaveLength(1);
|
||||
expect(wrapper.find(EuiCallOut).last().prop('iconType')).toEqual('iInCircle');
|
||||
});
|
||||
|
||||
it('renders any children', () => {
|
||||
setMockValues({ messages: [{ type: 'success' }] });
|
||||
|
||||
const wrapper = shallow(
|
||||
<Callouts>
|
||||
<button data-test-subj="testing">
|
||||
Some action - you could even clear flash messages here
|
||||
</button>
|
||||
</Callouts>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="testing"]').text()).toContain('Some action');
|
||||
});
|
||||
});
|
||||
|
||||
it('renders any children', () => {
|
||||
setMockValues({ messages: [{ type: 'success' }] });
|
||||
describe('toasts', () => {
|
||||
const actions = { dismissToastMessage: jest.fn() };
|
||||
beforeAll(() => setMockActions(actions));
|
||||
|
||||
const wrapper = shallow(
|
||||
<FlashMessages>
|
||||
<button data-test-subj="testing">
|
||||
Some action - you could even clear flash messages here
|
||||
</button>
|
||||
</FlashMessages>
|
||||
);
|
||||
it('renders an EUI toast list', () => {
|
||||
const mockToasts = [
|
||||
{ id: 'test', title: 'Hello world!!' },
|
||||
{
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
title: 'Success!',
|
||||
toastLifeTimeMs: 500,
|
||||
id: 'successToastId',
|
||||
},
|
||||
{
|
||||
color: 'danger',
|
||||
iconType: 'alert',
|
||||
title: 'Oh no!',
|
||||
text: <div data-test-subj="error">Something went wrong</div>,
|
||||
id: 'errorToastId',
|
||||
},
|
||||
];
|
||||
setMockValues({ toastMessages: mockToasts });
|
||||
|
||||
expect(wrapper.find('[data-test-subj="testing"]').text()).toContain('Some action');
|
||||
const wrapper = shallow(<Toasts />);
|
||||
const euiToastList = wrapper.find(EuiGlobalToastList);
|
||||
|
||||
expect(euiToastList).toHaveLength(1);
|
||||
expect(euiToastList.prop('toasts')).toEqual(mockToasts);
|
||||
expect(euiToastList.prop('dismissToast')).toEqual(actions.dismissToastMessage);
|
||||
expect(euiToastList.prop('toastLifeTimeMs')).toEqual(5000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,32 +7,30 @@
|
|||
|
||||
import React, { Fragment } from 'react';
|
||||
|
||||
import { useValues } from 'kea';
|
||||
import { useValues, useActions } from 'kea';
|
||||
|
||||
import { EuiCallOut, EuiCallOutProps, EuiSpacer } from '@elastic/eui';
|
||||
import { EuiCallOut, EuiSpacer, EuiGlobalToastList } from '@elastic/eui';
|
||||
|
||||
import { FLASH_MESSAGE_TYPES, DEFAULT_TOAST_TIMEOUT } from './constants';
|
||||
import { FlashMessagesLogic } from './flash_messages_logic';
|
||||
|
||||
const FLASH_MESSAGE_TYPES = {
|
||||
success: { color: 'success' as EuiCallOutProps['color'], icon: 'check' },
|
||||
info: { color: 'primary' as EuiCallOutProps['color'], icon: 'iInCircle' },
|
||||
warning: { color: 'warning' as EuiCallOutProps['color'], icon: 'alert' },
|
||||
error: { color: 'danger' as EuiCallOutProps['color'], icon: 'alert' },
|
||||
};
|
||||
export const FlashMessages: React.FC = ({ children }) => (
|
||||
<>
|
||||
<Callouts>{children}</Callouts>
|
||||
<Toasts />
|
||||
</>
|
||||
);
|
||||
|
||||
export const FlashMessages: React.FC = ({ children }) => {
|
||||
export const Callouts: React.FC = ({ children }) => {
|
||||
const { messages } = useValues(FlashMessagesLogic);
|
||||
|
||||
// If we have no messages to display, do not render the element at all
|
||||
if (!messages.length) return null;
|
||||
|
||||
return (
|
||||
<div data-test-subj="FlashMessages">
|
||||
<div aria-live="polite" role="region" data-test-subj="FlashMessages">
|
||||
{messages.map(({ type, message, description }, index) => (
|
||||
<Fragment key={index}>
|
||||
<EuiCallOut
|
||||
color={FLASH_MESSAGE_TYPES[type].color}
|
||||
iconType={FLASH_MESSAGE_TYPES[type].icon}
|
||||
iconType={FLASH_MESSAGE_TYPES[type].iconType}
|
||||
title={message}
|
||||
>
|
||||
{description}
|
||||
|
@ -44,3 +42,16 @@ export const FlashMessages: React.FC = ({ children }) => {
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Toasts: React.FC = () => {
|
||||
const { toastMessages } = useValues(FlashMessagesLogic);
|
||||
const { dismissToastMessage } = useActions(FlashMessagesLogic);
|
||||
|
||||
return (
|
||||
<EuiGlobalToastList
|
||||
toasts={toastMessages}
|
||||
dismissToast={dismissToastMessage}
|
||||
toastLifeTimeMs={DEFAULT_TOAST_TIMEOUT}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,11 +15,13 @@ import { FlashMessagesLogic, mountFlashMessagesLogic } from './flash_messages_lo
|
|||
import { IFlashMessage } from './types';
|
||||
|
||||
describe('FlashMessagesLogic', () => {
|
||||
const mount = () => mountFlashMessagesLogic();
|
||||
const mount = () => {
|
||||
resetContext({});
|
||||
return mountFlashMessagesLogic();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
resetContext({});
|
||||
});
|
||||
|
||||
it('has default values', () => {
|
||||
|
@ -27,67 +29,112 @@ describe('FlashMessagesLogic', () => {
|
|||
expect(FlashMessagesLogic.values).toEqual({
|
||||
messages: [],
|
||||
queuedMessages: [],
|
||||
toastMessages: [],
|
||||
historyListener: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
describe('setFlashMessages()', () => {
|
||||
it('sets an array of messages', () => {
|
||||
const messages: IFlashMessage[] = [
|
||||
{ type: 'success', message: 'Hello world!!' },
|
||||
{ type: 'error', message: 'Whoa nelly!', description: 'Uh oh' },
|
||||
{ type: 'info', message: 'Everything is fine, nothing is ruined' },
|
||||
];
|
||||
|
||||
describe('messages', () => {
|
||||
beforeAll(() => {
|
||||
mount();
|
||||
FlashMessagesLogic.actions.setFlashMessages(messages);
|
||||
|
||||
expect(FlashMessagesLogic.values.messages).toEqual(messages);
|
||||
});
|
||||
|
||||
it('automatically converts to an array if a single message obj is passed in', () => {
|
||||
const message = { type: 'success', message: 'I turn into an array!' } as IFlashMessage;
|
||||
describe('setFlashMessages', () => {
|
||||
it('sets an array of messages', () => {
|
||||
const messages: IFlashMessage[] = [
|
||||
{ type: 'success', message: 'Hello world!!' },
|
||||
{ type: 'error', message: 'Whoa nelly!', description: 'Uh oh' },
|
||||
{ type: 'info', message: 'Everything is fine, nothing is ruined' },
|
||||
];
|
||||
|
||||
mount();
|
||||
FlashMessagesLogic.actions.setFlashMessages(message);
|
||||
FlashMessagesLogic.actions.setFlashMessages(messages);
|
||||
|
||||
expect(FlashMessagesLogic.values.messages).toEqual([message]);
|
||||
expect(FlashMessagesLogic.values.messages).toEqual(messages);
|
||||
});
|
||||
|
||||
it('automatically converts to an array if a single message obj is passed in', () => {
|
||||
const message = { type: 'success', message: 'I turn into an array!' } as IFlashMessage;
|
||||
|
||||
FlashMessagesLogic.actions.setFlashMessages(message);
|
||||
|
||||
expect(FlashMessagesLogic.values.messages).toEqual([message]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearFlashMessages', () => {
|
||||
it('resets messages back to an empty array', () => {
|
||||
FlashMessagesLogic.actions.clearFlashMessages();
|
||||
|
||||
expect(FlashMessagesLogic.values.messages).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearFlashMessages()', () => {
|
||||
it('sets messages back to an empty array', () => {
|
||||
describe('queuedMessages', () => {
|
||||
beforeAll(() => {
|
||||
mount();
|
||||
FlashMessagesLogic.actions.setFlashMessages('test' as any);
|
||||
FlashMessagesLogic.actions.clearFlashMessages();
|
||||
});
|
||||
|
||||
expect(FlashMessagesLogic.values.messages).toEqual([]);
|
||||
describe('setQueuedMessages', () => {
|
||||
it('sets an array of messages', () => {
|
||||
const queuedMessage: IFlashMessage = { type: 'error', message: 'You deleted a thing' };
|
||||
|
||||
FlashMessagesLogic.actions.setQueuedMessages(queuedMessage);
|
||||
|
||||
expect(FlashMessagesLogic.values.queuedMessages).toEqual([queuedMessage]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearQueuedMessages', () => {
|
||||
it('resets queued messages back to an empty array', () => {
|
||||
FlashMessagesLogic.actions.clearQueuedMessages();
|
||||
|
||||
expect(FlashMessagesLogic.values.queuedMessages).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setQueuedMessages()', () => {
|
||||
it('sets an array of messages', () => {
|
||||
const queuedMessage: IFlashMessage = { type: 'error', message: 'You deleted a thing' };
|
||||
|
||||
describe('toastMessages', () => {
|
||||
beforeAll(() => {
|
||||
mount();
|
||||
FlashMessagesLogic.actions.setQueuedMessages(queuedMessage);
|
||||
|
||||
expect(FlashMessagesLogic.values.queuedMessages).toEqual([queuedMessage]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearQueuedMessages()', () => {
|
||||
it('sets queued messages back to an empty array', () => {
|
||||
mount();
|
||||
FlashMessagesLogic.actions.setQueuedMessages('test' as any);
|
||||
FlashMessagesLogic.actions.clearQueuedMessages();
|
||||
describe('addToastMessage', () => {
|
||||
it('appends a toast message to the current toasts array', () => {
|
||||
FlashMessagesLogic.actions.addToastMessage({ id: 'hello' });
|
||||
FlashMessagesLogic.actions.addToastMessage({ id: 'world' });
|
||||
FlashMessagesLogic.actions.addToastMessage({ id: 'lorem ipsum' });
|
||||
|
||||
expect(FlashMessagesLogic.values.queuedMessages).toEqual([]);
|
||||
expect(FlashMessagesLogic.values.toastMessages).toEqual([
|
||||
{ id: 'hello' },
|
||||
{ id: 'world' },
|
||||
{ id: 'lorem ipsum' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dismissToastMessage', () => {
|
||||
it('removes a specific toast ID from the current toasts array', () => {
|
||||
FlashMessagesLogic.actions.dismissToastMessage({ id: 'world' });
|
||||
|
||||
expect(FlashMessagesLogic.values.toastMessages).toEqual([
|
||||
{ id: 'hello' },
|
||||
{ id: 'lorem ipsum' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearToastMessages', () => {
|
||||
it('resets toast messages back to an empty array', () => {
|
||||
FlashMessagesLogic.actions.clearToastMessages();
|
||||
|
||||
expect(FlashMessagesLogic.values.toastMessages).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('history listener logic', () => {
|
||||
describe('setHistoryListener()', () => {
|
||||
describe('setHistoryListener', () => {
|
||||
it('sets the historyListener value', () => {
|
||||
mount();
|
||||
FlashMessagesLogic.actions.setHistoryListener('test' as any);
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
import { kea, MakeLogicType } from 'kea';
|
||||
|
||||
import { EuiGlobalToastListToast as IToast } from '@elastic/eui';
|
||||
|
||||
import { KibanaLogic } from '../kibana';
|
||||
|
||||
import { IFlashMessage } from './types';
|
||||
|
@ -14,6 +16,7 @@ import { IFlashMessage } from './types';
|
|||
interface FlashMessagesValues {
|
||||
messages: IFlashMessage[];
|
||||
queuedMessages: IFlashMessage[];
|
||||
toastMessages: IToast[];
|
||||
historyListener: Function | null;
|
||||
}
|
||||
interface FlashMessagesActions {
|
||||
|
@ -21,6 +24,9 @@ interface FlashMessagesActions {
|
|||
clearFlashMessages(): void;
|
||||
setQueuedMessages(messages: IFlashMessage | IFlashMessage[]): { messages: IFlashMessage[] };
|
||||
clearQueuedMessages(): void;
|
||||
addToastMessage(newToast: IToast): { newToast: IToast };
|
||||
dismissToastMessage(removedToast: IToast): { removedToast: IToast };
|
||||
clearToastMessages(): void;
|
||||
setHistoryListener(historyListener: Function): { historyListener: Function };
|
||||
}
|
||||
|
||||
|
@ -34,6 +40,9 @@ export const FlashMessagesLogic = kea<MakeLogicType<FlashMessagesValues, FlashMe
|
|||
clearFlashMessages: () => null,
|
||||
setQueuedMessages: (messages) => ({ messages: convertToArray(messages) }),
|
||||
clearQueuedMessages: () => null,
|
||||
addToastMessage: (newToast) => ({ newToast }),
|
||||
dismissToastMessage: (removedToast) => ({ removedToast }),
|
||||
clearToastMessages: () => null,
|
||||
setHistoryListener: (historyListener) => ({ historyListener }),
|
||||
},
|
||||
reducers: {
|
||||
|
@ -51,6 +60,15 @@ export const FlashMessagesLogic = kea<MakeLogicType<FlashMessagesValues, FlashMe
|
|||
clearQueuedMessages: () => [],
|
||||
},
|
||||
],
|
||||
toastMessages: [
|
||||
[],
|
||||
{
|
||||
addToastMessage: (toasts, { newToast }) => [...toasts, newToast],
|
||||
dismissToastMessage: (toasts, { removedToast }) =>
|
||||
toasts.filter(({ id }) => id !== removedToast.id),
|
||||
clearToastMessages: () => [],
|
||||
},
|
||||
],
|
||||
historyListener: [
|
||||
null,
|
||||
{
|
||||
|
|
|
@ -14,5 +14,7 @@ export {
|
|||
setErrorMessage,
|
||||
setQueuedSuccessMessage,
|
||||
setQueuedErrorMessage,
|
||||
flashSuccessToast,
|
||||
flashErrorToast,
|
||||
clearFlashMessages,
|
||||
} from './set_message_helpers';
|
||||
|
|
|
@ -14,6 +14,8 @@ import {
|
|||
setQueuedSuccessMessage,
|
||||
setQueuedErrorMessage,
|
||||
clearFlashMessages,
|
||||
flashSuccessToast,
|
||||
flashErrorToast,
|
||||
} from './set_message_helpers';
|
||||
|
||||
describe('Flash Message Helpers', () => {
|
||||
|
@ -72,4 +74,82 @@ describe('Flash Message Helpers', () => {
|
|||
|
||||
expect(FlashMessagesLogic.values.messages).toEqual([]);
|
||||
});
|
||||
|
||||
describe('toast helpers', () => {
|
||||
afterEach(() => {
|
||||
FlashMessagesLogic.actions.clearToastMessages();
|
||||
});
|
||||
|
||||
describe('without optional args', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(global.Date, 'now').mockReturnValueOnce(1234567890);
|
||||
});
|
||||
|
||||
it('flashSuccessToast', () => {
|
||||
flashSuccessToast('You did a thing!');
|
||||
|
||||
expect(FlashMessagesLogic.values.toastMessages).toEqual([
|
||||
{
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
title: 'You did a thing!',
|
||||
id: 'successToast-1234567890',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('flashErrorToast', () => {
|
||||
flashErrorToast('Something went wrong');
|
||||
|
||||
expect(FlashMessagesLogic.values.toastMessages).toEqual([
|
||||
{
|
||||
color: 'danger',
|
||||
iconType: 'alert',
|
||||
title: 'Something went wrong',
|
||||
id: 'errorToast-1234567890',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with optional args', () => {
|
||||
it('flashSuccessToast', () => {
|
||||
flashSuccessToast('You did a thing!', {
|
||||
text: '<button>View new thing</button>',
|
||||
toastLifeTimeMs: 50,
|
||||
id: 'customId',
|
||||
});
|
||||
|
||||
expect(FlashMessagesLogic.values.toastMessages).toEqual([
|
||||
{
|
||||
color: 'success',
|
||||
iconType: 'check',
|
||||
title: 'You did a thing!',
|
||||
text: '<button>View new thing</button>',
|
||||
toastLifeTimeMs: 50,
|
||||
id: 'customId',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('flashErrorToast', () => {
|
||||
flashErrorToast('Something went wrong', {
|
||||
text: "Here's some helpful advice on what to do",
|
||||
toastLifeTimeMs: 50000,
|
||||
id: 'specificErrorId',
|
||||
});
|
||||
|
||||
expect(FlashMessagesLogic.values.toastMessages).toEqual([
|
||||
{
|
||||
color: 'danger',
|
||||
iconType: 'alert',
|
||||
title: 'Something went wrong',
|
||||
text: "Here's some helpful advice on what to do",
|
||||
toastLifeTimeMs: 50000,
|
||||
id: 'specificErrorId',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FLASH_MESSAGE_TYPES } from './constants';
|
||||
import { FlashMessagesLogic } from './flash_messages_logic';
|
||||
import { ToastOptions } from './types';
|
||||
|
||||
export const setSuccessMessage = (message: string) => {
|
||||
FlashMessagesLogic.actions.setFlashMessages({
|
||||
|
@ -38,3 +40,21 @@ export const setQueuedErrorMessage = (message: string) => {
|
|||
export const clearFlashMessages = () => {
|
||||
FlashMessagesLogic.actions.clearFlashMessages();
|
||||
};
|
||||
|
||||
export const flashSuccessToast = (message: string, toastOptions: ToastOptions = {}) => {
|
||||
FlashMessagesLogic.actions.addToastMessage({
|
||||
...FLASH_MESSAGE_TYPES.success,
|
||||
...toastOptions,
|
||||
title: message,
|
||||
id: toastOptions?.id || `successToast-${Date.now()}`,
|
||||
});
|
||||
};
|
||||
|
||||
export const flashErrorToast = (message: string, toastOptions: ToastOptions = {}) => {
|
||||
FlashMessagesLogic.actions.addToastMessage({
|
||||
...FLASH_MESSAGE_TYPES.error,
|
||||
...toastOptions,
|
||||
title: message,
|
||||
id: toastOptions?.id || `errorToast-${Date.now()}`,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -5,10 +5,20 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { ReactNode, ReactChild } from 'react';
|
||||
|
||||
export type FlashMessageTypes = 'success' | 'info' | 'warning' | 'error';
|
||||
export type FlashMessageColors = 'success' | 'primary' | 'warning' | 'danger';
|
||||
|
||||
export interface IFlashMessage {
|
||||
type: 'success' | 'info' | 'warning' | 'error';
|
||||
type: FlashMessageTypes;
|
||||
message: ReactNode;
|
||||
description?: ReactNode;
|
||||
}
|
||||
|
||||
// @see EuiGlobalToastListToast for more props
|
||||
export interface ToastOptions {
|
||||
text?: ReactChild; // Additional text below the message/title, same as IFlashMessage['description']
|
||||
toastLifeTimeMs?: number; // Allows customing per-toast timeout
|
||||
id?: string;
|
||||
}
|
||||
|
|
|
@ -98,7 +98,7 @@ describe('GroupOverview', () => {
|
|||
messages: [mockSuccessMessage],
|
||||
});
|
||||
const wrapper = shallow(<Groups />);
|
||||
const flashMessages = wrapper.find(FlashMessages).dive().shallow();
|
||||
const flashMessages = wrapper.find(FlashMessages).dive().childAt(0).dive();
|
||||
|
||||
expect(flashMessages.find('[data-test-subj="NewGroupManageButton"]')).toHaveLength(1);
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue