-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* Set up basic shared FlashMessages & FlashMessagesLogic * Add top-level FlashMessagesProvider and history listener - This ensures that: - Our FlashMessagesLogic is a global state that persists throughout the entire app and only unmounts when the app itself does (allowing for persistent messages if needed) - history.listen enables the same behavior as previously, where flash messages would be cleared between page views * Set up queued messages that appear on page nav/load * [AS] Add FlashMessages component to Engines Overview + add Kea/Redux context/state to mountWithContext (in order for tests to pass) * Fix missing type exports, replace previous IFlashMessagesProps * [WS] Remove flashMessages state in OverviewLogic - in favor of either connecting it or using FlashMessagesLogic directly in the future * PR feedback: DRY out EUI callout color type def * PR Feedback: make flashMessages method names more explicit * PR Feedback: Shorter FlashMessagesLogic type names * PR feedback: Typing Co-authored-by: Byron Hulcher <byronhulcher@gmail.com> Co-authored-by: Byron Hulcher <byronhulcher@gmail.com> Co-authored-by: Byron Hulcher <byronhulcher@gmail.com>
- Loading branch information
1 parent
d2fd65c
commit 12579fb
Showing
14 changed files
with
434 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
64 changes: 64 additions & 0 deletions
64
...ugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import '../../__mocks__/kea.mock'; | ||
|
||
import { useValues } from 'kea'; | ||
import React from 'react'; | ||
import { shallow } from 'enzyme'; | ||
import { EuiCallOut } from '@elastic/eui'; | ||
|
||
import { FlashMessages } from './'; | ||
|
||
describe('FlashMessages', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('does not render if no messages exist', () => { | ||
(useValues as jest.Mock).mockImplementationOnce(() => ({ messages: [] })); | ||
|
||
const wrapper = shallow(<FlashMessages />); | ||
|
||
expect(wrapper.isEmptyRender()).toBe(true); | ||
}); | ||
|
||
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' }, | ||
]; | ||
(useValues as jest.Mock).mockImplementationOnce(() => ({ messages: mockMessages })); | ||
|
||
const wrapper = shallow(<FlashMessages />); | ||
|
||
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', () => { | ||
(useValues as jest.Mock).mockImplementationOnce(() => ({ messages: [{ type: 'success' }] })); | ||
|
||
const wrapper = shallow( | ||
<FlashMessages> | ||
<button data-test-subj="testing"> | ||
Some action - you could even clear flash messages here | ||
</button> | ||
</FlashMessages> | ||
); | ||
|
||
expect(wrapper.find('[data-test-subj="testing"]').text()).toContain('Some action'); | ||
}); | ||
}); |
43 changes: 43 additions & 0 deletions
43
...ck/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import React, { Fragment } from 'react'; | ||
import { useValues } from 'kea'; | ||
import { EuiCallOut, EuiCallOutProps, EuiSpacer } from '@elastic/eui'; | ||
|
||
import { FlashMessagesLogic, IFlashMessagesValues } 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: 'cross' }, | ||
}; | ||
|
||
export const FlashMessages: React.FC = ({ children }) => { | ||
const { messages } = useValues(FlashMessagesLogic) as IFlashMessagesValues; | ||
|
||
// 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"> | ||
{messages.map(({ type, message, description }, index) => ( | ||
<Fragment key={index}> | ||
<EuiCallOut | ||
color={FLASH_MESSAGE_TYPES[type].color} | ||
iconType={FLASH_MESSAGE_TYPES[type].icon} | ||
title={message} | ||
> | ||
{description} | ||
</EuiCallOut> | ||
<EuiSpacer /> | ||
</Fragment> | ||
))} | ||
{children} | ||
</div> | ||
); | ||
}; |
136 changes: 136 additions & 0 deletions
136
.../enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import { resetContext } from 'kea'; | ||
|
||
import { FlashMessagesLogic, IFlashMessage } from './flash_messages_logic'; | ||
|
||
describe('FlashMessagesLogic', () => { | ||
const DEFAULT_VALUES = { | ||
messages: [], | ||
queuedMessages: [], | ||
historyListener: null, | ||
}; | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
resetContext({}); | ||
}); | ||
|
||
it('has expected default values', () => { | ||
FlashMessagesLogic.mount(); | ||
expect(FlashMessagesLogic.values).toEqual(DEFAULT_VALUES); | ||
}); | ||
|
||
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' }, | ||
]; | ||
|
||
FlashMessagesLogic.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; | ||
|
||
FlashMessagesLogic.mount(); | ||
FlashMessagesLogic.actions.setFlashMessages(message); | ||
|
||
expect(FlashMessagesLogic.values.messages).toEqual([message]); | ||
}); | ||
}); | ||
|
||
describe('clearFlashMessages()', () => { | ||
it('sets messages back to an empty array', () => { | ||
FlashMessagesLogic.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.mount(); | ||
FlashMessagesLogic.actions.setQueuedMessages(queuedMessage); | ||
|
||
expect(FlashMessagesLogic.values.queuedMessages).toEqual([queuedMessage]); | ||
}); | ||
}); | ||
|
||
describe('clearQueuedMessages()', () => { | ||
it('sets queued messages back to an empty array', () => { | ||
FlashMessagesLogic.mount(); | ||
FlashMessagesLogic.actions.setQueuedMessages('test' as any); | ||
FlashMessagesLogic.actions.clearQueuedMessages(); | ||
|
||
expect(FlashMessagesLogic.values.queuedMessages).toEqual([]); | ||
}); | ||
}); | ||
|
||
describe('history listener logic', () => { | ||
describe('setHistoryListener()', () => { | ||
it('sets the historyListener value', () => { | ||
FlashMessagesLogic.mount(); | ||
FlashMessagesLogic.actions.setHistoryListener('test' as any); | ||
|
||
expect(FlashMessagesLogic.values.historyListener).toEqual('test'); | ||
}); | ||
}); | ||
|
||
describe('listenToHistory()', () => { | ||
it('listens for history changes and clears messages on change', () => { | ||
FlashMessagesLogic.mount(); | ||
FlashMessagesLogic.actions.setQueuedMessages(['queuedMessages'] as any); | ||
jest.spyOn(FlashMessagesLogic.actions, 'clearFlashMessages'); | ||
jest.spyOn(FlashMessagesLogic.actions, 'setFlashMessages'); | ||
jest.spyOn(FlashMessagesLogic.actions, 'clearQueuedMessages'); | ||
jest.spyOn(FlashMessagesLogic.actions, 'setHistoryListener'); | ||
|
||
const mockListener = jest.fn(() => jest.fn()); | ||
const history = { listen: mockListener } as any; | ||
FlashMessagesLogic.actions.listenToHistory(history); | ||
|
||
expect(mockListener).toHaveBeenCalled(); | ||
expect(FlashMessagesLogic.actions.setHistoryListener).toHaveBeenCalled(); | ||
|
||
const mockHistoryChange = (mockListener.mock.calls[0] as any)[0]; | ||
mockHistoryChange(); | ||
expect(FlashMessagesLogic.actions.clearFlashMessages).toHaveBeenCalled(); | ||
expect(FlashMessagesLogic.actions.setFlashMessages).toHaveBeenCalledWith([ | ||
'queuedMessages', | ||
]); | ||
expect(FlashMessagesLogic.actions.clearQueuedMessages).toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
describe('beforeUnmount', () => { | ||
it('removes history listener on unmount', () => { | ||
const mockUnlistener = jest.fn(); | ||
const unmount = FlashMessagesLogic.mount(); | ||
|
||
FlashMessagesLogic.actions.setHistoryListener(mockUnlistener); | ||
unmount(); | ||
|
||
expect(mockUnlistener).toHaveBeenCalled(); | ||
}); | ||
|
||
it('does not crash if no listener exists', () => { | ||
const unmount = FlashMessagesLogic.mount(); | ||
unmount(); | ||
}); | ||
}); | ||
}); | ||
}); |
87 changes: 87 additions & 0 deletions
87
...ugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import { kea } from 'kea'; | ||
import { ReactNode } from 'react'; | ||
import { History } from 'history'; | ||
|
||
import { IKeaLogic, TKeaReducers, IKeaParams } from '../types'; | ||
|
||
export interface IFlashMessage { | ||
type: 'success' | 'info' | 'warning' | 'error'; | ||
message: ReactNode; | ||
description?: ReactNode; | ||
} | ||
|
||
export interface IFlashMessagesValues { | ||
messages: IFlashMessage[]; | ||
queuedMessages: IFlashMessage[]; | ||
historyListener: Function | null; | ||
} | ||
export interface IFlashMessagesActions { | ||
setFlashMessages(messages: IFlashMessage | IFlashMessage[]): void; | ||
clearFlashMessages(): void; | ||
setQueuedMessages(messages: IFlashMessage | IFlashMessage[]): void; | ||
clearQueuedMessages(): void; | ||
listenToHistory(history: History): void; | ||
setHistoryListener(historyListener: Function): void; | ||
} | ||
|
||
const convertToArray = (messages: IFlashMessage | IFlashMessage[]) => | ||
!Array.isArray(messages) ? [messages] : messages; | ||
|
||
export const FlashMessagesLogic = kea({ | ||
actions: (): IFlashMessagesActions => ({ | ||
setFlashMessages: (messages) => ({ messages: convertToArray(messages) }), | ||
clearFlashMessages: () => null, | ||
setQueuedMessages: (messages) => ({ messages: convertToArray(messages) }), | ||
clearQueuedMessages: () => null, | ||
listenToHistory: (history) => history, | ||
setHistoryListener: (historyListener) => ({ historyListener }), | ||
}), | ||
reducers: (): TKeaReducers<IFlashMessagesValues, IFlashMessagesActions> => ({ | ||
messages: [ | ||
[], | ||
{ | ||
setFlashMessages: (_, { messages }) => messages, | ||
clearFlashMessages: () => [], | ||
}, | ||
], | ||
queuedMessages: [ | ||
[], | ||
{ | ||
setQueuedMessages: (_, { messages }) => messages, | ||
clearQueuedMessages: () => [], | ||
}, | ||
], | ||
historyListener: [ | ||
null, | ||
{ | ||
setHistoryListener: (_, { historyListener }) => historyListener, | ||
}, | ||
], | ||
}), | ||
listeners: ({ values, actions }): Partial<IFlashMessagesActions> => ({ | ||
listenToHistory: (history) => { | ||
// On React Router navigation, clear previous flash messages and load any queued messages | ||
const unlisten = history.listen(() => { | ||
actions.clearFlashMessages(); | ||
actions.setFlashMessages(values.queuedMessages); | ||
actions.clearQueuedMessages(); | ||
}); | ||
actions.setHistoryListener(unlisten); | ||
}, | ||
}), | ||
events: ({ values }) => ({ | ||
beforeUnmount: () => { | ||
const { historyListener: removeHistoryListener } = values; | ||
if (removeHistoryListener) removeHistoryListener(); | ||
}, | ||
}), | ||
} as IKeaParams<IFlashMessagesValues, IFlashMessagesActions>) as IKeaLogic< | ||
IFlashMessagesValues, | ||
IFlashMessagesActions | ||
>; |
Oops, something went wrong.