diff --git a/public/components/element_content/element_content.js b/public/components/element_content/element_content.js index dbecd847d971c..788b5b0bb10cb 100644 --- a/public/components/element_content/element_content.js +++ b/public/components/element_content/element_content.js @@ -1,10 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { pure, compose, branch, renderComponent, withProps } from 'recompose'; +import { pure, compose, branch, renderComponent } from 'recompose'; import Style from 'style-it'; +import { getType } from '../../../common/lib/get_type'; import { Loading } from '../loading'; import { RenderWithFn } from '../render_with_fn'; -import { getType } from '../../../common/lib/get_type'; +import { ElementShareContainer } from '../element_share_container'; import { InvalidExpression } from './invalid_expression'; import { InvalidElementType } from './invalid_element_type'; @@ -31,21 +32,20 @@ const branches = [ !renderFunction // We can't find an element in the registry for this ); }, renderComponent(InvalidExpression)), - withProps(({ handlers }) => ({ - handlers: { - done() {}, - ...handlers, - }, - })), ]; -// NOTE: the data-shared-* attributes here are used for reporting export const ElementContent = compose(pure, ...branches)( ({ renderable, renderFunction, size, handlers }) => { + const { getFilter, setFilter, done, onComplete } = handlers; + return Style.it( renderable.css,
-
+ -
+
); } ); ElementContent.propTypes = { - renderable: PropTypes.object, - renderFunction: PropTypes.object, + renderable: PropTypes.shape({ + css: PropTypes.string, + value: PropTypes.object, + }), + renderFunction: PropTypes.shape({ + name: PropTypes.string, + render: PropTypes.func, + reuseDomNode: PropTypes.bool, + }), size: PropTypes.object, - handlers: PropTypes.object, + handlers: PropTypes.shape({ + setFilter: PropTypes.func.isRequired, + getFilter: PropTypes.func.isRequired, + done: PropTypes.func.isRequired, + onComplete: PropTypes.func.isRequired, // local, not passed through + }).isRequired, state: PropTypes.string, }; diff --git a/public/components/element_content/index.js b/public/components/element_content/index.js index 6c2b013aebc3e..d881dfb8e91e1 100644 --- a/public/components/element_content/index.js +++ b/public/components/element_content/index.js @@ -11,8 +11,7 @@ export const ElementContent = compose( )(Component); ElementContent.propTypes = { - renderable: PropTypes.object, - size: PropTypes.object, - handlers: PropTypes.object, - state: PropTypes.string, + renderable: PropTypes.shape({ + as: PropTypes.string, + }), }; diff --git a/public/components/element_share_container/element_share_container.js b/public/components/element_share_container/element_share_container.js new file mode 100644 index 0000000000000..0ec8352279844 --- /dev/null +++ b/public/components/element_share_container/element_share_container.js @@ -0,0 +1,59 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export class ElementShareContainer extends React.PureComponent { + static propTypes = { + functionName: PropTypes.string.isRequired, + onComplete: PropTypes.func.isRequired, + className: PropTypes.string, + children: PropTypes.node.isRequired, + }; + + state = { + renderComplete: false, + }; + + componentDidMount() { + const { functionName, onComplete } = this.props; + const isDevelopment = process.env.NODE_ENV !== 'production'; + let t; + + // check that the done event is called within a certain time + if (isDevelopment) { + const timeout = 15000; // 15 seconds + t = setTimeout(() => { + // TODO: show this message in a proper notification + console.warn(`done handler never called in render function: ${functionName}`); + }, timeout); + } + + // dispatches a custom DOM event on the container when the element is complete + onComplete(() => { + clearTimeout(t); + const ev = new Event('renderComplete'); + this.sharedItemRef.dispatchEvent(ev); + + // if the element is finished before reporting is listening for then + // renderComplete event, the report never completes. to get around that + // issue, track the completed state locally and set the + // [data-render-complete] value accordingly. + // this is similar to renderComplete directive in Kibana, + // see: src/ui/public/render_complete/directive.js + this.setState({ renderComplete: true }); + }); + } + + render() { + // NOTE: the data-shared-item and data-render-complete attributes are used for reporting + return ( +
(this.sharedItemRef = ref)} + > + {this.props.children} +
+ ); + } +} diff --git a/public/components/element_share_container/index.js b/public/components/element_share_container/index.js new file mode 100644 index 0000000000000..6196708b280c3 --- /dev/null +++ b/public/components/element_share_container/index.js @@ -0,0 +1 @@ +export { ElementShareContainer } from './element_share_container'; diff --git a/public/components/element_wrapper/index.js b/public/components/element_wrapper/index.js index 475c25d7516ae..84373c6c18b5e 100644 --- a/public/components/element_wrapper/index.js +++ b/public/components/element_wrapper/index.js @@ -47,5 +47,7 @@ const mergeProps = (stateProps, dispatchProps, { element }) => { export const ElementWrapper = connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component); ElementWrapper.propTypes = { - element: PropTypes.object, + element: PropTypes.shape({ + id: PropTypes.string.isRequired, + }).isRequired, }; diff --git a/public/components/element_wrapper/lib/handlers.js b/public/components/element_wrapper/lib/handlers.js index c386729ddbe0c..1fa5816adf2a4 100644 --- a/public/components/element_wrapper/lib/handlers.js +++ b/public/components/element_wrapper/lib/handlers.js @@ -1,6 +1,9 @@ import { setFilter } from '../../../state/actions/elements'; export function createHandlers(element, pageId, dispatch) { + let isComplete = false; + let completeFn = () => {}; + return { setFilter(text) { dispatch(setFilter(text, element.id, pageId, true)); @@ -9,5 +12,15 @@ export function createHandlers(element, pageId, dispatch) { getFilter() { return element.filter; }, + + onComplete(fn) { + completeFn = fn; + }, + + done() { + if (isComplete) return; // don't emit if the element is already done + isComplete = true; + completeFn(); + }, }; } diff --git a/public/components/render_with_fn/index.js b/public/components/render_with_fn/index.js index ce06e6580046d..92c9d58251838 100644 --- a/public/components/render_with_fn/index.js +++ b/public/components/render_with_fn/index.js @@ -12,11 +12,12 @@ export const RenderWithFn = compose( }) ), withProps(({ handlers, elementHandlers }) => ({ - handlers: Object.assign(elementHandlers, handlers, { done: () => {} }), + handlers: Object.assign(elementHandlers, handlers), onError: message => notify.error(message), })) )(Component); RenderWithFn.propTypes = { handlers: PropTypes.object, + elementHandlers: PropTypes.object, }; diff --git a/public/components/render_with_fn/lib/handlers.js b/public/components/render_with_fn/lib/handlers.js index 259857d9fc29d..40aa92fa862f8 100644 --- a/public/components/render_with_fn/lib/handlers.js +++ b/public/components/render_with_fn/lib/handlers.js @@ -1,8 +1,7 @@ export class ElementHandlers { - constructor() { - this.resize = () => {}; - this.destroy = () => {}; - } + resize() {} + + destroy() {} onResize(fn) { this.resize = fn; diff --git a/public/components/render_with_fn/render_with_fn.js b/public/components/render_with_fn/render_with_fn.js index 38e92a4e09a2b..1fa2f067f309f 100644 --- a/public/components/render_with_fn/render_with_fn.js +++ b/public/components/render_with_fn/render_with_fn.js @@ -6,16 +6,29 @@ import { RenderToDom } from '../render_to_dom'; export class RenderWithFn extends React.Component { static propTypes = { - name: PropTypes.string, + name: PropTypes.string.isRequired, renderFn: PropTypes.func.isRequired, reuseNode: PropTypes.bool, - handlers: PropTypes.object, - destroyFn: PropTypes.func, - config: PropTypes.object, - size: PropTypes.object, + handlers: PropTypes.shape({ + // element handlers, see components/element_wrapper/lib/handlers.js + setFilter: PropTypes.func.isRequired, + getFilter: PropTypes.func.isRequired, + done: PropTypes.func.isRequired, + // render handlers, see lib/handlers.js + resize: PropTypes.func.isRequired, + onResize: PropTypes.func.isRequired, + destroy: PropTypes.func.isRequired, + onDestroy: PropTypes.func.isRequired, + }), + config: PropTypes.object.isRequired, + size: PropTypes.object.isRequired, onError: PropTypes.func.isRequired, }; + static defaultProps = { + reuseNode: false, + }; + static domNode = null; componentDidMount() { @@ -109,7 +122,6 @@ export class RenderWithFn extends React.Component { return (
({ ReactDOM.render(
, domNode, () => handlers.done()); handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode)); - - handlers.done(); }, });