From 302a5d8d69f800618c1b79a6ac0e7a26e33b0c09 Mon Sep 17 00:00:00 2001 From: Joe Fleming Date: Tue, 21 May 2019 10:13:01 -0700 Subject: [PATCH] Feat: Autoplay pages in fullscreen (#35981) * feat: add autoplay redux boilerplate WIP auto-play settings * feat: add page cycle settings * feat: add cycle toggle hotkey * chore: add tooltip text to settings icon * settings layout * fix: handle invalid input for custom interval * chore: address nit --- .../control_settings/auto_refresh_controls.js | 100 +++++++----------- .../control_settings/control_settings.js | 68 +++++------- .../control_settings/control_settings.scss | 2 +- .../control_settings/custom_interval.js | 90 ++++++++++++++++ .../workpad_header/control_settings/index.js | 22 +++- .../control_settings/kiosk_controls.js | 95 +++++++++++++++++ .../fullscreen_control/fullscreen_control.js | 4 + .../fullscreen_control/index.js | 4 + x-pack/plugins/canvas/public/lib/keymap.js | 2 + .../canvas/public/state/actions/workpad.js | 2 + .../canvas/public/state/initial_state.js | 4 + .../canvas/public/state/middleware/index.js | 4 +- .../state/middleware/workpad_autoplay.js | 79 ++++++++++++++ .../canvas/public/state/reducers/transient.js | 10 +- .../canvas/public/state/selectors/workpad.js | 4 + 15 files changed, 384 insertions(+), 106 deletions(-) create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.js create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/control_settings/kiosk_controls.js create mode 100644 x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.js diff --git a/x-pack/plugins/canvas/public/components/workpad_header/control_settings/auto_refresh_controls.js b/x-pack/plugins/canvas/public/components/workpad_header/control_settings/auto_refresh_controls.js index 68d8143230d6e..f8a7c0edeb4c1 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/control_settings/auto_refresh_controls.js +++ b/x-pack/plugins/canvas/public/components/workpad_header/control_settings/auto_refresh_controls.js @@ -4,16 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment, Component } from 'react'; +import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexGrid, EuiFlexItem, - EuiFormRow, - EuiButton, EuiLink, - EuiFieldText, EuiSpacer, EuiHorizontalRule, EuiDescriptionList, @@ -21,32 +18,25 @@ import { EuiDescriptionListDescription, EuiFormLabel, EuiText, + EuiButtonIcon, + EuiToolTip, } from '@elastic/eui'; import { timeDurationString } from '../../../lib/time_duration'; import { RefreshControl } from '../refresh_control'; +import { CustomInterval } from './custom_interval'; const ListGroup = ({ children }) => ; -export class AutoRefreshControls extends Component { - static propTypes = { - refreshInterval: PropTypes.number, - setRefresh: PropTypes.func.isRequired, - disableInterval: PropTypes.func.isRequired, - }; +export const AutoRefreshControls = ({ refreshInterval, setRefresh, disableInterval }) => { + const RefreshItem = ({ duration, label }) => ( +
  • + setRefresh(duration)}>{label} +
  • + ); - refreshInput = null; - - render() { - const { refreshInterval, setRefresh, disableInterval } = this.props; - - const RefreshItem = ({ duration, label }) => ( -
  • - setRefresh(duration)}>{label} -
  • - ); - - return ( -
    + return ( + + @@ -55,11 +45,6 @@ export class AutoRefreshControls extends Component { {refreshInterval > 0 ? ( Every {timeDurationString(refreshInterval)} -
    - - Disable auto-refresh - -
    ) : ( Manually @@ -68,7 +53,22 @@ export class AutoRefreshControls extends Component {
    - + + {refreshInterval > 0 ? ( + + + + + + ) : null} + + + +
    @@ -100,35 +100,17 @@ export class AutoRefreshControls extends Component {
    + - + + setRefresh(value)} /> + +
    + ); +}; -
    { - ev.preventDefault(); - setRefresh(this.refreshInput.value); - }} - > - - - - (this.refreshInput = i)} /> - - - - - - Set - - - - -
    -
    - ); - } -} +AutoRefreshControls.propTypes = { + refreshInterval: PropTypes.number, + setRefresh: PropTypes.func.isRequired, + disableInterval: PropTypes.func.isRequired, +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/control_settings/control_settings.js b/x-pack/plugins/canvas/public/components/workpad_header/control_settings/control_settings.js index 8801c198ffbbb..4e73fca88fec4 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/control_settings/control_settings.js +++ b/x-pack/plugins/canvas/public/components/workpad_header/control_settings/control_settings.js @@ -6,45 +6,29 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { Popover } from '../../popover'; import { AutoRefreshControls } from './auto_refresh_controls'; +import { KioskControls } from './kiosk_controls'; -const getRefreshInterval = (val = '') => { - // if it's a number, just use it directly - if (!isNaN(Number(val))) { - return val; - } - - // if it's a string, try to parse out the shorthand duration value - const match = String(val).match(/^([0-9]{1,})([hmsd])$/); - - // TODO: do something better with improper input, like show an error... - if (!match) { - return; - } - - switch (match[2]) { - case 's': - return match[1] * 1000; - case 'm': - return match[1] * 1000 * 60; - case 'h': - return match[1] * 1000 * 60 * 60; - case 'd': - return match[1] * 1000 * 60 * 60 * 24; - } -}; - -export const ControlSettings = ({ setRefreshInterval, refreshInterval }) => { - const setRefresh = val => setRefreshInterval(getRefreshInterval(val)); +export const ControlSettings = ({ + setRefreshInterval, + refreshInterval, + autoplayEnabled, + autoplayInterval, + enableAutoplay, + setAutoplayInterval, +}) => { + const setRefresh = val => setRefreshInterval(val); const disableInterval = () => { setRefresh(0); }; const popoverButton = handleClick => ( - + + + ); return ( @@ -54,19 +38,21 @@ export const ControlSettings = ({ setRefreshInterval, refreshInterval }) => { anchorPosition="rightUp" panelClassName="canvasControlSettings__popover" > - {({ closePopover }) => ( + {() => ( { - setRefresh(val); - closePopover(); - }} - disableInterval={() => { - disableInterval(); - closePopover(); - }} + setRefresh={val => setRefresh(val)} + disableInterval={() => disableInterval()} + /> + + + @@ -78,4 +64,8 @@ export const ControlSettings = ({ setRefreshInterval, refreshInterval }) => { ControlSettings.propTypes = { refreshInterval: PropTypes.number, setRefreshInterval: PropTypes.func.isRequired, + autoplayEnabled: PropTypes.bool, + autoplayInterval: PropTypes.number, + enableAutoplay: PropTypes.func.isRequired, + setAutoplayInterval: PropTypes.func.isRequired, }; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/control_settings/control_settings.scss b/x-pack/plugins/canvas/public/components/workpad_header/control_settings/control_settings.scss index c481c59dac160..beaead8b99fc3 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/control_settings/control_settings.scss +++ b/x-pack/plugins/canvas/public/components/workpad_header/control_settings/control_settings.scss @@ -1,3 +1,3 @@ .canvasControlSettings__popover { - width: 300px; + width: 600px; } diff --git a/x-pack/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.js b/x-pack/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.js new file mode 100644 index 0000000000000..2e2bf0722f5a1 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/control_settings/custom_interval.js @@ -0,0 +1,90 @@ +/* + * 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 React, { useState } from 'react'; +import PropTypes from 'prop-types'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiButton, EuiFieldText } from '@elastic/eui'; + +const getRefreshInterval = (val = '') => { + // if it's a number, there is no interval, return undefined + if (!isNaN(Number(val))) { + return; + } + + // if it's a string, try to parse out the shorthand duration value + const match = String(val).match(/^([0-9]{1,})([hmsd])$/); + + // if it's invalid, there is no interval, return undefined + if (!match) { + return; + } + + switch (match[2]) { + case 's': + return match[1] * 1000; + case 'm': + return match[1] * 1000 * 60; + case 'h': + return match[1] * 1000 * 60 * 60; + case 'd': + return match[1] * 1000 * 60 * 60 * 24; + } +}; + +export const CustomInterval = ({ gutterSize, buttonSize, onSubmit, defaultValue }) => { + const [customInterval, setCustomInterval] = useState(defaultValue); + const refreshInterval = getRefreshInterval(customInterval); + const isInvalid = Boolean(customInterval.length && !refreshInterval); + + const handleChange = ev => setCustomInterval(ev.target.value); + + return ( +
    { + ev.preventDefault(); + onSubmit(refreshInterval); + }} + > + + + + + + + + + + + Set + + + + +
    + ); +}; + +CustomInterval.propTypes = { + buttonSize: PropTypes.string, + gutterSize: PropTypes.string, + defaultValue: PropTypes.string, + onSubmit: PropTypes.func.isRequired, +}; + +CustomInterval.defaultProps = { + buttonSize: 's', + gutterSize: 's', + defaultValue: '', +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/control_settings/index.js b/x-pack/plugins/canvas/public/components/workpad_header/control_settings/index.js index 90e127582fecc..ef8fc10a24431 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/control_settings/index.js +++ b/x-pack/plugins/canvas/public/components/workpad_header/control_settings/index.js @@ -5,16 +5,28 @@ */ import { connect } from 'react-redux'; -import { setRefreshInterval } from '../../../state/actions/workpad'; -import { getRefreshInterval } from '../../../state/selectors/workpad'; +import { + setRefreshInterval, + enableAutoplay, + setAutoplayInterval, +} from '../../../state/actions/workpad'; +import { getRefreshInterval, getAutoplay } from '../../../state/selectors/workpad'; import { ControlSettings as Component } from './control_settings'; -const mapStateToProps = state => ({ - refreshInterval: getRefreshInterval(state), -}); +const mapStateToProps = state => { + const { enabled, interval } = getAutoplay(state); + + return { + refreshInterval: getRefreshInterval(state), + autoplayEnabled: enabled, + autoplayInterval: interval, + }; +}; const mapDispatchToProps = { setRefreshInterval, + enableAutoplay, + setAutoplayInterval, }; export const ControlSettings = connect( diff --git a/x-pack/plugins/canvas/public/components/workpad_header/control_settings/kiosk_controls.js b/x-pack/plugins/canvas/public/components/workpad_header/control_settings/kiosk_controls.js new file mode 100644 index 0000000000000..27a365ec2572a --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/control_settings/kiosk_controls.js @@ -0,0 +1,95 @@ +/* + * 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 React from 'react'; +import PropTypes from 'prop-types'; +import { + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFormLabel, + EuiHorizontalRule, + EuiLink, + EuiSpacer, + EuiSwitch, + EuiText, + EuiFlexGrid, + EuiFlexItem, + EuiFlexGroup, +} from '@elastic/eui'; +import { timeDurationString } from '../../../lib/time_duration'; +import { CustomInterval } from './custom_interval'; + +const ListGroup = ({ children }) => ; + +export const KioskControls = ({ + autoplayEnabled, + autoplayInterval, + onSetEnabled, + onSetInterval, +}) => { + const RefreshItem = ({ duration, label }) => ( +
  • + onSetInterval(duration)}>{label} +
  • + ); + + return ( + + + + Cycle fullscreen pages + + Every {timeDurationString(autoplayInterval)} + + + + +
    + onSetEnabled(ev.target.checked)} + /> + +
    + + Change cycling interval + + + + + + + + + + + + + + + + + + + + +
    + + + onSetInterval(value)} /> + +
    + ); +}; + +KioskControls.propTypes = { + autoplayEnabled: PropTypes.bool.isRequired, + autoplayInterval: PropTypes.number.isRequired, + onSetEnabled: PropTypes.func.isRequired, + onSetInterval: PropTypes.func.isRequired, +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/fullscreen_control.js b/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/fullscreen_control.js index 4e2bc84deef7d..a80f120874151 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/fullscreen_control.js +++ b/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/fullscreen_control.js @@ -16,6 +16,10 @@ export class FullscreenControl extends React.PureComponent { if (enterFullscreen || exitFullscreen) { this.toggleFullscreen(); } + + if (action === 'PAGE_CYCLE_TOGGLE') { + this.props.enableAutoplay(!this.props.autoplayEnabled); + } }; toggleFullscreen = () => { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/index.js b/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/index.js index 31b0e97eee438..088eaafd3310f 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/index.js +++ b/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/index.js @@ -6,11 +6,14 @@ import { connect } from 'react-redux'; import { setFullscreen, selectToplevelNodes } from '../../../state/actions/transient'; +import { enableAutoplay } from '../../../state/actions/workpad'; import { getFullscreen } from '../../../state/selectors/app'; +import { getAutoplay } from '../../../state/selectors/workpad'; import { FullscreenControl as Component } from './fullscreen_control'; const mapStateToProps = state => ({ isFullscreen: getFullscreen(state), + autoplayEnabled: getAutoplay(state).enabled, }); const mapDispatchToProps = dispatch => ({ @@ -18,6 +21,7 @@ const mapDispatchToProps = dispatch => ({ dispatch(setFullscreen(value)); value && dispatch(selectToplevelNodes([])); }, + enableAutoplay: enabled => dispatch(enableAutoplay(enabled)), }); export const FullscreenControl = connect( diff --git a/x-pack/plugins/canvas/public/lib/keymap.js b/x-pack/plugins/canvas/public/lib/keymap.js index 80ea05de7269f..6b3e59428cf3e 100644 --- a/x-pack/plugins/canvas/public/lib/keymap.js +++ b/x-pack/plugins/canvas/public/lib/keymap.js @@ -53,6 +53,7 @@ const deleteElementShortcuts = ['del', 'backspace']; const groupShortcut = ['g']; const ungroupShortcut = ['u']; const fullscreentExitShortcut = ['esc']; +const fullscreenPageCycle = ['p']; export const keymap = { ELEMENT: { @@ -120,6 +121,7 @@ export const keymap = { key === 'help' ? osShortcuts : osShortcuts.concat(['space', 'right']) ), REFRESH: refreshShortcut, + PAGE_CYCLE_TOGGLE: { ...getShortcuts(fullscreenPageCycle), help: 'Toggle page cycling' }, }, EXPRESSION: { displayName: 'Expression controls', diff --git a/x-pack/plugins/canvas/public/state/actions/workpad.js b/x-pack/plugins/canvas/public/state/actions/workpad.js index af9206018c297..a207488495740 100644 --- a/x-pack/plugins/canvas/public/state/actions/workpad.js +++ b/x-pack/plugins/canvas/public/state/actions/workpad.js @@ -16,6 +16,8 @@ export const setWriteable = createAction('setWriteable'); export const setColors = createAction('setColors'); export const setRefreshInterval = createAction('setRefreshInterval'); export const setWorkpadCSS = createAction('setWorkpadCSS'); +export const enableAutoplay = createAction('enableAutoplay'); +export const setAutoplayInterval = createAction('setAutoplayInterval'); export const initializeWorkpad = createThunk('initializeWorkpad', ({ dispatch }) => { dispatch(fetchAllRenderables()); diff --git a/x-pack/plugins/canvas/public/state/initial_state.js b/x-pack/plugins/canvas/public/state/initial_state.js index 0c03762cc56d3..2ba4cd1c37f36 100644 --- a/x-pack/plugins/canvas/public/state/initial_state.js +++ b/x-pack/plugins/canvas/public/state/initial_state.js @@ -26,6 +26,10 @@ export const getInitialState = path => { refresh: { interval: 0, }, + autoplay: { + enabled: false, + interval: 10000, + }, // values in resolvedArgs should live under a unique index so they can be looked up. // The ID of the element is a great example. // In there will live an object with a status (string), value (any), and error (Error) property. diff --git a/x-pack/plugins/canvas/public/state/middleware/index.js b/x-pack/plugins/canvas/public/state/middleware/index.js index 88481c4682eb8..04a77ca06792a 100644 --- a/x-pack/plugins/canvas/public/state/middleware/index.js +++ b/x-pack/plugins/canvas/public/state/middleware/index.js @@ -14,6 +14,7 @@ import { historyMiddleware } from './history'; import { inFlight } from './in_flight'; import { workpadUpdate } from './workpad_update'; import { workpadRefresh } from './workpad_refresh'; +import { workpadAutoplay } from './workpad_autoplay'; import { appReady } from './app_ready'; import { elementStats } from './element_stats'; import { resolvedArgs } from './resolved_args'; @@ -30,7 +31,8 @@ const middlewares = [ inFlight, appReady, workpadUpdate, - workpadRefresh + workpadRefresh, + workpadAutoplay ), ]; diff --git a/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.js b/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.js new file mode 100644 index 0000000000000..75da7807944ba --- /dev/null +++ b/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.js @@ -0,0 +1,79 @@ +/* + * 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 { inFlightComplete } from '../actions/resolved_args'; +import { getFullscreen } from '../selectors/app'; +import { getInFlight } from '../selectors/resolved_args'; +import { getWorkpad, getPages, getSelectedPageIndex, getAutoplay } from '../selectors/workpad'; +import { routerProvider } from '../../lib/router_provider'; + +export const workpadAutoplay = ({ getState }) => next => { + let playTimeout; + let displayInterval = 0; + + const router = routerProvider(); + + function updateWorkpad() { + if (displayInterval === 0) { + return; + } + + // check the request in flight status + const inFlightActive = getInFlight(getState()); + + // only navigate if no requests are in-flight + if (!inFlightActive) { + // update the elements on the workpad + const workpadId = getWorkpad(getState()).id; + const pageIndex = getSelectedPageIndex(getState()); + const pageCount = getPages(getState()).length; + const nextPage = Math.min(pageIndex + 1, pageCount - 1); + + // go to start if on the last page + if (nextPage === pageIndex) { + router.navigateTo('loadWorkpad', { id: workpadId, page: 1 }); + } else { + router.navigateTo('loadWorkpad', { id: workpadId, page: nextPage + 1 }); + } + } + + startDelayedUpdate(); + } + + function stopAutoUpdate() { + clearTimeout(playTimeout); // cancel any pending update requests + } + + function startDelayedUpdate() { + stopAutoUpdate(); + playTimeout = setTimeout(() => { + updateWorkpad(); + }, displayInterval); + } + + return action => { + next(action); + + const isFullscreen = getFullscreen(getState()); + const autoplay = getAutoplay(getState()); + const shouldPlay = isFullscreen && autoplay.enabled && autoplay.interval > 0; + displayInterval = autoplay.interval; + + // when in-flight requests are finished, update the workpad after a given delay + if (action.type === inFlightComplete.toString() && shouldPlay) { + startDelayedUpdate(); + } // create new update request + + // This middleware creates or destroys an interval that will cause workpad elements to update + // clear any pending timeout + stopAutoUpdate(); + + // if interval is larger than 0, start the delayed update + if (shouldPlay) { + startDelayedUpdate(); + } + }; +}; diff --git a/x-pack/plugins/canvas/public/state/reducers/transient.js b/x-pack/plugins/canvas/public/state/reducers/transient.js index 43b2c9ccc188c..3924e08791e25 100644 --- a/x-pack/plugins/canvas/public/state/reducers/transient.js +++ b/x-pack/plugins/canvas/public/state/reducers/transient.js @@ -10,7 +10,7 @@ import { restoreHistory } from '../actions/history'; import * as pageActions from '../actions/pages'; import * as transientActions from '../actions/transient'; import { removeElements } from '../actions/elements'; -import { setRefreshInterval } from '../actions/workpad'; +import { setRefreshInterval, enableAutoplay, setAutoplayInterval } from '../actions/workpad'; export const transientReducer = handleActions( { @@ -63,6 +63,14 @@ export const transientReducer = handleActions( [setRefreshInterval]: (transientState, { payload }) => { return { ...transientState, refresh: { interval: Number(payload) || 0 } }; }, + + [enableAutoplay]: (transientState, { payload }) => { + return set(transientState, 'autoplay.enabled', Boolean(payload) || false); + }, + + [setAutoplayInterval]: (transientState, { payload }) => { + return set(transientState, 'autoplay.interval', Number(payload) || 0); + }, }, {} ); diff --git a/x-pack/plugins/canvas/public/state/selectors/workpad.js b/x-pack/plugins/canvas/public/state/selectors/workpad.js index 4bfa748e51fd6..7d9f5181fe9d1 100644 --- a/x-pack/plugins/canvas/public/state/selectors/workpad.js +++ b/x-pack/plugins/canvas/public/state/selectors/workpad.js @@ -240,3 +240,7 @@ export function getContextForIndex(state, index) { export function getRefreshInterval(state) { return get(state, 'transient.refresh.interval', 0); } + +export function getAutoplay(state) { + return get(state, 'transient.autoplay'); +}