mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
## Summary Apply toast guidelines from EUI (https://elastic.github.io/eui/#/guidelines/toasts) to SIEM applications; ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. - [x] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) - ~~[ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ - ~~[ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios - ~~[ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers - ~~[ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ - ~~[ ] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~
This commit is contained in:
parent
1daf91b7e3
commit
96e713c15b
20 changed files with 821 additions and 203 deletions
|
@ -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(
|
||||
<EuiErrorBoundary>
|
||||
<I18nContext>
|
||||
<ReduxStoreProvider store={store}>
|
||||
<ApolloProvider client={libs.apolloClient}>
|
||||
<ThemeProvider
|
||||
theme={() => ({
|
||||
eui: libs.framework.darkMode ? euiDarkVars : euiLightVars,
|
||||
darkMode: libs.framework.darkMode,
|
||||
})}
|
||||
>
|
||||
<KibanaConfigContext.Provider value={libs.framework}>
|
||||
<MlCapabilitiesProvider>
|
||||
<PageRouter history={history} />
|
||||
</MlCapabilitiesProvider>
|
||||
</KibanaConfigContext.Provider>
|
||||
</ThemeProvider>
|
||||
<ErrorToast />
|
||||
</ApolloProvider>
|
||||
</ReduxStoreProvider>
|
||||
<ManageGlobalToaster>
|
||||
<ReduxStoreProvider store={store}>
|
||||
<ApolloProvider client={libs.apolloClient}>
|
||||
<ThemeProvider
|
||||
theme={() => ({
|
||||
eui: libs.framework.darkMode ? euiDarkVars : euiLightVars,
|
||||
darkMode: libs.framework.darkMode,
|
||||
})}
|
||||
>
|
||||
<KibanaConfigContext.Provider value={libs.framework}>
|
||||
<MlCapabilitiesProvider>
|
||||
<PageRouter history={history} />
|
||||
</MlCapabilitiesProvider>
|
||||
</KibanaConfigContext.Provider>
|
||||
</ThemeProvider>
|
||||
<ErrorToastDispatcher />
|
||||
<GlobalToaster />
|
||||
</ApolloProvider>
|
||||
</ReduxStoreProvider>
|
||||
</ManageGlobalToaster>
|
||||
</I18nContext>
|
||||
</EuiErrorBoundary>
|
||||
);
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Error Toast rendering it renders the default Authentication table 1`] = `
|
||||
<Connect(pure(Component))
|
||||
toastLifeTimeMs={9999999999}
|
||||
/>
|
||||
`;
|
|
@ -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<Props>(({ toastLifeTimeMs = 10000, errors = [], removeError }) =>
|
||||
globalListFromToasts(errorsToToasts(errors), removeError!, toastLifeTimeMs)
|
||||
);
|
||||
|
||||
export const globalListFromToasts = (
|
||||
toasts: Toast[],
|
||||
removeError: ActionCreator<{ id: string }>,
|
||||
toastLifeTimeMs: number
|
||||
) =>
|
||||
toasts.length !== 0 ? (
|
||||
<EuiGlobalToastList
|
||||
toasts={toasts}
|
||||
dismissToast={({ id }) => 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: <p>{message}</p>,
|
||||
};
|
||||
return toast;
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getErrorSelector = appSelectors.errorsSelector();
|
||||
return (state: State) => getErrorSelector(state);
|
||||
};
|
||||
|
||||
export const ErrorToast = connect(
|
||||
makeMapStateToProps,
|
||||
{
|
||||
removeError: appActions.removeError,
|
||||
}
|
||||
)(ErrorToastComponent);
|
|
@ -0,0 +1,7 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Error Toast Dispatcher rendering it renders 1`] = `
|
||||
<Connect(ErrorToastDispatcherComponent)
|
||||
toastLifeTimeMs={9999999999}
|
||||
/>
|
||||
`;
|
|
@ -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(
|
||||
<Provider store={store}>
|
||||
<ErrorToast toastLifeTimeMs={9999999999} />
|
||||
<ErrorToastDispatcher toastLifeTimeMs={9999999999} />
|
||||
</Provider>
|
||||
);
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
|
@ -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);
|
|
@ -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<OwnProps>(
|
|||
timelineId,
|
||||
updateAutoSaveMsg,
|
||||
updateTimeline,
|
||||
}) => (
|
||||
<EuiGlobalToastList
|
||||
toasts={
|
||||
timelineId != null && newTimelineModel != null
|
||||
? [
|
||||
{
|
||||
id: 'AutoSaveWarningMsg',
|
||||
title: i18n.TITLE,
|
||||
color: 'warning',
|
||||
iconType: 'alert',
|
||||
toastLifeTimeMs: 15000,
|
||||
text: (
|
||||
<>
|
||||
<p>{i18n.DESCRIPTION}</p>
|
||||
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
size="s"
|
||||
onClick={() => {
|
||||
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}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
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: (
|
||||
<>
|
||||
<p>{i18n.DESCRIPTION}</p>
|
||||
<EuiFlexGroup justifyContent="flexEnd" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
size="s"
|
||||
onClick={() => {
|
||||
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}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
),
|
||||
};
|
||||
dispatchToaster({
|
||||
type: 'addToaster',
|
||||
toast,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
const mapStateToProps = (state: State) => {
|
||||
|
|
52
x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap
generated
Normal file
52
x-pack/legacy/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap
generated
Normal file
|
@ -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`] = `
|
||||
<EuiOverlayMask>
|
||||
<EuiModal
|
||||
maxWidth={true}
|
||||
onClose={[Function]}
|
||||
>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
Your visualization has error(s)
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
<EuiModalBody>
|
||||
<EuiCallOut
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
size="s"
|
||||
title="Test & Test"
|
||||
/>
|
||||
<EuiSpacer
|
||||
size="s"
|
||||
/>
|
||||
<EuiAccordion
|
||||
buttonContent="Error 1, Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt u ..."
|
||||
data-test-subj="modal-all-errors-accordion"
|
||||
id="accordion1"
|
||||
initialIsOpen={true}
|
||||
key="id-super-id-0"
|
||||
paddingSize="none"
|
||||
>
|
||||
<Styled(EuiCodeBlock)>
|
||||
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.
|
||||
</Styled(EuiCodeBlock)>
|
||||
</EuiAccordion>
|
||||
</EuiModalBody>
|
||||
<EuiModalFooter>
|
||||
<EuiButton
|
||||
color="primary"
|
||||
data-test-subj="modal-all-errors-close"
|
||||
fill={true}
|
||||
iconSide="left"
|
||||
onClick={[Function]}
|
||||
size="m"
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
`;
|
|
@ -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 (
|
||||
<>
|
||||
<button
|
||||
data-test-subj="add-toast"
|
||||
type="button"
|
||||
onClick={() => dispatch({ type: 'addToaster', toast: mockToast })}
|
||||
/>
|
||||
{toasts.map(toast => (
|
||||
<span
|
||||
data-test-subj={`add-toaster-${toast.id}`}
|
||||
key={`add-toaster-${toast.id}`}
|
||||
>{`${toast.title} ${toast.text}`}</span>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const wrapper = mount(
|
||||
<ManageGlobalToaster>
|
||||
<AddToaster />
|
||||
</ManageGlobalToaster>
|
||||
);
|
||||
wrapper.find('[data-test-subj="add-toast"]').simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('[data-test-subj="add-toaster-id-super-id"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('we can delete a toast in the reducer', () => {
|
||||
const DeleteToaster = () => {
|
||||
const [{ toasts }, dispatch] = useStateToaster();
|
||||
useEffect(() => {
|
||||
if (toasts.length === 0) {
|
||||
dispatch({ type: 'addToaster', toast: mockToast });
|
||||
}
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
data-test-subj="add-toast"
|
||||
type="button"
|
||||
onClick={() => dispatch({ type: 'deleteToaster', id: mockToast.id })}
|
||||
/>
|
||||
{toasts.map(toast => (
|
||||
<span
|
||||
data-test-subj={`delete-toaster-${toast.id}`}
|
||||
key={`delete-toaster-${toast.id}`}
|
||||
>{`${toast.title} ${toast.text}`}</span>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const wrapper = mount(
|
||||
<ManageGlobalToaster>
|
||||
<DeleteToaster />
|
||||
</ManageGlobalToaster>
|
||||
);
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('[data-test-subj="delete-toaster-id-super-id"]').exists()).toBe(true);
|
||||
|
||||
wrapper.find('[data-test-subj="add-toast"]').simulate('click');
|
||||
wrapper.update();
|
||||
wait(() => {
|
||||
expect(wrapper.find('[data-test-subj="delete-toaster-id-super-id"]').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Global Toaster', () => {
|
||||
test('Render a basic toaster', () => {
|
||||
const AddToaster = () => {
|
||||
const [{ toasts }, dispatch] = useStateToaster();
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
data-test-subj="add-toast"
|
||||
type="button"
|
||||
onClick={() => dispatch({ type: 'addToaster', toast: mockToast })}
|
||||
/>
|
||||
{toasts.map(toast => (
|
||||
<span key={`add-toaster-${toast.id}`}>{`${toast.title} ${toast.text}`}</span>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const wrapper = mount(
|
||||
<ManageGlobalToaster>
|
||||
<AddToaster />
|
||||
<GlobalToaster />
|
||||
</ManageGlobalToaster>
|
||||
);
|
||||
wrapper.find('[data-test-subj="add-toast"]').simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('.euiGlobalToastList').exists()).toBe(true);
|
||||
expect(wrapper.find('.euiToastHeader__title').text()).toBe('Test & Test');
|
||||
});
|
||||
|
||||
test('Render an error toaster', () => {
|
||||
let mockErrorToast: AppToast = cloneDeep(mockToast);
|
||||
mockErrorToast.title = 'Test & Test ERROR';
|
||||
mockErrorToast = set('errors', [mockErrorToast.text], mockErrorToast);
|
||||
|
||||
const AddToaster = () => {
|
||||
const [{ toasts }, dispatch] = useStateToaster();
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
data-test-subj="add-toast"
|
||||
type="button"
|
||||
onClick={() => dispatch({ type: 'addToaster', toast: mockErrorToast })}
|
||||
/>
|
||||
{toasts.map(toast => (
|
||||
<span key={`add-toaster-${toast.id}`}>{`${toast.title} ${toast.text}`}</span>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const wrapper = mount(
|
||||
<ManageGlobalToaster>
|
||||
<AddToaster />
|
||||
<GlobalToaster />
|
||||
</ManageGlobalToaster>
|
||||
);
|
||||
wrapper.find('[data-test-subj="add-toast"]').simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('.euiGlobalToastList').exists()).toBe(true);
|
||||
expect(wrapper.find('.euiToastHeader__title').text()).toBe('Test & Test ERROR');
|
||||
expect(wrapper.find('button[data-test-subj="toaster-show-all-error-modal"]').exists()).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('Only show one toast at the time', () => {
|
||||
const mockOneMoreToast: AppToast = cloneDeep(mockToast);
|
||||
mockOneMoreToast.id = 'id-super-id-II';
|
||||
mockOneMoreToast.title = 'Test & Test II';
|
||||
|
||||
const AddToaster = () => {
|
||||
const [{ toasts }, dispatch] = useStateToaster();
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
data-test-subj="add-toast"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
dispatch({ type: 'addToaster', toast: mockToast });
|
||||
dispatch({ type: 'addToaster', toast: mockOneMoreToast });
|
||||
}}
|
||||
/>
|
||||
{toasts.map(toast => (
|
||||
<span key={`add-toaster-${toast.id}`}>{`${toast.title} ${toast.text}`}</span>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const wrapper = mount(
|
||||
<ManageGlobalToaster>
|
||||
<AddToaster />
|
||||
<GlobalToaster />
|
||||
</ManageGlobalToaster>
|
||||
);
|
||||
wrapper.find('[data-test-subj="add-toast"]').simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('.euiToast').length).toBe(1);
|
||||
expect(wrapper.find('.euiToastHeader__title').text()).toBe('Test & Test');
|
||||
wait(
|
||||
() => {
|
||||
expect(wrapper.find('.euiToast').length).toBe(1);
|
||||
expect(wrapper.find('.euiToastHeader__title').text()).toBe('Test & Test II');
|
||||
},
|
||||
{ timeout: 110 }
|
||||
);
|
||||
});
|
||||
|
||||
test('Do not show anymore toaster when modal error is open', () => {
|
||||
let mockErrorToast: AppToast = cloneDeep(mockToast);
|
||||
mockErrorToast.id = 'id-super-id-error';
|
||||
mockErrorToast = set('errors', [mockErrorToast.text], mockErrorToast);
|
||||
|
||||
const AddToaster = () => {
|
||||
const [{ toasts }, dispatch] = useStateToaster();
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
data-test-subj="add-toast"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
dispatch({ type: 'addToaster', toast: mockErrorToast });
|
||||
dispatch({ type: 'addToaster', toast: mockToast });
|
||||
}}
|
||||
/>
|
||||
{toasts.map(toast => (
|
||||
<span key={`add-toaster-${toast.id}`}>{`${toast.title} ${toast.text}`}</span>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const wrapper = mount(
|
||||
<ManageGlobalToaster>
|
||||
<AddToaster />
|
||||
<GlobalToaster />
|
||||
</ManageGlobalToaster>
|
||||
);
|
||||
wrapper.find('[data-test-subj="add-toast"]').simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
wrapper.find('button[data-test-subj="toaster-show-all-error-modal"]').simulate('click');
|
||||
|
||||
wrapper.update();
|
||||
|
||||
wait(
|
||||
() => {
|
||||
expect(wrapper.find('.euiToast').length).toBe(0);
|
||||
},
|
||||
{ timeout: 110 }
|
||||
);
|
||||
});
|
||||
|
||||
test('Show new toaster when modal error is closing', () => {
|
||||
let mockErrorToast: AppToast = cloneDeep(mockToast);
|
||||
mockErrorToast.id = 'id-super-id-error';
|
||||
mockErrorToast = set('errors', [mockErrorToast.text], mockErrorToast);
|
||||
|
||||
const AddToaster = () => {
|
||||
const [{ toasts }, dispatch] = useStateToaster();
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
data-test-subj="add-toast"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
dispatch({ type: 'addToaster', toast: mockErrorToast });
|
||||
dispatch({ type: 'addToaster', toast: mockToast });
|
||||
}}
|
||||
/>
|
||||
{toasts.map(toast => (
|
||||
<span key={`add-toaster-${toast.id}`}>{`${toast.title} ${toast.text}`}</span>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
const wrapper = mount(
|
||||
<ManageGlobalToaster>
|
||||
<AddToaster />
|
||||
<GlobalToaster />
|
||||
</ManageGlobalToaster>
|
||||
);
|
||||
wrapper.find('[data-test-subj="add-toast"]').simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
wrapper.find('button[data-test-subj="toaster-show-all-error-modal"]').simulate('click');
|
||||
|
||||
wrapper.update();
|
||||
|
||||
wait(
|
||||
() => {
|
||||
expect(wrapper.find('.euiToast').length).toBe(0);
|
||||
|
||||
wrapper.find('[data-test-subj="modal-all-errors-close"]').simulate('click');
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('.euiToast').length).toBe(1);
|
||||
expect(wrapper.find('.euiToastHeader__title').text()).toBe('Test & Test II');
|
||||
},
|
||||
{ timeout: 110 }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
122
x-pack/legacy/plugins/siem/public/components/toasters/index.tsx
Normal file
122
x-pack/legacy/plugins/siem/public/components/toasters/index.tsx
Normal file
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* 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, EuiButton } from '@elastic/eui';
|
||||
import { noop } from 'lodash/fp';
|
||||
import React, { createContext, Dispatch, useReducer, useContext, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ModalAllErrors } from './modal_all_errors';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export interface AppToast extends Toast {
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
interface ToastState {
|
||||
toasts: AppToast[];
|
||||
}
|
||||
|
||||
const initialToasterState: ToastState = {
|
||||
toasts: [],
|
||||
};
|
||||
|
||||
export type ActionToaster =
|
||||
| { type: 'addToaster'; toast: AppToast }
|
||||
| { type: 'deleteToaster'; id: string }
|
||||
| { type: 'toggleWaitToShowNextToast' };
|
||||
|
||||
export const StateToasterContext = createContext<[ToastState, Dispatch<ActionToaster>]>([
|
||||
initialToasterState,
|
||||
() => noop,
|
||||
]);
|
||||
|
||||
export const useStateToaster = () => useContext(StateToasterContext);
|
||||
|
||||
interface ManageGlobalToasterProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ManageGlobalToaster = ({ children }: ManageGlobalToasterProps) => {
|
||||
const reducerToaster = (state: ToastState, action: ActionToaster) => {
|
||||
switch (action.type) {
|
||||
case 'addToaster':
|
||||
return { ...state, toasts: [...state.toasts, action.toast] };
|
||||
case 'deleteToaster':
|
||||
return { ...state, toasts: state.toasts.filter(msg => msg.id !== action.id) };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StateToasterContext.Provider value={useReducer(reducerToaster, initialToasterState)}>
|
||||
{children}
|
||||
</StateToasterContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
interface GlobalToasterProps {
|
||||
toastLifeTimeMs?: number;
|
||||
}
|
||||
|
||||
export const GlobalToaster = ({ toastLifeTimeMs = 5000 }: GlobalToasterProps) => {
|
||||
const [{ toasts }, dispatch] = useStateToaster();
|
||||
const [isShowing, setIsShowing] = useState(false);
|
||||
const [toastInModal, setToastInModal] = useState<AppToast | null>(null);
|
||||
|
||||
const toggle = (toast: AppToast) => {
|
||||
if (isShowing) {
|
||||
dispatch({ type: 'deleteToaster', id: toast.id });
|
||||
setToastInModal(null);
|
||||
} else {
|
||||
setToastInModal(toast);
|
||||
}
|
||||
setIsShowing(!isShowing);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{toasts.length > 0 && !isShowing && (
|
||||
<EuiGlobalToastList
|
||||
toasts={[formatToErrorToastIfNeeded(toasts[0], toggle)]}
|
||||
dismissToast={({ id }) => {
|
||||
dispatch({ type: 'deleteToaster', id });
|
||||
}}
|
||||
toastLifeTimeMs={toastLifeTimeMs}
|
||||
/>
|
||||
)}
|
||||
{toastInModal != null && (
|
||||
<ModalAllErrors isShowing={isShowing} toast={toastInModal} toggle={toggle} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const formatToErrorToastIfNeeded = (
|
||||
toast: AppToast,
|
||||
toggle: (toast: AppToast) => void
|
||||
): AppToast => {
|
||||
if (toast != null && toast.errors != null && toast.errors.length > 0) {
|
||||
toast.text = (
|
||||
<ErrorToastContainer>
|
||||
<EuiButton
|
||||
data-test-subj="toaster-show-all-error-modal"
|
||||
size="s"
|
||||
color="danger"
|
||||
onClick={() => toast != null && toggle(toast)}
|
||||
>
|
||||
{i18n.SEE_ALL_ERRORS}
|
||||
</EuiButton>
|
||||
</ErrorToastContainer>
|
||||
);
|
||||
}
|
||||
return toast;
|
||||
};
|
||||
|
||||
const ErrorToastContainer = styled.div`
|
||||
text-align: right;
|
||||
`;
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 { shallow } from 'enzyme';
|
||||
import toJson from 'enzyme-to-json';
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { ModalAllErrors } from './modal_all_errors';
|
||||
import { AppToast } from '.';
|
||||
import { cloneDeep } from 'lodash/fp';
|
||||
|
||||
const mockToast: AppToast = {
|
||||
color: 'danger',
|
||||
id: 'id-super-id',
|
||||
iconType: 'alert',
|
||||
title: 'Test & Test',
|
||||
errors: [
|
||||
'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('Modal all errors', () => {
|
||||
const toggle = jest.fn();
|
||||
describe('rendering', () => {
|
||||
test('it renders the default all errors modal when isShowing is positive', () => {
|
||||
const wrapper = shallow(
|
||||
<ModalAllErrors isShowing={true} toast={mockToast} toggle={toggle} />
|
||||
);
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it renders null when isShowing is negative', () => {
|
||||
const wrapper = shallow(
|
||||
<ModalAllErrors isShowing={false} toast={mockToast} toggle={toggle} />
|
||||
);
|
||||
expect(wrapper.html()).toEqual(null);
|
||||
});
|
||||
|
||||
test('it renders multiple errors in modal', () => {
|
||||
const mockToastWithTwoError = cloneDeep(mockToast);
|
||||
mockToastWithTwoError.errors = [
|
||||
'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.',
|
||||
'Error 2, 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.',
|
||||
'Error 3, 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.',
|
||||
];
|
||||
const wrapper = shallow(
|
||||
<ModalAllErrors isShowing={true} toast={mockToastWithTwoError} toggle={toggle} />
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="modal-all-errors-accordion"]').length).toBe(
|
||||
mockToastWithTwoError.errors.length
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
test('Make sure that toggle function has been called when you click on the close button', () => {
|
||||
const wrapper = shallow(
|
||||
<ModalAllErrors isShowing={true} toast={mockToast} toggle={toggle} />
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="modal-all-errors-close"]').simulate('click');
|
||||
wrapper.update();
|
||||
expect(toggle).toHaveBeenCalledWith(mockToast);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiOverlayMask,
|
||||
EuiModal,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiModalBody,
|
||||
EuiCallOut,
|
||||
EuiSpacer,
|
||||
EuiCodeBlock,
|
||||
EuiModalFooter,
|
||||
EuiAccordion,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AppToast } from '.';
|
||||
import * as i18n from './translations';
|
||||
|
||||
interface FullErrorProps {
|
||||
isShowing: boolean;
|
||||
toast: AppToast;
|
||||
toggle: (toast: AppToast) => void;
|
||||
}
|
||||
|
||||
export const ModalAllErrors = ({ isShowing, toast, toggle }: FullErrorProps) =>
|
||||
isShowing && toast != null ? (
|
||||
<EuiOverlayMask>
|
||||
<EuiModal onClose={() => toggle(toast)}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{i18n.TITLE_ERROR_MODAL}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<EuiCallOut title={toast.title} color="danger" size="s" iconType="alert" />
|
||||
<EuiSpacer size="s" />
|
||||
{toast.errors != null &&
|
||||
toast.errors.map((error, index) => (
|
||||
<EuiAccordion
|
||||
key={`${toast.id}-${index}`}
|
||||
id="accordion1"
|
||||
initialIsOpen={index === 0 ? true : false}
|
||||
buttonContent={error.length > 100 ? `${error.substring(0, 100)} ...` : error}
|
||||
data-test-subj="modal-all-errors-accordion"
|
||||
>
|
||||
<MyEuiCodeBlock>{error}</MyEuiCodeBlock>
|
||||
</EuiAccordion>
|
||||
))}
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButton onClick={() => toggle(toast)} fill data-test-subj="modal-all-errors-close">
|
||||
{i18n.CLOSE_ERROR_MODAL}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
</EuiOverlayMask>
|
||||
) : null;
|
||||
|
||||
const MyEuiCodeBlock = styled(EuiCodeBlock)`
|
||||
margin-top: 4px;
|
||||
`;
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
export const SEE_ALL_ERRORS = i18n.translate('xpack.siem.modalAllErrors.seeAllErrors.button', {
|
||||
defaultMessage: 'See the full error(s)',
|
||||
});
|
||||
|
||||
export const TITLE_ERROR_MODAL = i18n.translate('xpack.siem.modalAllErrors.title', {
|
||||
defaultMessage: 'Your visualization has error(s)',
|
||||
});
|
||||
|
||||
export const CLOSE_ERROR_MODAL = i18n.translate('xpack.siem.modalAllErrors.close.button', {
|
||||
defaultMessage: 'Close',
|
||||
});
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { onError, ErrorLink } from 'apollo-link-error';
|
||||
import { get } from 'lodash/fp';
|
||||
import { get, throttle, noop } from 'lodash/fp';
|
||||
|
||||
import uuid from 'uuid';
|
||||
import * as i18n from './translations';
|
||||
|
@ -15,21 +15,24 @@ import { appActions } from '../../store/actions';
|
|||
|
||||
export const errorLinkHandler: ErrorLink.ErrorHandler = ({ graphQLErrors, networkError }) => {
|
||||
const store = getStore();
|
||||
const dispatch = throttle(50, store != null ? store.dispatch : noop);
|
||||
|
||||
if (graphQLErrors != null && store != null) {
|
||||
graphQLErrors.forEach(({ message }) =>
|
||||
store.dispatch(
|
||||
appActions.addError({ id: uuid.v4(), title: i18n.DATA_FETCH_FAILURE, message })
|
||||
)
|
||||
dispatch(
|
||||
appActions.addError({
|
||||
id: uuid.v4(),
|
||||
title: i18n.DATA_FETCH_FAILURE,
|
||||
message: graphQLErrors.map(({ message }) => message),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (networkError != null && store != null) {
|
||||
store.dispatch(
|
||||
dispatch(
|
||||
appActions.addError({
|
||||
id: uuid.v4(),
|
||||
title: i18n.NETWORK_FAILURE,
|
||||
message: networkError.message,
|
||||
message: [networkError.message],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,12 +4,13 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiGlobalToastList, EuiGlobalToastListToast as Toast } from '@elastic/eui';
|
||||
import { EuiGlobalToastListToast as Toast, EuiButtonIcon } from '@elastic/eui';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import * as React from 'react';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import * as i18n from './translations';
|
||||
import { useStateToaster } from '../../components/toasters';
|
||||
|
||||
export type OnCopy = ({
|
||||
content,
|
||||
|
@ -38,44 +39,9 @@ interface Props {
|
|||
toastLifeTimeMs?: number;
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: Toast[];
|
||||
}
|
||||
|
||||
export class Clipboard extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
toasts: [],
|
||||
};
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { toastLifeTimeMs = 5000 } = this.props;
|
||||
|
||||
// TODO: 1 error is: Visible, non-interactive elements with click handlers must have at least one keyboard listener
|
||||
// TODO: 2 error is: Elements with the 'button' interactive role must be focusable
|
||||
// TODO: Investigate this error
|
||||
/* eslint-disable */
|
||||
return (
|
||||
<>
|
||||
<div role="button" data-test-subj="clipboard" onClick={this.onClick}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
<EuiGlobalToastList
|
||||
toasts={this.state.toasts}
|
||||
dismissToast={this.removeToast}
|
||||
toastLifeTimeMs={toastLifeTimeMs}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
/* eslint-enable */
|
||||
}
|
||||
|
||||
private onClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
const { content, onCopy, titleSummary } = this.props;
|
||||
|
||||
export const Clipboard = ({ children, content, onCopy, titleSummary, toastLifeTimeMs }: Props) => {
|
||||
const dispatchToaster = useStateToaster()[1];
|
||||
const onClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
|
@ -86,15 +52,23 @@ export class Clipboard extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
if (isSuccess) {
|
||||
this.setState(prevState => ({
|
||||
toasts: [...prevState.toasts, getSuccessToast({ titleSummary })],
|
||||
}));
|
||||
dispatchToaster({
|
||||
type: 'addToaster',
|
||||
toast: { toastLifeTimeMs, ...getSuccessToast({ titleSummary }) },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private removeToast = (removedToast: Toast): void => {
|
||||
this.setState(prevState => ({
|
||||
toasts: prevState.toasts.filter(toast => toast.id !== removedToast.id),
|
||||
}));
|
||||
};
|
||||
}
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.COPY_TO_THE_CLIPBOARD}
|
||||
color="subdued"
|
||||
data-test-subj="clipboard"
|
||||
iconSize="s"
|
||||
iconType="copyClipboard"
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</EuiButtonIcon>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -17,3 +17,7 @@ export const COPIED = i18n.translate('xpack.siem.clipboard.copied', {
|
|||
export const TO_THE_CLIPBOARD = i18n.translate('xpack.siem.clipboard.to.the.clipboard', {
|
||||
defaultMessage: 'to the clipboard',
|
||||
});
|
||||
|
||||
export const COPY_TO_THE_CLIPBOARD = i18n.translate('xpack.siem.clipboard.copy.to.the.clipboard', {
|
||||
defaultMessage: 'Copy to the clipboard',
|
||||
});
|
||||
|
|
|
@ -27,7 +27,7 @@ export const WithCopyToClipboard = pure<{ text: string; titleSummary?: string }>
|
|||
({ text, titleSummary, children }) => (
|
||||
<WithCopyToClipboardContainer>
|
||||
<>{children}</>
|
||||
<Clipboard content={text} titleSummary={titleSummary}>
|
||||
<Clipboard content={text} titleSummary={titleSummary} toastLifeTimeMs={800}>
|
||||
<EuiIcon
|
||||
color="text"
|
||||
type="copyClipboard"
|
||||
|
|
|
@ -24,8 +24,8 @@ export const mockGlobalState: State = {
|
|||
app: {
|
||||
notesById: {},
|
||||
errors: [
|
||||
{ id: 'error-id-1', title: 'title-1', message: 'error-message-1' },
|
||||
{ id: 'error-id-2', title: 'title-2', message: 'error-message-2' },
|
||||
{ id: 'error-id-1', title: 'title-1', message: ['error-message-1'] },
|
||||
{ id: 'error-id-2', title: 'title-2', message: ['error-message-2'] },
|
||||
],
|
||||
},
|
||||
hosts: {
|
||||
|
|
|
@ -14,6 +14,8 @@ export const updateNote = actionCreator<{ note: Note }>('UPDATE_NOTE');
|
|||
|
||||
export const addNotes = actionCreator<{ notes: Note[] }>('ADD_NOTE');
|
||||
|
||||
export const addError = actionCreator<{ id: string; title: string; message: string }>('ADD_ERRORS');
|
||||
export const addError = actionCreator<{ id: string; title: string; message: string[] }>(
|
||||
'ADD_ERRORS'
|
||||
);
|
||||
|
||||
export const removeError = actionCreator<{ id: string }>('REMOVE_ERRORS');
|
||||
|
|
|
@ -15,7 +15,7 @@ export interface NotesById {
|
|||
export interface Error {
|
||||
id: string;
|
||||
title: string;
|
||||
message: string;
|
||||
message: string[];
|
||||
}
|
||||
|
||||
export type ErrorModel = Error[];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue