diff --git a/x-pack/legacy/plugins/siem/public/apps/start_app.tsx b/x-pack/legacy/plugins/siem/public/apps/start_app.tsx index 6ea1fd5d57f9..424a4f19126b 100644 --- a/x-pack/legacy/plugins/siem/public/apps/start_app.tsx +++ b/x-pack/legacy/plugins/siem/public/apps/start_app.tsx @@ -17,11 +17,12 @@ import { BehaviorSubject } from 'rxjs'; import { pluck } from 'rxjs/operators'; import { I18nContext } from 'ui/i18n'; -import { ErrorToast } from '../components/error_toast'; +import { ErrorToastDispatcher } from '../components/error_toast_dispatcher'; import { KibanaConfigContext } from '../lib/adapters/framework/kibana_framework_adapter'; import { AppFrontendLibs } from '../lib/lib'; import { PageRouter } from '../routes'; import { createStore } from '../store/store'; +import { GlobalToaster, ManageGlobalToaster } from '../components/toasters'; import { MlCapabilitiesProvider } from '../components/ml/permissions/ml_capabilities_provider'; export const startApp = async (libs: AppFrontendLibs) => { @@ -34,23 +35,26 @@ export const startApp = async (libs: AppFrontendLibs) => { libs.framework.render( - - - ({ - eui: libs.framework.darkMode ? euiDarkVars : euiLightVars, - darkMode: libs.framework.darkMode, - })} - > - - - - - - - - - + + + + ({ + eui: libs.framework.darkMode ? euiDarkVars : euiLightVars, + darkMode: libs.framework.darkMode, + })} + > + + + + + + + + + + + ); diff --git a/x-pack/legacy/plugins/siem/public/components/error_toast/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/error_toast/__snapshots__/index.test.tsx.snap deleted file mode 100644 index cbe1706cf597..000000000000 --- a/x-pack/legacy/plugins/siem/public/components/error_toast/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Error Toast rendering it renders the default Authentication table 1`] = ` - -`; diff --git a/x-pack/legacy/plugins/siem/public/components/error_toast/index.tsx b/x-pack/legacy/plugins/siem/public/components/error_toast/index.tsx deleted file mode 100644 index 37815177dc82..000000000000 --- a/x-pack/legacy/plugins/siem/public/components/error_toast/index.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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 { EuiGlobalToastList, EuiGlobalToastListToast as Toast } from '@elastic/eui'; -import React from 'react'; -import { connect } from 'react-redux'; -import { pure } from 'recompose'; -import { ActionCreator } from 'typescript-fsa'; - -import { appModel, appSelectors, State } from '../../store'; -import { appActions } from '../../store/app'; - -interface OwnProps { - toastLifeTimeMs?: number; -} - -interface ReduxProps { - errors?: appModel.Error[]; -} - -interface DispatchProps { - addError?: ActionCreator<{ id: string; title: string; message: string }>; - removeError?: ActionCreator<{ id: string }>; -} - -type Props = OwnProps & ReduxProps & DispatchProps; - -const ErrorToastComponent = pure(({ toastLifeTimeMs = 10000, errors = [], removeError }) => - globalListFromToasts(errorsToToasts(errors), removeError!, toastLifeTimeMs) -); - -export const globalListFromToasts = ( - toasts: Toast[], - removeError: ActionCreator<{ id: string }>, - toastLifeTimeMs: number -) => - toasts.length !== 0 ? ( - removeError({ id })} - toastLifeTimeMs={toastLifeTimeMs} - /> - ) : null; - -export const errorsToToasts = (errors: appModel.Error[]): Toast[] => - errors.map(({ id, title, message }) => { - const toast: Toast = { - id, - title, - color: 'danger', - iconType: 'alert', - text:

{message}

, - }; - return toast; - }); - -const makeMapStateToProps = () => { - const getErrorSelector = appSelectors.errorsSelector(); - return (state: State) => getErrorSelector(state); -}; - -export const ErrorToast = connect( - makeMapStateToProps, - { - removeError: appActions.removeError, - } -)(ErrorToastComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000000..d90a337bbeed --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Error Toast Dispatcher rendering it renders 1`] = ` + +`; diff --git a/x-pack/legacy/plugins/siem/public/components/error_toast/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx similarity index 83% rename from x-pack/legacy/plugins/siem/public/components/error_toast/index.test.tsx rename to x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx index 52aad5a72011..6c4bc797d39f 100644 --- a/x-pack/legacy/plugins/siem/public/components/error_toast/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx @@ -12,10 +12,10 @@ import { Provider } from 'react-redux'; import { apolloClientObservable, mockGlobalState } from '../../mock'; import { createStore } from '../../store/store'; -import { ErrorToast } from '.'; +import { ErrorToastDispatcher } from '.'; import { State } from '../../store/reducer'; -describe('Error Toast', () => { +describe('Error Toast Dispatcher', () => { const state: State = mockGlobalState; let store = createStore(state, apolloClientObservable); @@ -24,10 +24,10 @@ describe('Error Toast', () => { }); describe('rendering', () => { - test('it renders the default Authentication table', () => { + test('it renders', () => { const wrapper = shallow( - + ); expect(toJson(wrapper)).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.tsx b/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.tsx new file mode 100644 index 000000000000..998b90d11a4d --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/error_toast_dispatcher/index.tsx @@ -0,0 +1,66 @@ +/* + * 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 { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { ActionCreator } from 'typescript-fsa'; + +import { appModel, appSelectors, State } from '../../store'; +import { appActions } from '../../store/app'; +import { useStateToaster } from '../toasters'; + +interface OwnProps { + toastLifeTimeMs?: number; +} + +interface ReduxProps { + errors?: appModel.Error[]; +} + +interface DispatchProps { + removeError: ActionCreator<{ id: string }>; +} + +type Props = OwnProps & ReduxProps & DispatchProps; + +const ErrorToastDispatcherComponent = ({ + toastLifeTimeMs = 5000, + errors = [], + removeError, +}: Props) => { + const [{ toasts }, dispatchToaster] = useStateToaster(); + useEffect(() => { + errors.forEach(({ id, title, message }) => { + if (!toasts.some(toast => toast.id === id)) { + dispatchToaster({ + type: 'addToaster', + toast: { + color: 'danger', + id, + iconType: 'alert', + title, + errors: message, + toastLifeTimeMs, + }, + }); + } + removeError({ id }); + }); + }); + return null; +}; + +const makeMapStateToProps = () => { + const getErrorSelector = appSelectors.errorsSelector(); + return (state: State) => getErrorSelector(state); +}; + +export const ErrorToastDispatcher = connect( + makeMapStateToProps, + { + removeError: appActions.removeError, + } +)(ErrorToastDispatcherComponent); diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/index.tsx index 92bf0f2aac50..36f4f4a3442c 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/auto_save_warning/index.tsx @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiGlobalToastList } from '@elastic/eui'; +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiGlobalToastListToast as Toast, +} from '@elastic/eui'; import { getOr } from 'lodash/fp'; import { pure } from 'recompose'; import * as React from 'react'; @@ -18,6 +23,7 @@ import { TimelineModel } from '../../../store/timeline/model'; import * as i18n from './translations'; import { timelineActions } from '../../../store/timeline'; import { AutoSavedWarningMsg } from '../../../store/timeline/types'; +import { useStateToaster } from '../../toasters'; interface ReduxProps { timelineId: string | null; @@ -48,49 +54,46 @@ const AutoSaveWarningMsgComponent = pure( timelineId, updateAutoSaveMsg, updateTimeline, - }) => ( - -

{i18n.DESCRIPTION}

- - - { - updateTimeline({ id: timelineId, timeline: newTimelineModel }); - updateAutoSaveMsg({ timelineId: null, newTimelineModel: null }); - setTimelineRangeDatePicker({ - from: getOr(0, 'dateRange.start', newTimelineModel), - to: getOr(0, 'dateRange.end', newTimelineModel), - }); - }} - > - {i18n.REFRESH_TIMELINE} - - - - - ), - }, - ] - : [] - } - dismissToast={() => { - updateAutoSaveMsg({ timelineId: null, newTimelineModel: null }); - }} - toastLifeTimeMs={6000} - /> - ) + }) => { + const dispatchToaster = useStateToaster()[1]; + if (timelineId != null && newTimelineModel != null) { + const toast: Toast = { + id: 'AutoSaveWarningMsg', + title: i18n.TITLE, + color: 'warning', + iconType: 'alert', + toastLifeTimeMs: 10000, + text: ( + <> +

{i18n.DESCRIPTION}

+ + + { + updateTimeline({ id: timelineId, timeline: newTimelineModel }); + updateAutoSaveMsg({ timelineId: null, newTimelineModel: null }); + setTimelineRangeDatePicker({ + from: getOr(0, 'dateRange.start', newTimelineModel), + to: getOr(0, 'dateRange.end', newTimelineModel), + }); + }} + > + {i18n.REFRESH_TIMELINE} + + + + + ), + }; + dispatchToaster({ + type: 'addToaster', + toast, + }); + } + + return null; + } ); const mapStateToProps = (state: State) => { diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap new file mode 100644 index 000000000000..7e4c94e9c0fe --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap @@ -0,0 +1,52 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Modal all errors rendering it renders the default all errors modal when isShowing is positive 1`] = ` + + + + + Your visualization has error(s) + + + + + + + + Error 1, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + + + + + + Close + + + + +`; diff --git a/x-pack/legacy/plugins/siem/public/components/toasters/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/toasters/index.test.tsx new file mode 100644 index 000000000000..ff7d7cbee646 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/components/toasters/index.test.tsx @@ -0,0 +1,301 @@ +/* + * 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 { cloneDeep, set } from 'lodash/fp'; +import { mount } from 'enzyme'; +import React, { useEffect } from 'react'; +import { wait } from 'react-testing-library'; + +import { AppToast, useStateToaster, ManageGlobalToaster, GlobalToaster } from '.'; + +const mockToast: AppToast = { + color: 'danger', + id: 'id-super-id', + iconType: 'alert', + title: 'Test & Test', + toastLifeTimeMs: 100, + text: + 'Error 1, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', +}; + +describe('Toaster', () => { + describe('Manage Global Toaster Reducer', () => { + test('we can add a toast in the reducer', () => { + const AddToaster = () => { + const [{ toasts }, dispatch] = useStateToaster(); + return ( + <> +