[CANVAS] Moves notify to a canvas service (#63268)

* Moves notify to a canvas service

* Typecheck fix
This commit is contained in:
Corey Robertson 2020-04-24 10:53:27 -04:00 committed by GitHub
parent dfddcdd903
commit 4051c94568
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 287 additions and 152 deletions

View file

@ -33,6 +33,9 @@ import { ACTION_VALUE_CLICK } from '../../../../../src/plugins/data/public/actio
import { init as initStatsReporter } from './lib/ui_metric'; import { init as initStatsReporter } from './lib/ui_metric';
import { CapabilitiesStrings } from '../i18n'; import { CapabilitiesStrings } from '../i18n';
import { startServices, stopServices, services } from './services';
const { ReadOnlyBadge: strings } = CapabilitiesStrings; const { ReadOnlyBadge: strings } = CapabilitiesStrings;
let restoreAction: ActionByType<any> | undefined; let restoreAction: ActionByType<any> | undefined;
@ -51,8 +54,14 @@ export const renderApp = (
{ element }: AppMountParameters, { element }: AppMountParameters,
canvasStore: Store canvasStore: Store
) => { ) => {
const canvasServices = Object.entries(services).reduce((reduction, [key, provider]) => {
reduction[key] = provider.getService();
return reduction;
}, {} as Record<string, any>);
ReactDOM.render( ReactDOM.render(
<KibanaContextProvider services={{ ...plugins, ...coreStart }}> <KibanaContextProvider services={{ ...plugins, ...coreStart, canvas: canvasServices }}>
<I18nProvider> <I18nProvider>
<Provider store={canvasStore}> <Provider store={canvasStore}>
<App /> <App />
@ -71,6 +80,8 @@ export const initializeCanvas = async (
startPlugins: CanvasStartDeps, startPlugins: CanvasStartDeps,
registries: SetupRegistries registries: SetupRegistries
) => { ) => {
startServices(coreSetup, coreStart, setupPlugins, startPlugins);
// Create Store // Create Store
const canvasStore = await createStore(coreSetup, setupPlugins); const canvasStore = await createStore(coreSetup, setupPlugins);
@ -130,6 +141,7 @@ export const initializeCanvas = async (
}; };
export const teardownCanvas = (coreStart: CoreStart, startPlugins: CanvasStartDeps) => { export const teardownCanvas = (coreStart: CoreStart, startPlugins: CanvasStartDeps) => {
stopServices();
destroyRegistries(); destroyRegistries();
resetInterpreter(); resetInterpreter();

View file

@ -6,7 +6,7 @@
import { ErrorStrings } from '../../../i18n'; import { ErrorStrings } from '../../../i18n';
import * as workpadService from '../../lib/workpad_service'; import * as workpadService from '../../lib/workpad_service';
import { notify } from '../../lib/notify'; import { notifyService } from '../../services';
import { getBaseBreadcrumb, getWorkpadBreadcrumb, setBreadcrumb } from '../../lib/breadcrumbs'; import { getBaseBreadcrumb, getWorkpadBreadcrumb, setBreadcrumb } from '../../lib/breadcrumbs';
import { getDefaultWorkpad } from '../../state/defaults'; import { getDefaultWorkpad } from '../../state/defaults';
import { setWorkpad } from '../../state/actions/workpad'; import { setWorkpad } from '../../state/actions/workpad';
@ -33,7 +33,9 @@ export const routes = [
dispatch(resetAssets()); dispatch(resetAssets());
router.redirectTo('loadWorkpad', { id: newWorkpad.id, page: 1 }); router.redirectTo('loadWorkpad', { id: newWorkpad.id, page: 1 });
} catch (err) { } catch (err) {
notify.error(err, { title: strings.getCreateFailureErrorMessage() }); notifyService
.getService()
.error(err, { title: strings.getCreateFailureErrorMessage() });
router.redirectTo('home'); router.redirectTo('home');
} }
}, },
@ -59,7 +61,9 @@ export const routes = [
// reset transient properties when changing workpads // reset transient properties when changing workpads
dispatch(setZoomScale(1)); dispatch(setZoomScale(1));
} catch (err) { } catch (err) {
notify.error(err, { title: strings.getLoadFailureErrorMessage() }); notifyService
.getService()
.error(err, { title: strings.getLoadFailureErrorMessage() });
return router.redirectTo('home'); return router.redirectTo('home');
} }
} }

View file

@ -15,7 +15,7 @@ import { AssetModal } from './asset_modal';
const { AssetManager: strings } = ComponentStrings; const { AssetManager: strings } = ComponentStrings;
interface Props { export interface Props {
/** A list of assets, if available */ /** A list of assets, if available */
assetValues: AssetType[]; assetValues: AssetType[];
/** Function to invoke when an asset is selected to be added as an element to the workpad */ /** Function to invoke when an asset is selected to be added as an element to the workpad */

View file

@ -8,29 +8,36 @@ import { connect } from 'react-redux';
import { compose, withProps } from 'recompose'; import { compose, withProps } from 'recompose';
import { set, get } from 'lodash'; import { set, get } from 'lodash';
import { fromExpression, toExpression } from '@kbn/interpreter/common'; import { fromExpression, toExpression } from '@kbn/interpreter/common';
import { notify } from '../../lib/notify';
import { getAssets } from '../../state/selectors/assets'; import { getAssets } from '../../state/selectors/assets';
// @ts-ignore Untyped local
import { removeAsset, createAsset } from '../../state/actions/assets'; import { removeAsset, createAsset } from '../../state/actions/assets';
// @ts-ignore Untyped local
import { elementsRegistry } from '../../lib/elements_registry'; import { elementsRegistry } from '../../lib/elements_registry';
// @ts-ignore Untyped local
import { addElement } from '../../state/actions/elements'; import { addElement } from '../../state/actions/elements';
import { getSelectedPage } from '../../state/selectors/workpad'; import { getSelectedPage } from '../../state/selectors/workpad';
import { encode } from '../../../common/lib/dataurl'; import { encode } from '../../../common/lib/dataurl';
import { getId } from '../../lib/get_id'; import { getId } from '../../lib/get_id';
// @ts-ignore Untyped Local
import { findExistingAsset } from '../../lib/find_existing_asset'; import { findExistingAsset } from '../../lib/find_existing_asset';
import { VALID_IMAGE_TYPES } from '../../../common/lib/constants'; import { VALID_IMAGE_TYPES } from '../../../common/lib/constants';
import { AssetManager as Component } from './asset_manager'; import { withKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { WithKibanaProps } from '../../';
import { AssetManager as Component, Props as AssetManagerProps } from './asset_manager';
const mapStateToProps = state => ({ import { State, ExpressionAstExpression, AssetType } from '../../../types';
const mapStateToProps = (state: State) => ({
assets: getAssets(state), assets: getAssets(state),
selectedPage: getSelectedPage(state), selectedPage: getSelectedPage(state),
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = (dispatch: (action: any) => void) => ({
onAddImageElement: pageId => assetId => { onAddImageElement: (pageId: string) => (assetId: string) => {
const imageElement = elementsRegistry.get('image'); const imageElement = elementsRegistry.get('image');
const elementAST = fromExpression(imageElement.expression); const elementAST = fromExpression(imageElement.expression);
const selector = ['chain', '0', 'arguments', 'dataurl']; const selector = ['chain', '0', 'arguments', 'dataurl'];
const subExp = [ const subExp: ExpressionAstExpression[] = [
{ {
type: 'expression', type: 'expression',
chain: [ chain: [
@ -44,11 +51,11 @@ const mapDispatchToProps = dispatch => ({
], ],
}, },
]; ];
const newAST = set(elementAST, selector, subExp); const newAST = set<ExpressionAstExpression>(elementAST, selector, subExp);
imageElement.expression = toExpression(newAST); imageElement.expression = toExpression(newAST);
dispatch(addElement(pageId, imageElement)); dispatch(addElement(pageId, imageElement));
}, },
onAssetAdd: (type, content) => { onAssetAdd: (type: string, content: string) => {
// make the ID here and pass it into the action // make the ID here and pass it into the action
const assetId = getId('asset'); const assetId = getId('asset');
dispatch(createAsset(type, content, assetId)); dispatch(createAsset(type, content, assetId));
@ -56,10 +63,14 @@ const mapDispatchToProps = dispatch => ({
// then return the id, so the caller knows the id that will be created // then return the id, so the caller knows the id that will be created
return assetId; return assetId;
}, },
onAssetDelete: assetId => dispatch(removeAsset(assetId)), onAssetDelete: (assetId: string) => dispatch(removeAsset(assetId)),
}); });
const mergeProps = (stateProps, dispatchProps, ownProps) => { const mergeProps = (
stateProps: ReturnType<typeof mapStateToProps>,
dispatchProps: ReturnType<typeof mapDispatchToProps>,
ownProps: AssetManagerProps
) => {
const { assets, selectedPage } = stateProps; const { assets, selectedPage } = stateProps;
const { onAssetAdd } = dispatchProps; const { onAssetAdd } = dispatchProps;
const assetValues = Object.values(assets); // pull values out of assets object const assetValues = Object.values(assets); // pull values out of assets object
@ -70,16 +81,16 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
onAddImageElement: dispatchProps.onAddImageElement(stateProps.selectedPage), onAddImageElement: dispatchProps.onAddImageElement(stateProps.selectedPage),
selectedPage, selectedPage,
assetValues, assetValues,
onAssetAdd: file => { onAssetAdd: (file: File) => {
const [type, subtype] = get(file, 'type', '').split('/'); const [type, subtype] = get(file, 'type', '').split('/');
if (type === 'image' && VALID_IMAGE_TYPES.indexOf(subtype) >= 0) { if (type === 'image' && VALID_IMAGE_TYPES.indexOf(subtype) >= 0) {
return encode(file).then(dataurl => { return encode(file).then(dataurl => {
const type = 'dataurl'; const dataurlType = 'dataurl';
const existingId = findExistingAsset(type, dataurl, assetValues); const existingId = findExistingAsset(dataurlType, dataurl, assetValues);
if (existingId) { if (existingId) {
return existingId; return existingId;
} }
return onAssetAdd(type, dataurl); return onAssetAdd(dataurlType, dataurl);
}); });
} }
@ -88,7 +99,11 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
}; };
}; };
export const AssetManager = compose( export const AssetManager = compose<any, any>(
connect(mapStateToProps, mapDispatchToProps, mergeProps), connect(mapStateToProps, mapDispatchToProps, mergeProps),
withProps({ onAssetCopy: asset => notify.success(`Copied '${asset.id}' to clipboard`) }) withKibana,
withProps(({ kibana }: WithKibanaProps) => ({
onAssetCopy: (asset: AssetType) =>
kibana.services.canvas.notify.success(`Copied '${asset.id}' to clipboard`),
}))
)(Component); )(Component);

View file

@ -7,7 +7,7 @@
import { compose, withProps, withPropsOnChange } from 'recompose'; import { compose, withProps, withPropsOnChange } from 'recompose';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import isEqual from 'react-fast-compare'; import isEqual from 'react-fast-compare';
import { notify } from '../../lib/notify'; import { withKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { RenderWithFn as Component } from './render_with_fn'; import { RenderWithFn as Component } from './render_with_fn';
import { ElementHandlers } from './lib/handlers'; import { ElementHandlers } from './lib/handlers';
@ -19,9 +19,10 @@ export const RenderWithFn = compose(
handlers: Object.assign(new ElementHandlers(), handlers), handlers: Object.assign(new ElementHandlers(), handlers),
}) })
), ),
withProps({ withKibana,
onError: notify.error, withProps(props => ({
}) onError: props.kibana.services.canvas.notify.error,
}))
)(Component); )(Component);
RenderWithFn.propTypes = { RenderWithFn.propTypes = {

View file

@ -11,8 +11,8 @@ import { camelCase } from 'lodash';
// @ts-ignore Untyped local // @ts-ignore Untyped local
import { cloneSubgraphs } from '../../lib/clone_subgraphs'; import { cloneSubgraphs } from '../../lib/clone_subgraphs';
import * as customElementService from '../../lib/custom_element_service'; import * as customElementService from '../../lib/custom_element_service';
// @ts-ignore Untyped local import { withKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { notify } from '../../lib/notify'; import { WithKibanaProps } from '../../';
// @ts-ignore Untyped local // @ts-ignore Untyped local
import { selectToplevelNodes } from '../../state/actions/transient'; import { selectToplevelNodes } from '../../state/actions/transient';
// @ts-ignore Untyped local // @ts-ignore Untyped local
@ -64,7 +64,7 @@ const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({
const mergeProps = ( const mergeProps = (
stateProps: StateProps, stateProps: StateProps,
dispatchProps: DispatchProps, dispatchProps: DispatchProps,
ownProps: OwnPropsWithState ownProps: OwnPropsWithState & WithKibanaProps
): ComponentProps => { ): ComponentProps => {
const { pageId } = stateProps; const { pageId } = stateProps;
const { onClose, search, setCustomElements } = ownProps; const { onClose, search, setCustomElements } = ownProps;
@ -92,7 +92,9 @@ const mergeProps = (
try { try {
await findCustomElements(); await findCustomElements();
} catch (err) { } catch (err) {
notify.error(err, { title: `Couldn't find custom elements` }); ownProps.kibana.services.canvas.notify.error(err, {
title: `Couldn't find custom elements`,
});
} }
}, },
// remove custom element // remove custom element
@ -101,7 +103,9 @@ const mergeProps = (
await customElementService.remove(id); await customElementService.remove(id);
await findCustomElements(); await findCustomElements();
} catch (err) { } catch (err) {
notify.error(err, { title: `Couldn't delete custom elements` }); ownProps.kibana.services.canvas.notify.error(err, {
title: `Couldn't delete custom elements`,
});
} }
}, },
// update custom element // update custom element
@ -115,13 +119,16 @@ const mergeProps = (
}); });
await findCustomElements(); await findCustomElements();
} catch (err) { } catch (err) {
notify.error(err, { title: `Couldn't update custom elements` }); ownProps.kibana.services.canvas.notify.error(err, {
title: `Couldn't update custom elements`,
});
} }
}, },
}; };
}; };
export const SavedElementsModal = compose<ComponentProps, OwnProps>( export const SavedElementsModal = compose<ComponentProps, OwnProps>(
withKibana,
withState('search', 'setSearch', ''), withState('search', 'setSearch', ''),
withState('customElements', 'setCustomElements', []), withState('customElements', 'setCustomElements', []),
connect(mapStateToProps, mapDispatchToProps, mergeProps) connect(mapStateToProps, mapDispatchToProps, mergeProps)

View file

@ -12,7 +12,6 @@ import {
getRenderedWorkpadExpressions, getRenderedWorkpadExpressions,
} from '../../../../state/selectors/workpad'; } from '../../../../state/selectors/workpad';
// @ts-ignore Untyped local // @ts-ignore Untyped local
import { notify } from '../../../../lib/notify';
import { import {
downloadRenderedWorkpad, downloadRenderedWorkpad,
downloadRuntime, downloadRuntime,
@ -70,7 +69,7 @@ export const ShareWebsiteFlyout = compose<ComponentProps, Pick<Props, 'onClose'>
unsupportedRenderers, unsupportedRenderers,
onClose, onClose,
onCopy: () => { onCopy: () => {
notify.info(strings.getCopyShareConfigMessage()); kibana.services.canvas.notify.info(strings.getCopyShareConfigMessage());
}, },
onDownload: type => { onDownload: type => {
switch (type) { switch (type) {
@ -86,7 +85,9 @@ export const ShareWebsiteFlyout = compose<ComponentProps, Pick<Props, 'onClose'>
.post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad)) .post(`${basePath}${API_ROUTE_SHAREABLE_ZIP}`, JSON.stringify(renderedWorkpad))
.then(blob => downloadZippedRuntime(blob.data)) .then(blob => downloadZippedRuntime(blob.data))
.catch((err: Error) => { .catch((err: Error) => {
notify.error(err, { title: strings.getShareableZipErrorTitle(workpad.name) }); kibana.services.canvas.notify.error(err, {
title: strings.getShareableZipErrorTitle(workpad.name),
});
}); });
return; return;
default: default:

View file

@ -8,8 +8,6 @@ import { connect } from 'react-redux';
import { compose, withProps } from 'recompose'; import { compose, withProps } from 'recompose';
import { jobCompletionNotifications } from '../../../../../../../plugins/reporting/public'; import { jobCompletionNotifications } from '../../../../../../../plugins/reporting/public';
import { getWorkpad, getPages } from '../../../state/selectors/workpad'; import { getWorkpad, getPages } from '../../../state/selectors/workpad';
// @ts-ignore Untyped local
import { notify } from '../../../lib/notify';
import { getWindow } from '../../../lib/get_window'; import { getWindow } from '../../../lib/get_window';
import { downloadWorkpad } from '../../../lib/download_workpad'; import { downloadWorkpad } from '../../../lib/download_workpad';
import { ShareMenu as Component, Props as ComponentProps } from './share_menu'; import { ShareMenu as Component, Props as ComponentProps } from './share_menu';
@ -59,10 +57,10 @@ export const ShareMenu = compose<ComponentProps, {}>(
onCopy: type => { onCopy: type => {
switch (type) { switch (type) {
case 'pdf': case 'pdf':
notify.info(strings.getCopyPDFMessage()); kibana.services.canvas.notify.info(strings.getCopyPDFMessage());
break; break;
case 'reportingConfig': case 'reportingConfig':
notify.info(strings.getCopyReportingConfigMessage()); kibana.services.canvas.notify.info(strings.getCopyReportingConfigMessage());
break; break;
default: default:
throw new Error(strings.getUnknownExportErrorMessage(type)); throw new Error(strings.getUnknownExportErrorMessage(type));
@ -73,7 +71,7 @@ export const ShareMenu = compose<ComponentProps, {}>(
case 'pdf': case 'pdf':
return createPdf(workpad, { pageCount }, kibana.services.http.basePath) return createPdf(workpad, { pageCount }, kibana.services.http.basePath)
.then(({ data }: { data: { job: { id: string } } }) => { .then(({ data }: { data: { job: { id: string } } }) => {
notify.info(strings.getExportPDFMessage(), { kibana.services.canvas.notify.info(strings.getExportPDFMessage(), {
title: strings.getExportPDFTitle(workpad.name), title: strings.getExportPDFTitle(workpad.name),
}); });
@ -81,7 +79,9 @@ export const ShareMenu = compose<ComponentProps, {}>(
jobCompletionNotifications.add(data.job.id); jobCompletionNotifications.add(data.job.id);
}) })
.catch((err: Error) => { .catch((err: Error) => {
notify.error(err, { title: strings.getExportPDFErrorTitle(workpad.name) }); kibana.services.canvas.notify.error(err, {
title: strings.getExportPDFErrorTitle(workpad.name),
});
}); });
case 'json': case 'json':
downloadWorkpad(workpad.id); downloadWorkpad(workpad.id);

View file

@ -9,8 +9,6 @@ import { compose, withHandlers } from 'recompose';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/'; import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public/';
import { zoomHandlerCreators } from '../../../lib/app_handler_creators'; import { zoomHandlerCreators } from '../../../lib/app_handler_creators';
// @ts-ignore Untyped local
import { notify } from '../../../lib/notify';
import { State, CanvasWorkpadBoundingBox } from '../../../../types'; import { State, CanvasWorkpadBoundingBox } from '../../../../types';
// @ts-ignore Untyped local // @ts-ignore Untyped local
import { fetchAllRenderables } from '../../../state/actions/elements'; import { fetchAllRenderables } from '../../../state/actions/elements';

View file

@ -9,7 +9,6 @@ import { connect } from 'react-redux';
import { compose, withState, getContext, withHandlers, withProps } from 'recompose'; import { compose, withState, getContext, withHandlers, withProps } from 'recompose';
import moment from 'moment'; import moment from 'moment';
import * as workpadService from '../../lib/workpad_service'; import * as workpadService from '../../lib/workpad_service';
import { notify } from '../../lib/notify';
import { canUserWrite } from '../../state/selectors/app'; import { canUserWrite } from '../../state/selectors/app';
import { getWorkpad } from '../../state/selectors/workpad'; import { getWorkpad } from '../../state/selectors/workpad';
import { getId } from '../../lib/get_id'; import { getId } from '../../lib/get_id';
@ -32,7 +31,11 @@ export const WorkpadLoader = compose(
}), }),
connect(mapStateToProps), connect(mapStateToProps),
withState('workpads', 'setWorkpads', null), withState('workpads', 'setWorkpads', null),
withHandlers({ withKibana,
withProps(({ kibana }) => ({
notify: kibana.services.canvas.notify,
})),
withHandlers(({ kibana }) => ({
// Workpad creation via navigation // Workpad creation via navigation
createWorkpad: props => async workpad => { createWorkpad: props => async workpad => {
// workpad data uploaded, create and load it // workpad data uploaded, create and load it
@ -41,7 +44,9 @@ export const WorkpadLoader = compose(
await workpadService.create(workpad); await workpadService.create(workpad);
props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 }); props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 });
} catch (err) { } catch (err) {
notify.error(err, { title: errors.getUploadFailureErrorMessage() }); kibana.services.canvas.notify.error(err, {
title: errors.getUploadFailureErrorMessage(),
});
} }
return; return;
} }
@ -55,7 +60,7 @@ export const WorkpadLoader = compose(
const workpads = await workpadService.find(text); const workpads = await workpadService.find(text);
setWorkpads(workpads); setWorkpads(workpads);
} catch (err) { } catch (err) {
notify.error(err, { title: errors.getFindFailureErrorMessage() }); kibana.services.canvas.notify.error(err, { title: errors.getFindFailureErrorMessage() });
} }
}, },
@ -71,7 +76,7 @@ export const WorkpadLoader = compose(
await workpadService.create(workpad); await workpadService.create(workpad);
props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 }); props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 });
} catch (err) { } catch (err) {
notify.error(err, { title: errors.getCloneFailureErrorMessage() }); kibana.services.canvas.notify.error(err, { title: errors.getCloneFailureErrorMessage() });
} }
}, },
@ -92,7 +97,7 @@ export const WorkpadLoader = compose(
return Promise.all(removeWorkpads).then(results => { return Promise.all(removeWorkpads).then(results => {
let redirectHome = false; let redirectHome = false;
const [passes, errors] = results.reduce( const [passes, errored] = results.reduce(
([passes, errors], result) => { ([passes, errors], result) => {
if (result.id === loadedWorkpad && !result.err) { if (result.id === loadedWorkpad && !result.err) {
redirectHome = true; redirectHome = true;
@ -116,8 +121,8 @@ export const WorkpadLoader = compose(
workpads: remainingWorkpads, workpads: remainingWorkpads,
}; };
if (errors.length > 0) { if (errored.length > 0) {
notify.error(errors.getDeleteFailureErrorMessage()); kibana.services.canvas.notify.error(errors.getDeleteFailureErrorMessage());
} }
setWorkpads(workpadState); setWorkpads(workpadState);
@ -126,11 +131,10 @@ export const WorkpadLoader = compose(
props.router.navigateTo('home'); props.router.navigateTo('home');
} }
return errors.map(({ id }) => id); return errored.map(({ id }) => id);
}); });
}, },
}), })),
withKibana,
withProps(props => ({ withProps(props => ({
formatDate: date => { formatDate: date => {
const dateFormat = props.kibana.services.uiSettings.get('dateFormat'); const dateFormat = props.kibana.services.uiSettings.get('dateFormat');

View file

@ -6,12 +6,11 @@
import { get } from 'lodash'; import { get } from 'lodash';
import { getId } from '../../lib/get_id'; import { getId } from '../../lib/get_id';
import { notify } from '../../lib/notify';
import { ErrorStrings } from '../../../i18n'; import { ErrorStrings } from '../../../i18n';
const { WorkpadFileUpload: errors } = ErrorStrings; const { WorkpadFileUpload: errors } = ErrorStrings;
export const uploadWorkpad = (file, onUpload) => { export const uploadWorkpad = (file, onUpload, notify) => {
if (!file) { if (!file) {
return; return;
} }

View file

@ -6,7 +6,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { compose, withHandlers } from 'recompose'; import { compose, withHandlers } from 'recompose';
import { notify } from '../../../lib/notify';
import { uploadWorkpad } from '../upload_workpad'; import { uploadWorkpad } from '../upload_workpad';
import { ErrorStrings } from '../../../../i18n'; import { ErrorStrings } from '../../../../i18n';
import { WorkpadDropzone as Component } from './workpad_dropzone'; import { WorkpadDropzone as Component } from './workpad_dropzone';
@ -14,7 +13,7 @@ import { WorkpadDropzone as Component } from './workpad_dropzone';
const { WorkpadFileUpload: errors } = ErrorStrings; const { WorkpadFileUpload: errors } = ErrorStrings;
export const WorkpadDropzone = compose( export const WorkpadDropzone = compose(
withHandlers({ withHandlers(({ notify }) => ({
onDropAccepted: ({ onUpload }) => ([file]) => uploadWorkpad(file, onUpload), onDropAccepted: ({ onUpload }) => ([file]) => uploadWorkpad(file, onUpload),
onDropRejected: () => ([file]) => { onDropRejected: () => ([file]) => {
notify.warning(errors.getAcceptJSONOnlyErrorMessage(), { notify.warning(errors.getAcceptJSONOnlyErrorMessage(), {
@ -23,7 +22,7 @@ export const WorkpadDropzone = compose(
: errors.getFileUploadFailureWithoutFileNameErrorMessage(), : errors.getFileUploadFailureWithoutFileNameErrorMessage(),
}); });
}, },
}) }))
)(Component); )(Component);
WorkpadDropzone.propTypes = { WorkpadDropzone.propTypes = {

View file

@ -249,7 +249,11 @@ export class WorkpadLoader extends React.PureComponent {
return ( return (
<Fragment> <Fragment>
<WorkpadDropzone onUpload={this.onUpload} disabled={createPending || !canUserWrite}> <WorkpadDropzone
onUpload={this.onUpload}
disabled={createPending || !canUserWrite}
notify={this.props.notify}
>
<EuiBasicTable <EuiBasicTable
items={rows} items={rows}
itemId="id" itemId="id"
@ -327,7 +331,7 @@ export class WorkpadLoader extends React.PureComponent {
compressed compressed
className="canvasWorkpad__upload--compressed" className="canvasWorkpad__upload--compressed"
initialPromptText={strings.getFilePickerPlaceholder()} initialPromptText={strings.getFilePickerPlaceholder()}
onChange={([file]) => uploadWorkpad(file, this.onUpload)} onChange={([file]) => uploadWorkpad(file, this.onUpload, this.props.notify)}
accept="application/json" accept="application/json"
disabled={createPending || !canUserWrite} disabled={createPending || !canUserWrite}
/> />

View file

@ -7,9 +7,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { compose, getContext, withHandlers, withProps } from 'recompose'; import { compose, getContext, withHandlers, withProps } from 'recompose';
import * as workpadService from '../../lib/workpad_service'; import * as workpadService from '../../lib/workpad_service';
import { notify } from '../../lib/notify';
import { getId } from '../../lib/get_id'; import { getId } from '../../lib/get_id';
import { templatesRegistry } from '../../lib/templates_registry'; import { templatesRegistry } from '../../lib/templates_registry';
import { withKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { WorkpadTemplates as Component } from './workpad_templates'; import { WorkpadTemplates as Component } from './workpad_templates';
export const WorkpadTemplates = compose( export const WorkpadTemplates = compose(
@ -19,7 +19,8 @@ export const WorkpadTemplates = compose(
withProps(() => ({ withProps(() => ({
templates: templatesRegistry.toJS(), templates: templatesRegistry.toJS(),
})), })),
withHandlers({ withKibana,
withHandlers(({ kibana }) => ({
// Clone workpad given an id // Clone workpad given an id
cloneWorkpad: props => workpad => { cloneWorkpad: props => workpad => {
workpad.id = getId('workpad'); workpad.id = getId('workpad');
@ -31,7 +32,9 @@ export const WorkpadTemplates = compose(
return workpadService return workpadService
.create(workpad) .create(workpad)
.then(() => props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 })) .then(() => props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 }))
.catch(err => notify.error(err, { title: `Couldn't clone workpad template` })); .catch(err =>
kibana.services.canvas.notify.error(err, { title: `Couldn't clone workpad template` })
);
}, },
}) }))
)(Component); )(Component);

View file

@ -10,6 +10,7 @@ import {
CoreStart, CoreStart,
} from '../../../../../src/core/public'; } from '../../../../../src/core/public';
import { CanvasSetup, CanvasStart, CanvasSetupDeps, CanvasStartDeps, CanvasPlugin } from './plugin'; import { CanvasSetup, CanvasStart, CanvasSetupDeps, CanvasStartDeps, CanvasPlugin } from './plugin';
import { CanvasServices } from './services';
export const plugin: PluginInitializer< export const plugin: PluginInitializer<
CanvasSetup, CanvasSetup,
@ -22,7 +23,7 @@ export const plugin: PluginInitializer<
export interface WithKibanaProps { export interface WithKibanaProps {
kibana: { kibana: {
services: CoreStart & CanvasStartDeps; services: CoreStart & CanvasStartDeps & { canvas: CanvasServices };
}; };
} }

View file

@ -6,8 +6,7 @@
import fileSaver from 'file-saver'; import fileSaver from 'file-saver';
import { API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD } from '../../common/lib/constants'; import { API_ROUTE_SHAREABLE_RUNTIME_DOWNLOAD } from '../../common/lib/constants';
import { ErrorStrings } from '../../i18n'; import { ErrorStrings } from '../../i18n';
// @ts-ignore untyped local import { notifyService } from '../services';
import { notify } from './notify';
// @ts-ignore untyped local // @ts-ignore untyped local
import * as workpadService from './workpad_service'; import * as workpadService from './workpad_service';
import { CanvasRenderedWorkpad } from '../../shareable_runtime/types'; import { CanvasRenderedWorkpad } from '../../shareable_runtime/types';
@ -20,7 +19,7 @@ export const downloadWorkpad = async (workpadId: string) => {
const jsonBlob = new Blob([JSON.stringify(workpad)], { type: 'application/json' }); const jsonBlob = new Blob([JSON.stringify(workpad)], { type: 'application/json' });
fileSaver.saveAs(jsonBlob, `canvas-workpad-${workpad.name}-${workpad.id}.json`); fileSaver.saveAs(jsonBlob, `canvas-workpad-${workpad.name}-${workpad.id}.json`);
} catch (err) { } catch (err) {
notify.error(err, { title: strings.getDownloadFailureErrorMessage() }); notifyService.getService().error(err, { title: strings.getDownloadFailureErrorMessage() });
} }
}; };
@ -32,7 +31,9 @@ export const downloadRenderedWorkpad = async (renderedWorkpad: CanvasRenderedWor
`canvas-embed-workpad-${renderedWorkpad.name}-${renderedWorkpad.id}.json` `canvas-embed-workpad-${renderedWorkpad.name}-${renderedWorkpad.id}.json`
); );
} catch (err) { } catch (err) {
notify.error(err, { title: strings.getDownloadRenderedWorkpadFailureErrorMessage() }); notifyService
.getService()
.error(err, { title: strings.getDownloadRenderedWorkpadFailureErrorMessage() });
} }
}; };
@ -42,7 +43,9 @@ export const downloadRuntime = async (basePath: string) => {
window.open(path); window.open(path);
return; return;
} catch (err) { } catch (err) {
notify.error(err, { title: strings.getDownloadRuntimeFailureErrorMessage() }); notifyService
.getService()
.error(err, { title: strings.getDownloadRuntimeFailureErrorMessage() });
} }
}; };
@ -51,6 +54,8 @@ export const downloadZippedRuntime = async (data: any) => {
const zip = new Blob([data], { type: 'octet/stream' }); const zip = new Blob([data], { type: 'octet/stream' });
fileSaver.saveAs(zip, 'canvas-workpad-embed.zip'); fileSaver.saveAs(zip, 'canvas-workpad-embed.zip');
} catch (err) { } catch (err) {
notify.error(err, { title: strings.getDownloadZippedRuntimeFailureErrorMessage() }); notifyService
.getService()
.error(err, { title: strings.getDownloadZippedRuntimeFailureErrorMessage() });
} }
}; };

View file

@ -4,14 +4,12 @@
* you may not use this file except in compliance with the Elastic License. * you may not use this file except in compliance with the Elastic License.
*/ */
import { Http2ServerResponse } from 'http2';
import { camelCase } from 'lodash'; import { camelCase } from 'lodash';
// @ts-ignore unconverted local file // @ts-ignore unconverted local file
import { getClipboardData, setClipboardData } from './clipboard'; import { getClipboardData, setClipboardData } from './clipboard';
// @ts-ignore unconverted local file // @ts-ignore unconverted local file
import { cloneSubgraphs } from './clone_subgraphs'; import { cloneSubgraphs } from './clone_subgraphs';
// @ts-ignore unconverted local file import { notifyService } from '../services';
import { notify } from './notify';
import * as customElementService from './custom_element_service'; import * as customElementService from './custom_element_service';
import { getId } from './get_id'; import { getId } from './get_id';
import { PositionedElement } from '../../types'; import { PositionedElement } from '../../types';
@ -86,15 +84,17 @@ export const basicHandlerCreators = {
customElementService customElementService
.create(customElement) .create(customElement)
.then(() => .then(() =>
notify.success( notifyService
`Custom element '${customElement.displayName || customElement.id}' was saved`, .getService()
{ .success(
'data-test-subj': 'canvasCustomElementCreate-success', `Custom element '${customElement.displayName || customElement.id}' was saved`,
} {
) 'data-test-subj': 'canvasCustomElementCreate-success',
}
)
) )
.catch((result: Http2ServerResponse) => .catch((error: Error) =>
notify.warning(result, { notifyService.getService().warning(error, {
title: `Custom element '${customElement.displayName || title: `Custom element '${customElement.displayName ||
customElement.id}' was not saved`, customElement.id}' was not saved`,
}) })
@ -138,13 +138,13 @@ export const clipboardHandlerCreators = {
if (selectedNodes.length) { if (selectedNodes.length) {
setClipboardData({ selectedNodes }); setClipboardData({ selectedNodes });
removeNodes(selectedNodes.map(extractId), pageId); removeNodes(selectedNodes.map(extractId), pageId);
notify.success('Cut element to clipboard'); notifyService.getService().success('Cut element to clipboard');
} }
}, },
copyNodes: ({ selectedNodes }: Props) => (): void => { copyNodes: ({ selectedNodes }: Props) => (): void => {
if (selectedNodes.length) { if (selectedNodes.length) {
setClipboardData({ selectedNodes }); setClipboardData({ selectedNodes });
notify.success('Copied element to clipboard'); notifyService.getService().success('Copied element to clipboard');
} }
}, },
pasteNodes: ({ insertNodes, pageId, selectToplevelNodes }: Props) => (): void => { pasteNodes: ({ insertNodes, pageId, selectToplevelNodes }: Props) => (): void => {

View file

@ -10,8 +10,7 @@ import { API_ROUTE } from '../../common/lib/constants';
// @ts-ignore untyped local // @ts-ignore untyped local
import { fetch } from '../../common/lib/fetch'; import { fetch } from '../../common/lib/fetch';
import { ErrorStrings } from '../../i18n'; import { ErrorStrings } from '../../i18n';
// @ts-ignore untyped local import { notifyService } from '../services';
import { notify } from './notify';
import { getCoreStart } from '../legacy'; import { getCoreStart } from '../legacy';
const { esService: strings } = ErrorStrings; const { esService: strings } = ErrorStrings;
@ -38,7 +37,7 @@ export const getFields = (index = '_all') => {
.sort() .sort()
) )
.catch((err: Error) => .catch((err: Error) =>
notify.error(err, { notifyService.getService().error(err, {
title: strings.getFieldsFetchErrorMessage(index), title: strings.getFieldsFetchErrorMessage(index),
}) })
); );
@ -57,7 +56,9 @@ export const getIndices = () =>
return savedObject.attributes.title; return savedObject.attributes.title;
}); });
}) })
.catch((err: Error) => notify.error(err, { title: strings.getIndicesFetchErrorMessage() })); .catch((err: Error) =>
notifyService.getService().error(err, { title: strings.getIndicesFetchErrorMessage() })
);
export const getDefaultIndex = () => { export const getDefaultIndex = () => {
const defaultIndexId = getAdvancedSettings().get('defaultIndex'); const defaultIndexId = getAdvancedSettings().get('defaultIndex');
@ -66,6 +67,10 @@ export const getDefaultIndex = () => {
? getSavedObjectsClient() ? getSavedObjectsClient()
.get<IndexPatternAttributes>('index-pattern', defaultIndexId) .get<IndexPatternAttributes>('index-pattern', defaultIndexId)
.then(defaultIndex => defaultIndex.attributes.title) .then(defaultIndex => defaultIndex.attributes.title)
.catch(err => notify.error(err, { title: strings.getDefaultIndexFetchErrorMessage() })) .catch(err =>
notifyService
.getService()
.error(err, { title: strings.getDefaultIndexFetchErrorMessage() })
)
: Promise.resolve(''); : Promise.resolve('');
}; };

View file

@ -1,52 +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 { get } from 'lodash';
import { getCoreStart, getStartPlugins } from '../legacy';
const getToastNotifications = function() {
return getCoreStart().notifications.toasts;
};
const formatMsg = function(...args) {
return getStartPlugins().__LEGACY.formatMsg(...args);
};
const getToast = (err, opts = {}) => {
const errData = get(err, 'response') || err;
const errMsg = formatMsg(errData);
const { title, ...rest } = opts;
let text = null;
if (title) {
text = errMsg;
}
return {
...rest,
title: title || errMsg,
text,
};
};
export const notify = {
/*
* @param {(string | Object)} err: message or Error object
* @param {Object} opts: option to override toast title or icon, see https://github.com/elastic/kibana/blob/master/src/legacy/ui/public/notify/toasts/TOAST_NOTIFICATIONS.md
*/
error(err, opts) {
getToastNotifications().addDanger(getToast(err, opts));
},
warning(err, opts) {
getToastNotifications().addWarning(getToast(err, opts));
},
info(err, opts) {
getToastNotifications().add(getToast(err, opts));
},
success(err, opts) {
getToastNotifications().addSuccess(getToast(err, opts));
},
};

View file

@ -6,8 +6,7 @@
import { fromExpression, getType } from '@kbn/interpreter/common'; import { fromExpression, getType } from '@kbn/interpreter/common';
import { ExpressionValue, ExpressionAstExpression } from 'src/plugins/expressions/public'; import { ExpressionValue, ExpressionAstExpression } from 'src/plugins/expressions/public';
// @ts-ignore Untyped Local import { notifyService } from '../services';
import { notify } from './notify';
import { CanvasStartDeps, CanvasSetupDeps } from '../plugin'; import { CanvasStartDeps, CanvasSetupDeps } from '../plugin';
@ -85,7 +84,7 @@ export async function runInterpreter(
throw new Error(`Ack! I don't know how to render a '${getType(renderable)}'`); throw new Error(`Ack! I don't know how to render a '${getType(renderable)}'`);
} catch (err) { } catch (err) {
notify.error(err); notifyService.getService().error(err);
throw err; throw err;
} }
} }

View file

@ -0,0 +1,73 @@
/*
* 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 { CoreSetup, CoreStart } from '../../../../../../src/core/public';
import { CanvasSetupDeps, CanvasStartDeps } from '../plugin';
import { notifyServiceFactory } from './notify';
export type CanvasServiceFactory<Service> = (
coreSetup: CoreSetup,
coreStart: CoreStart,
canvasSetupPlugins: CanvasSetupDeps,
canvasStartPlugins: CanvasStartDeps
) => Service;
class CanvasServiceProvider<Service> {
private factory: CanvasServiceFactory<Service>;
private service: Service | undefined;
constructor(factory: CanvasServiceFactory<Service>) {
this.factory = factory;
}
start(
coreSetup: CoreSetup,
coreStart: CoreStart,
canvasSetupPlugins: CanvasSetupDeps,
canvasStartPlugins: CanvasStartDeps
) {
this.service = this.factory(coreSetup, coreStart, canvasSetupPlugins, canvasStartPlugins);
}
getService(): Service {
if (!this.service) {
throw new Error('Service not ready');
}
return this.service;
}
stop() {
this.service = undefined;
}
}
export type ServiceFromProvider<P> = P extends CanvasServiceProvider<infer T> ? T : never;
export const services = {
notify: new CanvasServiceProvider(notifyServiceFactory),
};
export interface CanvasServices {
notify: ServiceFromProvider<typeof services.notify>;
}
export const startServices = (
coreSetup: CoreSetup,
coreStart: CoreStart,
canvasSetupPlugins: CanvasSetupDeps,
canvasStartPlugins: CanvasStartDeps
) => {
Object.entries(services).forEach(([key, provider]) =>
provider.start(coreSetup, coreStart, canvasSetupPlugins, canvasStartPlugins)
);
};
export const stopServices = () => {
Object.entries(services).forEach(([key, provider]) => provider.stop());
};
export const { notify: notifyService } = services;

View file

@ -0,0 +1,57 @@
/*
* 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 { get } from 'lodash';
import { CanvasServiceFactory } from '.';
import { formatMsg } from '../../../../../../src/plugins/kibana_legacy/public';
import { ToastInputFields } from '../../../../../../src/core/public';
const getToast = (err: Error | string, opts: ToastInputFields = {}) => {
const errData = (get(err, 'response') || err) as Error | string;
const errMsg = formatMsg(errData);
const { title, ...rest } = opts;
let text;
if (title) {
text = errMsg;
}
return {
...rest,
title: title || errMsg,
text,
};
};
interface NotifyService {
error: (err: string | Error, opts?: ToastInputFields) => void;
warning: (err: string | Error, opts?: ToastInputFields) => void;
info: (err: string | Error, opts?: ToastInputFields) => void;
success: (err: string | Error, opts?: ToastInputFields) => void;
}
export const notifyServiceFactory: CanvasServiceFactory<NotifyService> = (setup, start) => {
const toasts = start.notifications.toasts;
return {
/*
* @param {(string | Object)} err: message or Error object
* @param {Object} opts: option to override toast title or icon, see https://github.com/elastic/kibana/blob/master/src/legacy/ui/public/notify/toasts/TOAST_NOTIFICATIONS.md
*/
error(err, opts) {
toasts.addDanger(getToast(err, opts));
},
warning(err, opts) {
toasts.addWarning(getToast(err, opts));
},
info(err, opts) {
toasts.add(getToast(err, opts));
},
success(err, opts) {
toasts.addSuccess(getToast(err, opts));
},
};
};

View file

@ -13,9 +13,9 @@ import { getPages, getNodeById, getNodes, getSelectedPageIndex } from '../select
import { getValue as getResolvedArgsValue } from '../selectors/resolved_args'; import { getValue as getResolvedArgsValue } from '../selectors/resolved_args';
import { getDefaultElement } from '../defaults'; import { getDefaultElement } from '../defaults';
import { ErrorStrings } from '../../../i18n'; import { ErrorStrings } from '../../../i18n';
import { notify } from '../../lib/notify';
import { runInterpreter, interpretAst } from '../../lib/run_interpreter'; import { runInterpreter, interpretAst } from '../../lib/run_interpreter';
import { subMultitree } from '../../lib/aeroelastic/functional'; import { subMultitree } from '../../lib/aeroelastic/functional';
import { services } from '../../services';
import { selectToplevelNodes } from './transient'; import { selectToplevelNodes } from './transient';
import * as args from './resolved_args'; import * as args from './resolved_args';
@ -134,7 +134,7 @@ const fetchRenderableWithContextFn = ({ dispatch }, element, ast, context) => {
dispatch(getAction(renderable)); dispatch(getAction(renderable));
}) })
.catch(err => { .catch(err => {
notify.error(err); services.notify.getService().error(err);
dispatch(getAction(err)); dispatch(getAction(err));
}); });
}; };
@ -176,7 +176,7 @@ export const fetchAllRenderables = createThunk(
return runInterpreter(ast, null, { castToRender: true }) return runInterpreter(ast, null, { castToRender: true })
.then(renderable => ({ path: argumentPath, value: renderable })) .then(renderable => ({ path: argumentPath, value: renderable }))
.catch(err => { .catch(err => {
notify.error(err); services.notify.getService().error(err);
return { path: argumentPath, value: err }; return { path: argumentPath, value: err };
}); });
}); });
@ -293,7 +293,7 @@ const setAst = createThunk('setAst', ({ dispatch }, ast, element, pageId, doRend
const expression = toExpression(ast); const expression = toExpression(ast);
dispatch(setExpression(expression, element.id, pageId, doRender)); dispatch(setExpression(expression, element.id, pageId, doRender));
} catch (err) { } catch (err) {
notify.error(err); services.notify.getService().error(err);
// TODO: remove this, may have been added just to cause a re-render, but why? // TODO: remove this, may have been added just to cause a re-render, but why?
dispatch(setExpression(element.expression, element.id, pageId, doRender)); dispatch(setExpression(element.expression, element.id, pageId, doRender));

View file

@ -14,7 +14,7 @@ import { setAssets, resetAssets } from '../actions/assets';
import * as transientActions from '../actions/transient'; import * as transientActions from '../actions/transient';
import * as resolvedArgsActions from '../actions/resolved_args'; import * as resolvedArgsActions from '../actions/resolved_args';
import { update, updateAssets, updateWorkpad } from '../../lib/workpad_service'; import { update, updateAssets, updateWorkpad } from '../../lib/workpad_service';
import { notify } from '../../lib/notify'; import { services } from '../../services';
import { canUserWrite } from '../selectors/app'; import { canUserWrite } from '../selectors/app';
const { esPersist: strings } = ErrorStrings; const { esPersist: strings } = ErrorStrings;
@ -62,15 +62,15 @@ export const esPersistMiddleware = ({ getState }) => {
const statusCode = err.response && err.response.status; const statusCode = err.response && err.response.status;
switch (statusCode) { switch (statusCode) {
case 400: case 400:
return notify.error(err.response, { return services.notify.getService().error(err.response, {
title: strings.getSaveFailureTitle(), title: strings.getSaveFailureTitle(),
}); });
case 413: case 413:
return notify.error(strings.getTooLargeErrorMessage(), { return services.notify.getService().error(strings.getTooLargeErrorMessage(), {
title: strings.getSaveFailureTitle(), title: strings.getSaveFailureTitle(),
}); });
default: default:
return notify.error(err, { return services.notify.getService().error(err, {
title: strings.getUpdateFailureTitle(), title: strings.getUpdateFailureTitle(),
}); });
} }