diff --git a/x-pack/plugins/canvas/public/components/workpad/index.js b/x-pack/plugins/canvas/public/components/workpad/index.js deleted file mode 100644 index 6df639a117c4f..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad/index.js +++ /dev/null @@ -1,147 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React, { useContext, useCallback } from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import { pure, compose, withState, withProps, getContext, withHandlers } from 'recompose'; -import useObservable from 'react-use/lib/useObservable'; -import { transitionsRegistry } from '../../lib/transitions_registry'; -import { fetchAllRenderables } from '../../state/actions/elements'; -import { setZoomScale } from '../../state/actions/transient'; -import { getFullscreen, getZoomScale } from '../../state/selectors/app'; -import { - getSelectedPageIndex, - getAllElements, - getWorkpad, - getPages, -} from '../../state/selectors/workpad'; -import { zoomHandlerCreators } from '../../lib/app_handler_creators'; -import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; -import { LAUNCHED_FULLSCREEN, LAUNCHED_FULLSCREEN_AUTOPLAY } from '../../../common/lib/constants'; -import { WorkpadRoutingContext } from '../../routes/workpad'; -import { usePlatformService } from '../../services'; -import { Workpad as WorkpadComponent } from './workpad'; - -const mapStateToProps = (state) => { - const { width, height, id: workpadId, css: workpadCss } = getWorkpad(state); - return { - pages: getPages(state), - selectedPageNumber: getSelectedPageIndex(state) + 1, - totalElementCount: getAllElements(state).length, - width, - height, - workpadCss, - workpadId, - isFullscreen: getFullscreen(state), - zoomScale: getZoomScale(state), - }; -}; - -const mapDispatchToProps = { - fetchAllRenderables, - setZoomScale, -}; - -const mergeProps = (stateProps, dispatchProps, ownProps) => { - return { - ...ownProps, - ...stateProps, - ...dispatchProps, - }; -}; - -const AddContexts = (props) => { - const { isFullscreen, setFullscreen, undo, redo, autoplayInterval } = - useContext(WorkpadRoutingContext); - - const platformService = usePlatformService(); - - const hasHeaderBanner = useObservable(platformService.hasHeaderBanner$()); - - const setFullscreenWithEffect = useCallback( - (fullscreen) => { - setFullscreen(fullscreen); - if (fullscreen === true) { - trackCanvasUiMetric( - METRIC_TYPE.COUNT, - autoplayInterval > 0 - ? [LAUNCHED_FULLSCREEN, LAUNCHED_FULLSCREEN_AUTOPLAY] - : LAUNCHED_FULLSCREEN - ); - } - }, - [setFullscreen, autoplayInterval] - ); - - return ( - - ); -}; - -export const Workpad = compose( - pure, - getContext({ - router: PropTypes.object, - }), - withState('grid', 'setGrid', false), - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withState('transition', 'setTransition', null), - withState('prevSelectedPageNumber', 'setPrevSelectedPageNumber', 0), - withProps(({ selectedPageNumber, prevSelectedPageNumber, transition }) => { - function getAnimation(pageNumber) { - if (!transition || !transition.name) { - return null; - } - if (![selectedPageNumber, prevSelectedPageNumber].includes(pageNumber)) { - return null; - } - const { enter, exit } = transitionsRegistry.get(transition.name); - const laterPageNumber = Math.max(selectedPageNumber, prevSelectedPageNumber); - const name = pageNumber === laterPageNumber ? enter : exit; - const direction = prevSelectedPageNumber > selectedPageNumber ? 'reverse' : 'normal'; - return { name, direction }; - } - - return { getAnimation }; - }), - withHandlers({ - onPageChange: (props) => (pageNumber) => { - if (pageNumber === props.selectedPageNumber) { - return; - } - props.setPrevSelectedPageNumber(props.selectedPageNumber); - const transitionPage = Math.max(props.selectedPageNumber, pageNumber) - 1; - const { transition } = props.pages[transitionPage]; - if (transition) { - props.setTransition(transition); - } - props.router.navigateTo('loadWorkpad', { id: props.workpadId, page: pageNumber }); - }, - }), - withHandlers({ - onTransitionEnd: - ({ setTransition }) => - () => - setTransition(null), - nextPage: (props) => () => { - const pageNumber = Math.min(props.selectedPageNumber + 1, props.pages.length); - props.onPageChange(pageNumber); - }, - previousPage: (props) => () => { - const pageNumber = Math.max(1, props.selectedPageNumber - 1); - props.onPageChange(pageNumber); - }, - }), - withHandlers(zoomHandlerCreators) -)(AddContexts); diff --git a/x-pack/plugins/canvas/public/components/workpad/index.ts b/x-pack/plugins/canvas/public/components/workpad/index.ts new file mode 100644 index 0000000000000..83a3b36ac1c85 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { Workpad } from './workpad'; +export { Workpad as WorkpadComponent } from './workpad.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad/workpad.component.tsx b/x-pack/plugins/canvas/public/components/workpad/workpad.component.tsx new file mode 100644 index 0000000000000..a191d0c9ea361 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad/workpad.component.tsx @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import PropTypes from 'prop-types'; +import Style from 'style-it'; +import { WorkpadPage } from '../workpad_page'; +import { Fullscreen } from '../fullscreen'; +import { isTextInput } from '../../lib/is_text_input'; +import { HEADER_BANNER_HEIGHT, WORKPAD_CANVAS_BUFFER } from '../../../common/lib/constants'; +import { CommitFn, CanvasPage } from '../../../types'; +import { WorkpadShortcuts } from './workpad_shortcuts.component'; + +export interface Props { + fetchAllRenderables: () => void; + getAnimation: (pageNumber: number) => { name: string; direction: string } | null; + grid: boolean; + hasHeaderBanner?: boolean; + height: number; + isFullscreen: boolean; + nextPage: () => void; + onTransitionEnd: () => void; + pages: CanvasPage[]; + previousPage: () => void; + redoHistory: () => void; + registerLayout: (newLayout: CommitFn) => void; + resetZoom: () => void; + selectedPageNumber: number; + setFullscreen: (fullscreen: boolean) => void; + setGrid: (grid: boolean) => void; + totalElementCount: number; + width: number; + workpadCss: string; + undoHistory: () => void; + unregisterLayout: (oldLayout: CommitFn) => void; + zoomIn: () => void; + zoomOut: () => void; + zoomScale: number; +} + +export const Workpad: FC = ({ + fetchAllRenderables, + getAnimation, + grid, + hasHeaderBanner, + height, + isFullscreen, + nextPage, + onTransitionEnd, + pages, + previousPage, + redoHistory, + registerLayout, + resetZoom, + selectedPageNumber, + setFullscreen, + setGrid, + totalElementCount, + width, + workpadCss, + undoHistory, + unregisterLayout, + zoomIn, + zoomOut, + zoomScale, +}) => { + const headerBannerOffset = hasHeaderBanner ? HEADER_BANNER_HEIGHT : 0; + + const bufferStyle = { + height: isFullscreen ? height : (height + 2 * WORKPAD_CANVAS_BUFFER) * zoomScale, + width: isFullscreen ? width : (width + 2 * WORKPAD_CANVAS_BUFFER) * zoomScale, + }; + return ( + + + {!isFullscreen && ( + + )} + + + {({ isFullscreen: isFullscreenProp, windowSize }) => { + const scale = Math.min( + (windowSize.height - headerBannerOffset) / height, + windowSize.width / width + ); + + const fsStyle = isFullscreenProp + ? { + transform: `scale3d(${scale}, ${scale}, 1)`, + WebkitTransform: `scale3d(${scale}, ${scale}, 1)`, + msTransform: `scale3d(${scale}, ${scale}, 1)`, + height: windowSize.height < height ? 'auto' : height, + width: windowSize.width < width ? 'auto' : width, + top: hasHeaderBanner ? `${headerBannerOffset / 2}px` : undefined, + } + : {}; + + // NOTE: the data-shared-* attributes here are used for reporting + return Style.it( + workpadCss, + + {pages.map((page, i) => ( + + ))} + + + ); + }} + + + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/workpad/workpad.js b/x-pack/plugins/canvas/public/components/workpad/workpad.js deleted file mode 100644 index 9e1c0588e447b..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad/workpad.js +++ /dev/null @@ -1,160 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { Shortcuts } from 'react-shortcuts'; -import Style from 'style-it'; -import { WorkpadPage } from '../workpad_page'; -import { Fullscreen } from '../fullscreen'; -import { isTextInput } from '../../lib/is_text_input'; -import { HEADER_BANNER_HEIGHT, WORKPAD_CANVAS_BUFFER } from '../../../common/lib/constants'; - -export class Workpad extends React.PureComponent { - static propTypes = { - selectedPageNumber: PropTypes.number.isRequired, - getAnimation: PropTypes.func.isRequired, - onTransitionEnd: PropTypes.func.isRequired, - grid: PropTypes.bool.isRequired, - setGrid: PropTypes.func.isRequired, - pages: PropTypes.array.isRequired, - totalElementCount: PropTypes.number.isRequired, - isFullscreen: PropTypes.bool.isRequired, - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - workpadCss: PropTypes.string.isRequired, - undoHistory: PropTypes.func.isRequired, - redoHistory: PropTypes.func.isRequired, - nextPage: PropTypes.func.isRequired, - previousPage: PropTypes.func.isRequired, - fetchAllRenderables: PropTypes.func.isRequired, - registerLayout: PropTypes.func.isRequired, - unregisterLayout: PropTypes.func.isRequired, - zoomIn: PropTypes.func.isRequired, - zoomOut: PropTypes.func.isRequired, - resetZoom: PropTypes.func.isRequired, - hasHeaderBanner: PropTypes.bool, - }; - - _toggleFullscreen = () => { - const { setFullscreen, isFullscreen } = this.props; - setFullscreen(!isFullscreen); - }; - - // handle keypress events for editor events - _keyMap = { - REFRESH: this.props.fetchAllRenderables, - UNDO: this.props.undoHistory, - REDO: this.props.redoHistory, - GRID: () => this.props.setGrid(!this.props.grid), - ZOOM_IN: this.props.zoomIn, - ZOOM_OUT: this.props.zoomOut, - ZOOM_RESET: this.props.resetZoom, - PREV: this.props.previousPage, - NEXT: this.props.nextPage, - FULLSCREEN: this._toggleFullscreen, - }; - - _keyHandler = (action, event) => { - if (!isTextInput(event.target) && typeof this._keyMap[action] === 'function') { - event.preventDefault(); - this._keyMap[action](); - } - }; - - render() { - const { - selectedPageNumber, - getAnimation, - onTransitionEnd, - pages, - totalElementCount, - width, - height, - workpadCss, - grid, - isFullscreen, - registerLayout, - unregisterLayout, - zoomScale, - hasHeaderBanner = false, - } = this.props; - - const bufferStyle = { - height: isFullscreen ? height : (height + 2 * WORKPAD_CANVAS_BUFFER) * zoomScale, - width: isFullscreen ? width : (width + 2 * WORKPAD_CANVAS_BUFFER) * zoomScale, - }; - - const headerBannerOffset = hasHeaderBanner ? HEADER_BANNER_HEIGHT : 0; - - return ( - - - {!isFullscreen && ( - - )} - - - {({ isFullscreen, windowSize }) => { - const scale = Math.min( - (windowSize.height - headerBannerOffset) / height, - windowSize.width / width - ); - - const fsStyle = isFullscreen - ? { - transform: `scale3d(${scale}, ${scale}, 1)`, - WebkitTransform: `scale3d(${scale}, ${scale}, 1)`, - msTransform: `scale3d(${scale}, ${scale}, 1)`, - height: windowSize.height < height ? 'auto' : height, - width: windowSize.width < width ? 'auto' : width, - top: hasHeaderBanner ? `${headerBannerOffset / 2}px` : undefined, - } - : {}; - - // NOTE: the data-shared-* attributes here are used for reporting - return Style.it( - workpadCss, - - {pages.map((page, i) => ( - - ))} - - - ); - }} - - - - ); - } -} diff --git a/x-pack/plugins/canvas/public/components/workpad/workpad.tsx b/x-pack/plugins/canvas/public/components/workpad/workpad.tsx new file mode 100644 index 0000000000000..9165f7f8de84d --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad/workpad.tsx @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { FC, useContext, useCallback, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import useObservable from 'react-use/lib/useObservable'; +// @ts-expect-error +import { transitionsRegistry } from '../../lib/transitions_registry'; +// @ts-expect-error +import { fetchAllRenderables as fetchAllRenderablesAction } from '../../state/actions/elements'; +// @ts-expect-error +import { setZoomScale as setZoomScaleAction } from '../../state/actions/transient'; +import { getFullscreen, getZoomScale } from '../../state/selectors/app'; +import { + getSelectedPageIndex, + getAllElements, + getWorkpad, + getPages, +} from '../../state/selectors/workpad'; +import { useZoomHandlers } from '../../lib/app_handler_creators'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; +import { LAUNCHED_FULLSCREEN, LAUNCHED_FULLSCREEN_AUTOPLAY } from '../../../common/lib/constants'; +import { WorkpadRoutingContext } from '../../routes/workpad'; +import { usePlatformService } from '../../services'; +import { Workpad as WorkpadComponent, Props } from './workpad.component'; +import { State } from '../../../types'; + +type ContainerProps = Pick; + +export const Workpad: FC = (props) => { + const dispatch = useDispatch(); + const [grid, setGrid] = useState(false); + const [transition, setTransition] = useState(null); + const [prevSelectedPageNumber, setPrevSelectedPageNumber] = useState(0); + + const { + isFullscreen, + setFullscreen, + undo, + redo, + autoplayInterval, + nextPage, + previousPage, + } = useContext(WorkpadRoutingContext); + + const platformService = usePlatformService(); + + const hasHeaderBanner = useObservable(platformService.hasHeaderBanner$()); + + const propsFromState = useSelector((state: State) => { + const { width, height, id: workpadId, css: workpadCss } = getWorkpad(state); + return { + pages: getPages(state), + selectedPageNumber: getSelectedPageIndex(state) + 1, + totalElementCount: getAllElements(state).length, + width, + height, + workpadCss, + workpadId, + isFullscreen: getFullscreen(state), + zoomScale: getZoomScale(state), + }; + }); + + const fetchAllRenderables = useCallback(() => { + dispatch(fetchAllRenderablesAction()); + }, [dispatch]); + + const setZoomScale = useCallback( + (scale: number) => { + dispatch(setZoomScaleAction(scale)); + }, + [dispatch] + ); + + const getAnimation = useCallback( + (pageNumber) => { + if (!transition || !transition.name) { + return null; + } + if (![propsFromState.selectedPageNumber, prevSelectedPageNumber].includes(pageNumber)) { + return null; + } + const { enter, exit } = transitionsRegistry.get(transition.name); + const laterPageNumber = Math.max(propsFromState.selectedPageNumber, prevSelectedPageNumber); + const name = pageNumber === laterPageNumber ? enter : exit; + const direction = + prevSelectedPageNumber > propsFromState.selectedPageNumber ? 'reverse' : 'normal'; + return { name, direction }; + }, + [propsFromState.selectedPageNumber, transition, prevSelectedPageNumber] + ); + + const onTransitionEnd = useCallback(() => setTransition(null), [setTransition]); + + const setFullscreenWithEffect = useCallback( + (fullscreen) => { + setFullscreen(fullscreen); + if (fullscreen === true) { + trackCanvasUiMetric( + METRIC_TYPE.COUNT, + autoplayInterval > 0 + ? [LAUNCHED_FULLSCREEN, LAUNCHED_FULLSCREEN_AUTOPLAY] + : LAUNCHED_FULLSCREEN + ); + } + }, + [setFullscreen, autoplayInterval] + ); + + const zoomHandlers = useZoomHandlers({ setZoomScale, zoomScale: propsFromState.zoomScale }); + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/workpad/workpad_shortcuts.component.tsx b/x-pack/plugins/canvas/public/components/workpad/workpad_shortcuts.component.tsx new file mode 100644 index 0000000000000..47453752f4098 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad/workpad_shortcuts.component.tsx @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { Shortcuts } from 'react-shortcuts'; +import { isTextInput } from '../../lib/is_text_input'; +import { Props } from './workpad.component'; + +type ShortcutProps = Pick< + Props, + | 'fetchAllRenderables' + | 'undoHistory' + | 'redoHistory' + | 'setGrid' + | 'grid' + | 'zoomIn' + | 'zoomOut' + | 'resetZoom' + | 'previousPage' + | 'nextPage' + | 'setFullscreen' + | 'isFullscreen' +>; + +type ShortcutFn = () => void; + +interface Shortcuts { + REFRESH: ShortcutFn; + UNDO: ShortcutFn; + REDO: ShortcutFn; + GRID: ShortcutFn; + ZOOM_IN: ShortcutFn; + ZOOM_OUT: ShortcutFn; + ZOOM_RESET: ShortcutFn; + PREV: ShortcutFn; + NEXT: ShortcutFn; + FULLSCREEN: ShortcutFn; +} + +export class WorkpadShortcuts extends React.Component { + _toggleFullscreen = () => { + const { setFullscreen, isFullscreen } = this.props; + setFullscreen(!isFullscreen); + }; + + nextPage = () => { + this.props.nextPage(); + }; + + previousPage = () => { + this.props.previousPage(); + }; + + zoomIn = () => { + this.props.zoomIn(); + }; + + zoomOut = () => { + this.props.zoomOut(); + }; + + resetZoom = () => { + this.props.resetZoom(); + }; + + // handle keypress events for editor events + _keyMap: Shortcuts = { + REFRESH: this.props.fetchAllRenderables, + UNDO: this.props.undoHistory, + REDO: this.props.redoHistory, + GRID: () => this.props.setGrid(!this.props.grid), + ZOOM_IN: this.zoomIn, + ZOOM_OUT: this.zoomOut, + ZOOM_RESET: this.resetZoom, + PREV: this.previousPage, + NEXT: this.nextPage, + FULLSCREEN: this._toggleFullscreen, + }; + + _keyHandler = (action: keyof Shortcuts, event: KeyboardEvent) => { + if ( + !isTextInput(event.target as HTMLInputElement) && + typeof this._keyMap[action] === 'function' + ) { + event.preventDefault(); + this._keyMap[action](); + } + }; + + render() { + return ; + } +} diff --git a/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.component.tsx b/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.component.tsx index 5074eadf306e1..4ae6abe047455 100644 --- a/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.component.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, MouseEventHandler, useRef } from 'react'; +import React, { FC, MouseEventHandler, useRef, useCallback } from 'react'; import PropTypes from 'prop-types'; import { Sidebar } from '../../components/sidebar'; import { Toolbar } from '../../components/toolbar'; @@ -25,17 +25,17 @@ interface Props { export const WorkpadApp: FC = ({ deselectElement, isWriteable }) => { const interactivePageLayout = useRef(null); // future versions may enable editing on multiple pages => use array then - const registerLayout = (newLayout: CommitFn) => { + const registerLayout = useCallback((newLayout: CommitFn) => { if (interactivePageLayout.current !== newLayout) { interactivePageLayout.current = newLayout; } - }; + }, []); - const unregisterLayout = (oldLayout: CommitFn) => { + const unregisterLayout = useCallback((oldLayout: CommitFn) => { if (interactivePageLayout.current === oldLayout) { interactivePageLayout.current = null; } - }; + }, []); const commit = interactivePageLayout.current || (() => {}); diff --git a/x-pack/plugins/canvas/public/lib/app_handler_creators.ts b/x-pack/plugins/canvas/public/lib/app_handler_creators.ts index f678dfeb0b2b4..bab86136dc28f 100644 --- a/x-pack/plugins/canvas/public/lib/app_handler_creators.ts +++ b/x-pack/plugins/canvas/public/lib/app_handler_creators.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { useCallback } from 'react'; import { ZOOM_LEVELS, MAX_ZOOM_LEVEL, MIN_ZOOM_LEVEL } from '../../common/lib/constants'; export interface Props { @@ -12,10 +13,6 @@ export interface Props { * current zoom level of the workpad */ zoomScale: number; - /** - * zoom level to scale workpad to fit into the viewport - */ - fitZoomScale: number; /** * sets the new zoom level */ @@ -46,3 +43,25 @@ export const zoomHandlerCreators = { setZoomScale(1); }, }; + +export const useZoomHandlers = ({ zoomScale, setZoomScale }: Props) => { + const zoomIn = useCallback(() => { + const scaleUp = + ZOOM_LEVELS.find((zoomLevel: number) => zoomScale < zoomLevel) || MAX_ZOOM_LEVEL; + setZoomScale(scaleUp); + }, [zoomScale, setZoomScale]); + + const zoomOut = useCallback(() => { + const scaleDown = + ZOOM_LEVELS.slice() + .reverse() + .find((zoomLevel: number) => zoomScale > zoomLevel) || MIN_ZOOM_LEVEL; + setZoomScale(scaleDown); + }, [zoomScale, setZoomScale]); + + const resetZoom = useCallback(() => { + setZoomScale(1); + }, [setZoomScale]); + + return { zoomIn, zoomOut, resetZoom }; +};