From 07e6d2549d2b79c49be819a5a35e7a3735937d31 Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Tue, 25 May 2021 10:08:09 -0400 Subject: [PATCH 1/9] Switch Canvas to use React Router --- x-pack/plugins/canvas/i18n/components.ts | 8 + x-pack/plugins/canvas/public/application.tsx | 11 -- .../canvas/public/apps/export/routes.ts | 55 ------ .../public/apps/home/home_app/home_app.ts | 16 -- .../plugins/canvas/public/apps/home/routes.ts | 22 --- x-pack/plugins/canvas/public/apps/index.ts | 15 -- .../canvas/public/apps/workpad/routes.ts | 110 ----------- .../canvas/public/components/app/app.js | 74 -------- .../canvas/public/components/app/index.js | 53 ------ .../canvas/public/components/app/index.tsx | 49 +++++ .../canvas_loading.component.tsx} | 13 +- .../canvas_loading}/index.ts | 3 +- .../__snapshots__/export_app.test.tsx.snap | 0 .../export_app}/export_app.component.tsx | 10 +- .../export_app}/export_app.scss | 0 .../export_app}/export_app.test.tsx | 2 +- .../export_app}/export_app.ts | 6 +- .../export => components/export_app}/index.ts | 0 .../fullscreen/{index.js => index.tsx} | 13 +- .../home_app/home_app.component.tsx | 4 +- .../home_app/home_app.scss | 0 .../public/components/home_app/home_app.tsx | 25 +++ .../home => components}/home_app/index.ts | 0 .../canvas/public/components/link/link.tsx | 73 -------- .../page_manager/page_manager.component.tsx | 32 ++-- .../components/page_manager/page_manager.ts | 32 ---- .../components/page_manager/page_manager.tsx | 53 ++++++ .../components/page_preview/page_preview.ts | 25 --- .../components/page_preview/page_preview.tsx | 36 ++++ .../public/components/router/context.ts | 20 -- .../canvas/public/components/router/index.ts | 64 ------- .../canvas/public/components/router/router.js | 108 ----------- .../home => components/routing}/index.ts | 3 +- .../components/routing/routing_link.tsx | 39 ++++ .../components/toolbar/toolbar.component.tsx | 32 +--- .../canvas/public/components/workpad/index.js | 40 ++-- .../public/components/workpad/workpad.js | 8 - .../workpad_app/index.ts | 0 .../workpad_app/workpad_app.component.tsx | 12 +- .../workpad_app/workpad_app.scss | 0 .../workpad_app/workpad_app.ts | 8 +- .../workpad_app/workpad_telemetry.test.tsx | 4 +- .../workpad_app/workpad_telemetry.tsx | 6 +- .../edit_menu/{edit_menu.ts => edit_menu.tsx} | 12 +- .../fullscreen_control/fullscreen_control.tsx | 20 +- .../fullscreen_control/index.js | 94 +++++----- .../view_menu/auto_refresh_controls.tsx | 2 +- .../view_menu/kiosk_controls.tsx | 45 ++++- .../view_menu/view_menu.component.tsx | 16 +- .../view_menu/{view_menu.ts => view_menu.tsx} | 61 +++--- .../public/components/workpad_loader/index.js | 145 --------------- .../components/workpad_loader/index.tsx | 167 +++++++++++++++++ .../workpad_loader/workpad_loader.js | 9 +- .../public/components/workpad_page/index.js | 16 +- .../interaction_boundary.tsx | 2 +- .../components/workpad_templates/index.tsx | 37 ++-- x-pack/plugins/canvas/public/lib/app_state.ts | 124 ------------- .../plugins/canvas/public/lib/breadcrumbs.ts | 5 - .../canvas/public/lib/history_provider.js | 173 ------------------ .../canvas/public/lib/router_provider.js | 129 ------------- .../index.ts => routes/home/home_route.tsx} | 10 +- .../link/index.ts => routes/home/index.tsx} | 2 +- x-pack/plugins/canvas/public/routes/index.tsx | 22 +++ .../hooks/use_autoplay_helper.test.tsx | 81 ++++++++ .../workpad/hooks/use_autoplay_helper.ts | 29 +++ .../use_fullscreen_presentation_helper.ts | 31 ++++ .../workpad/hooks/use_page_sync.test.ts | 83 +++++++++ .../routes/workpad/hooks/use_page_sync.ts | 32 ++++ .../workpad/hooks/use_refresh_helper.test.tsx | 84 +++++++++ .../workpad/hooks/use_refresh_helper.ts | 36 ++++ .../hooks/use_restore_history.test.tsx | 78 ++++++++ .../workpad/hooks/use_restore_history.ts | 35 ++++ .../workpad/hooks/use_routing_context.ts | 160 ++++++++++++++++ .../routes/workpad/hooks/use_workpad.test.tsx | 67 +++++++ .../routes/workpad/hooks/use_workpad.ts | 42 +++++ .../workpad/hooks/use_workpad_history.test.ts | 101 ++++++++++ .../workpad/hooks/use_workpad_history.ts | 41 +++++ .../canvas/public/routes/workpad/index.tsx | 23 +++ .../public/routes/workpad/route_state.ts | 27 +++ .../workpad/workpad_presentation_helper.tsx | 38 ++++ .../public/routes/workpad/workpad_route.tsx | 120 ++++++++++++ .../workpad/workpad_routing_context.tsx | 57 ++++++ .../canvas/public/services/context.tsx | 1 + .../plugins/canvas/public/services/index.ts | 3 + .../plugins/canvas/public/services/notify.ts | 3 +- .../canvas/public/services/stubs/index.ts | 2 + .../canvas/public/services/stubs/workpad.ts | 21 +++ .../plugins/canvas/public/services/workpad.ts | 120 ++++++++++++ .../canvas/public/state/actions/pages.js | 6 +- .../public/state/middleware/app_ready.js | 27 --- .../public/state/middleware/breadcrumbs.js | 25 --- .../public/state/middleware/fullscreen.js | 23 --- .../canvas/public/state/middleware/history.js | 142 -------------- .../canvas/public/state/middleware/index.js | 14 +- .../state/middleware/workpad_autoplay.test.ts | 146 --------------- .../state/middleware/workpad_autoplay.ts | 94 ---------- .../state/middleware/workpad_refresh.test.ts | 159 ---------------- .../state/middleware/workpad_refresh.ts | 91 --------- .../canvas/public/state/reducers/pages.js | 39 ++-- x-pack/plugins/canvas/public/style/index.scss | 8 +- .../storybook/decorators/router_decorator.tsx | 5 +- 101 files changed, 2019 insertions(+), 2250 deletions(-) delete mode 100644 x-pack/plugins/canvas/public/apps/export/routes.ts delete mode 100644 x-pack/plugins/canvas/public/apps/home/home_app/home_app.ts delete mode 100644 x-pack/plugins/canvas/public/apps/home/routes.ts delete mode 100644 x-pack/plugins/canvas/public/apps/index.ts delete mode 100644 x-pack/plugins/canvas/public/apps/workpad/routes.ts delete mode 100644 x-pack/plugins/canvas/public/components/app/app.js delete mode 100644 x-pack/plugins/canvas/public/components/app/index.js create mode 100644 x-pack/plugins/canvas/public/components/app/index.tsx rename x-pack/plugins/canvas/public/components/{router/canvas_loading.js => canvas_loading/canvas_loading.component.tsx} (71%) rename x-pack/plugins/canvas/public/{apps/export => components/canvas_loading}/index.ts (77%) rename x-pack/plugins/canvas/public/{apps/export/export => components/export_app}/__snapshots__/export_app.test.tsx.snap (100%) rename x-pack/plugins/canvas/public/{apps/export/export => components/export_app}/export_app.component.tsx (87%) rename x-pack/plugins/canvas/public/{apps/export/export => components/export_app}/export_app.scss (100%) rename x-pack/plugins/canvas/public/{apps/export/export => components/export_app}/export_app.test.tsx (96%) rename x-pack/plugins/canvas/public/{apps/export/export => components/export_app}/export_app.ts (75%) rename x-pack/plugins/canvas/public/{apps/export/export => components/export_app}/index.ts (100%) rename x-pack/plugins/canvas/public/components/fullscreen/{index.js => index.tsx} (51%) rename x-pack/plugins/canvas/public/{apps/home => components}/home_app/home_app.component.tsx (88%) rename x-pack/plugins/canvas/public/{apps/home => components}/home_app/home_app.scss (100%) create mode 100644 x-pack/plugins/canvas/public/components/home_app/home_app.tsx rename x-pack/plugins/canvas/public/{apps/home => components}/home_app/index.ts (100%) delete mode 100644 x-pack/plugins/canvas/public/components/link/link.tsx delete mode 100644 x-pack/plugins/canvas/public/components/page_manager/page_manager.ts create mode 100644 x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx delete mode 100644 x-pack/plugins/canvas/public/components/page_preview/page_preview.ts create mode 100644 x-pack/plugins/canvas/public/components/page_preview/page_preview.tsx delete mode 100644 x-pack/plugins/canvas/public/components/router/context.ts delete mode 100644 x-pack/plugins/canvas/public/components/router/index.ts delete mode 100644 x-pack/plugins/canvas/public/components/router/router.js rename x-pack/plugins/canvas/public/{apps/home => components/routing}/index.ts (77%) create mode 100644 x-pack/plugins/canvas/public/components/routing/routing_link.tsx rename x-pack/plugins/canvas/public/{apps/workpad => components}/workpad_app/index.ts (100%) rename x-pack/plugins/canvas/public/{apps/workpad => components}/workpad_app/workpad_app.component.tsx (86%) rename x-pack/plugins/canvas/public/{apps/workpad => components}/workpad_app/workpad_app.scss (100%) rename x-pack/plugins/canvas/public/{apps/workpad => components}/workpad_app/workpad_app.ts (80%) rename x-pack/plugins/canvas/public/{apps/workpad => components}/workpad_app/workpad_telemetry.test.tsx (98%) rename x-pack/plugins/canvas/public/{apps/workpad => components}/workpad_app/workpad_telemetry.tsx (94%) rename x-pack/plugins/canvas/public/components/workpad_header/edit_menu/{edit_menu.ts => edit_menu.tsx} (93%) rename x-pack/plugins/canvas/public/components/workpad_header/view_menu/{view_menu.ts => view_menu.tsx} (70%) delete mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/index.js create mode 100644 x-pack/plugins/canvas/public/components/workpad_loader/index.tsx delete mode 100644 x-pack/plugins/canvas/public/lib/app_state.ts delete mode 100644 x-pack/plugins/canvas/public/lib/history_provider.js delete mode 100644 x-pack/plugins/canvas/public/lib/router_provider.js rename x-pack/plugins/canvas/public/{apps/workpad/index.ts => routes/home/home_route.tsx} (55%) rename x-pack/plugins/canvas/public/{components/link/index.ts => routes/home/index.tsx} (89%) create mode 100644 x-pack/plugins/canvas/public/routes/index.tsx create mode 100644 x-pack/plugins/canvas/public/routes/workpad/hooks/use_autoplay_helper.test.tsx create mode 100644 x-pack/plugins/canvas/public/routes/workpad/hooks/use_autoplay_helper.ts create mode 100644 x-pack/plugins/canvas/public/routes/workpad/hooks/use_fullscreen_presentation_helper.ts create mode 100644 x-pack/plugins/canvas/public/routes/workpad/hooks/use_page_sync.test.ts create mode 100644 x-pack/plugins/canvas/public/routes/workpad/hooks/use_page_sync.ts create mode 100644 x-pack/plugins/canvas/public/routes/workpad/hooks/use_refresh_helper.test.tsx create mode 100644 x-pack/plugins/canvas/public/routes/workpad/hooks/use_refresh_helper.ts create mode 100644 x-pack/plugins/canvas/public/routes/workpad/hooks/use_restore_history.test.tsx create mode 100644 x-pack/plugins/canvas/public/routes/workpad/hooks/use_restore_history.ts create mode 100644 x-pack/plugins/canvas/public/routes/workpad/hooks/use_routing_context.ts create mode 100644 x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.test.tsx create mode 100644 x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts create mode 100644 x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts create mode 100644 x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts create mode 100644 x-pack/plugins/canvas/public/routes/workpad/index.tsx create mode 100644 x-pack/plugins/canvas/public/routes/workpad/route_state.ts create mode 100644 x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx create mode 100644 x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx create mode 100644 x-pack/plugins/canvas/public/routes/workpad/workpad_routing_context.tsx create mode 100644 x-pack/plugins/canvas/public/services/stubs/workpad.ts create mode 100644 x-pack/plugins/canvas/public/services/workpad.ts delete mode 100644 x-pack/plugins/canvas/public/state/middleware/app_ready.js delete mode 100644 x-pack/plugins/canvas/public/state/middleware/breadcrumbs.js delete mode 100644 x-pack/plugins/canvas/public/state/middleware/fullscreen.js delete mode 100644 x-pack/plugins/canvas/public/state/middleware/history.js delete mode 100644 x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.test.ts delete mode 100644 x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.ts delete mode 100644 x-pack/plugins/canvas/public/state/middleware/workpad_refresh.test.ts delete mode 100644 x-pack/plugins/canvas/public/state/middleware/workpad_refresh.ts diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index b60f8db5b25b4..9b09fe809c4a7 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -1384,6 +1384,14 @@ export const ComponentStrings = { i18n.translate('xpack.canvas.workpadHeaderKioskControl.controlTitle', { defaultMessage: 'Cycle fullscreen pages', }), + getAutoplayListDurationManualText: () => + i18n.translate('xpack.canvas.workpadHeaderKioskControl.autoplayListDurationManual', { + defaultMessage: 'Manually', + }), + getDisableTooltip: () => + i18n.translate('xpack.canvas.workpadHeaderKioskControl.disableTooltip', { + defaultMessage: 'Disable auto-play', + }), }, WorkpadHeaderRefreshControlSettings: { getRefreshAriaLabel: () => diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index 154beb6faa7b0..4163cb88d5fef 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -18,7 +18,6 @@ import { includes, remove } from 'lodash'; import { AppMountParameters, CoreStart, CoreSetup, AppUpdater } from 'kibana/public'; import { CanvasStartDeps, CanvasSetupDeps } from './plugin'; -// @ts-expect-error untyped local import { App } from './components/app'; import { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; import { registerLanguage } from './lib/monaco_language_def'; @@ -32,10 +31,6 @@ import { init as initStatsReporter } from './lib/ui_metric'; import { CapabilitiesStrings } from '../i18n'; import { startServices, services, ServicesProvider } from './services'; -// @ts-expect-error untyped local -import { createHistory, destroyHistory } from './lib/history_provider'; -// @ts-expect-error untyped local -import { stopRouter } from './lib/router_provider'; import { initFunctions } from './functions'; // @ts-expect-error untyped local import { appUnload } from './state/actions/app'; @@ -103,9 +98,6 @@ export const initializeCanvas = async ( services.expressions.getService().registerFunction(fn); } - // Re-initialize our history - createHistory(); - // Create Store const canvasStore = await createStore(coreSetup, setupPlugins); @@ -178,7 +170,4 @@ export const teardownCanvas = (coreStart: CoreStart, startPlugins: CanvasStartDe coreStart.chrome.setBadge(undefined); coreStart.chrome.setHelpExtension(undefined); - - destroyHistory(); - stopRouter(); }; diff --git a/x-pack/plugins/canvas/public/apps/export/routes.ts b/x-pack/plugins/canvas/public/apps/export/routes.ts deleted file mode 100644 index 2887c780f308c..0000000000000 --- a/x-pack/plugins/canvas/public/apps/export/routes.ts +++ /dev/null @@ -1,55 +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 { Dispatch } from 'redux'; -// @ts-expect-error Untyped local -import * as workpadService from '../../lib/workpad_service'; -import { setWorkpad } from '../../state/actions/workpad'; -// @ts-expect-error Untyped local -import { fetchAllRenderables } from '../../state/actions/elements'; -// @ts-expect-error Untyped local -import { setPage } from '../../state/actions/pages'; -// @ts-expect-error Untyped local -import { setAssets } from '../../state/actions/assets'; -import { ExportApp } from './export'; - -export const routes = [ - { - path: '/export/workpad', - children: [ - { - name: 'exportWorkpad', - path: '/pdf/:id/page/:page', - action: (dispatch: Dispatch) => async ({ - params, - // @ts-expect-error Fix when Router is typed. - router, - }: { - params: { id: string; page: string }; - }) => { - // load workpad if given a new id via url param - const fetchedWorkpad = await workpadService.get(params.id); - const pageNumber = parseInt(params.page, 10); - - // redirect to home app on invalid workpad id or page number - if (fetchedWorkpad == null && isNaN(pageNumber)) { - return router.redirectTo('home'); - } - - const { assets, ...workpad } = fetchedWorkpad; - dispatch(setAssets(assets)); - dispatch(setWorkpad(workpad, { loadPages: false })); - dispatch(setPage(pageNumber - 1)); - dispatch(fetchAllRenderables({ onlyActivePage: true })); - }, - meta: { - component: ExportApp, - }, - }, - ], - }, -]; diff --git a/x-pack/plugins/canvas/public/apps/home/home_app/home_app.ts b/x-pack/plugins/canvas/public/apps/home/home_app/home_app.ts deleted file mode 100644 index 78012eed2c587..0000000000000 --- a/x-pack/plugins/canvas/public/apps/home/home_app/home_app.ts +++ /dev/null @@ -1,16 +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 { connect } from 'react-redux'; -import { resetWorkpad } from '../../../state/actions/workpad'; -import { HomeApp as Component } from './home_app.component'; - -export const HomeApp = connect(null, (dispatch) => ({ - onLoad() { - dispatch(resetWorkpad()); - }, -}))(Component); diff --git a/x-pack/plugins/canvas/public/apps/home/routes.ts b/x-pack/plugins/canvas/public/apps/home/routes.ts deleted file mode 100644 index 13d0d7d758f6d..0000000000000 --- a/x-pack/plugins/canvas/public/apps/home/routes.ts +++ /dev/null @@ -1,22 +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 { getBaseBreadcrumb, setBreadcrumb } from '../../lib/breadcrumbs'; -import { HomeApp } from './home_app'; - -export const routes = [ - { - name: 'home', - path: '/', - action: () => () => { - setBreadcrumb([getBaseBreadcrumb()]); - }, - meta: { - component: HomeApp, - }, - }, -]; diff --git a/x-pack/plugins/canvas/public/apps/index.ts b/x-pack/plugins/canvas/public/apps/index.ts deleted file mode 100644 index 74defd3533ae0..0000000000000 --- a/x-pack/plugins/canvas/public/apps/index.ts +++ /dev/null @@ -1,15 +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 * as home from './home'; -import * as workpad from './workpad'; -import * as exp from './export'; - -// @ts-expect-error Router and routes are not yet strongly typed -export const routes = [].concat(workpad.routes, home.routes, exp.routes); - -export const apps = [workpad.WorkpadApp, home.HomeApp, exp.ExportApp]; diff --git a/x-pack/plugins/canvas/public/apps/workpad/routes.ts b/x-pack/plugins/canvas/public/apps/workpad/routes.ts deleted file mode 100644 index 8ecba5e183343..0000000000000 --- a/x-pack/plugins/canvas/public/apps/workpad/routes.ts +++ /dev/null @@ -1,110 +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 { Dispatch } from 'redux'; -// @ts-expect-error -import * as workpadService from '../../lib/workpad_service'; -import { notifyService } from '../../services'; -import { getBaseBreadcrumb, getWorkpadBreadcrumb, setBreadcrumb } from '../../lib/breadcrumbs'; -// @ts-expect-error -import { getDefaultWorkpad } from '../../state/defaults'; -import { setWorkpad } from '../../state/actions/workpad'; -// @ts-expect-error -import { setAssets, resetAssets } from '../../state/actions/assets'; -// @ts-expect-error -import { setPage } from '../../state/actions/pages'; -import { getWorkpad } from '../../state/selectors/workpad'; -// @ts-expect-error -import { setZoomScale } from '../../state/actions/transient'; -import { ErrorStrings } from '../../../i18n'; -import { WorkpadApp } from './workpad_app'; -import { State } from '../../../types'; - -const { workpadRoutes: strings } = ErrorStrings; - -export const routes = [ - { - path: '/workpad', - children: [ - { - name: 'createWorkpad', - path: '/create', - // @ts-expect-error Fix when Router is typed. - action: (dispatch: Dispatch) => async ({ router }) => { - const newWorkpad = getDefaultWorkpad(); - try { - await workpadService.create(newWorkpad); - dispatch(setWorkpad(newWorkpad)); - dispatch(resetAssets()); - router.redirectTo('loadWorkpad', { id: newWorkpad.id, page: 1 }); - } catch (err) { - notifyService - .getService() - .error(err, { title: strings.getCreateFailureErrorMessage() }); - router.redirectTo('home'); - } - }, - meta: { - component: WorkpadApp, - }, - }, - { - name: 'loadWorkpad', - path: '/:id(/page/:page)', - action: (dispatch: Dispatch, getState: () => State) => async ({ - params, - // @ts-expect-error Fix when Router is typed. - router, - }: { - params: { id: string; page?: string }; - }) => { - // load workpad if given a new id via url param - const state = getState(); - const currentWorkpad = getWorkpad(state); - if (params.id !== currentWorkpad.id) { - try { - const fetchedWorkpad = await workpadService.get(params.id); - - const { assets, ...workpad } = fetchedWorkpad; - dispatch(setAssets(assets)); - dispatch(setWorkpad(workpad)); - - // reset transient properties when changing workpads - dispatch(setZoomScale(1)); - } catch (err) { - notifyService - .getService() - .error(err, { title: strings.getLoadFailureErrorMessage() }); - return router.redirectTo('home'); - } - } - - // fetch the workpad again, to get changes - const workpad = getWorkpad(getState()); - const pageNumber = params.page ? parseInt(params.page, 10) : null; - - // no page provided, append current page to url - if (!pageNumber || isNaN(pageNumber)) { - return router.redirectTo('loadWorkpad', { id: workpad.id, page: workpad.page + 1 }); - } - - // set the active page using the number provided in the url - const pageIndex = pageNumber - 1; - if (pageIndex !== workpad.page) { - dispatch(setPage(pageIndex)); - } - - // update the application's breadcrumb - setBreadcrumb([getBaseBreadcrumb(), getWorkpadBreadcrumb(workpad)]); - }, - meta: { - component: WorkpadApp, - }, - }, - ], - }, -]; diff --git a/x-pack/plugins/canvas/public/components/app/app.js b/x-pack/plugins/canvas/public/components/app/app.js deleted file mode 100644 index 7a2c6ea5c6b73..0000000000000 --- a/x-pack/plugins/canvas/public/components/app/app.js +++ /dev/null @@ -1,74 +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 { routes } from '../../apps'; -import { shortcutManager } from '../../lib/shortcut_manager'; -import { getWindow } from '../../lib/get_window'; -import { Router } from '../router'; - -import { ComponentStrings } from '../../../i18n'; - -const { App: strings } = ComponentStrings; - -export class App extends React.PureComponent { - static propTypes = { - appState: PropTypes.object.isRequired, - setAppReady: PropTypes.func.isRequired, - setAppError: PropTypes.func.isRequired, - onRouteChange: PropTypes.func.isRequired, - }; - - static childContextTypes = { - shortcuts: PropTypes.object.isRequired, - }; - - getChildContext() { - return { shortcuts: shortcutManager }; - } - - componentDidMount() { - const win = getWindow(); - win.canvasInitErrorHandler && win.canvasInitErrorHandler(); - } - - componentWillUnmount() { - const win = getWindow(); - win.canvasRestoreErrorHandler && win.canvasRestoreErrorHandler(); - } - - renderError = () => { - console.error(this.props.appState); - - return ( -
-
{strings.getLoadErrorTitle()}
-
{strings.getLoadErrorMessage(this.props.appState.messgae)}
-
- ); - }; - - render() { - if (this.props.appState instanceof Error) { - return this.renderError(); - } - - return ( -
- this.props.setAppReady(true)} - onError={(err) => this.props.setAppError(err)} - /> -
- ); - } -} diff --git a/x-pack/plugins/canvas/public/components/app/index.js b/x-pack/plugins/canvas/public/components/app/index.js deleted file mode 100644 index e872f9a1a5ae5..0000000000000 --- a/x-pack/plugins/canvas/public/components/app/index.js +++ /dev/null @@ -1,53 +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 { connect } from 'react-redux'; -import { compose, withProps } from 'recompose'; -import { getAppReady, getBasePath } from '../../state/selectors/app'; -import { appReady, appError } from '../../state/actions/app'; -import { withServices } from '../../services'; - -import { App as Component } from './app'; - -const mapStateToProps = (state) => { - // appReady could be an error object - const appState = getAppReady(state); - - return { - appState: typeof appState === 'object' ? appState : { ready: appState }, - basePath: getBasePath(state), - }; -}; - -const mapDispatchToProps = (dispatch) => ({ - setAppReady: () => async () => { - try { - // set app state to ready - dispatch(appReady()); - } catch (e) { - dispatch(appError(e)); - } - }, - setAppError: (payload) => dispatch(appError(payload)), -}); - -const mergeProps = (stateProps, dispatchProps, ownProps) => { - return { - ...ownProps, - ...stateProps, - ...dispatchProps, - setAppReady: dispatchProps.setAppReady(stateProps.basePath), - }; -}; - -export const App = compose( - connect(mapStateToProps, mapDispatchToProps, mergeProps), - withServices, - withProps((props) => ({ - onRouteChange: props.services.navLink.updatePath, - })) -)(Component); diff --git a/x-pack/plugins/canvas/public/components/app/index.tsx b/x-pack/plugins/canvas/public/components/app/index.tsx new file mode 100644 index 0000000000000..c2d13ea537f62 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/app/index.tsx @@ -0,0 +1,49 @@ +/* + * 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, useRef, useEffect } from 'react'; +import PropTypes from 'prop-types'; +import { History } from 'history'; +// @ts-expect-error +import createHashStateHistory from 'history-extra/dist/createHashStateHistory'; +import { useServices } from '../../services'; +// @ts-expect-error +import { shortcutManager } from '../../lib/shortcut_manager'; +import { CanvasRouter } from '../../routes'; + +class ShortcutManagerContextWrapper extends React.Component { + static childContextTypes = { + shortcuts: PropTypes.object.isRequired, + }; + + getChildContext() { + return { shortcuts: shortcutManager }; + } + + render() { + return <>{this.props.children}; + } +} + +export const App: FC = () => { + const historyRef = useRef(createHashStateHistory() as History); + const services = useServices(); + + useEffect(() => { + return historyRef.current.listen(({ pathname }) => { + services.navLink.updatePath(pathname); + }); + }); + + return ( + +
+ +
+
+ ); +}; diff --git a/x-pack/plugins/canvas/public/components/router/canvas_loading.js b/x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx similarity index 71% rename from x-pack/plugins/canvas/public/components/router/canvas_loading.js rename to x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx index 994367d5973c1..17d1391497f51 100644 --- a/x-pack/plugins/canvas/public/components/router/canvas_loading.js +++ b/x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx @@ -5,11 +5,10 @@ * 2.0. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { FC } from 'react'; import { EuiPanel, EuiLoadingChart, EuiSpacer, EuiText } from '@elastic/eui'; -export const CanvasLoading = ({ msg }) => ( +export const CanvasLoading: FC<{ msg?: string }> = ({ msg = 'Loading...' }) => (
@@ -20,11 +19,3 @@ export const CanvasLoading = ({ msg }) => (
); - -CanvasLoading.propTypes = { - msg: PropTypes.string, -}; - -CanvasLoading.defaultProps = { - msg: 'Loading...', -}; diff --git a/x-pack/plugins/canvas/public/apps/export/index.ts b/x-pack/plugins/canvas/public/components/canvas_loading/index.ts similarity index 77% rename from x-pack/plugins/canvas/public/apps/export/index.ts rename to x-pack/plugins/canvas/public/components/canvas_loading/index.ts index 898d61d935a8d..80efef1915082 100644 --- a/x-pack/plugins/canvas/public/apps/export/index.ts +++ b/x-pack/plugins/canvas/public/components/canvas_loading/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export { routes } from './routes'; -export { ExportApp } from './export'; +export * from './canvas_loading.component'; diff --git a/x-pack/plugins/canvas/public/apps/export/export/__snapshots__/export_app.test.tsx.snap b/x-pack/plugins/canvas/public/components/export_app/__snapshots__/export_app.test.tsx.snap similarity index 100% rename from x-pack/plugins/canvas/public/apps/export/export/__snapshots__/export_app.test.tsx.snap rename to x-pack/plugins/canvas/public/components/export_app/__snapshots__/export_app.test.tsx.snap diff --git a/x-pack/plugins/canvas/public/apps/export/export/export_app.component.tsx b/x-pack/plugins/canvas/public/components/export_app/export_app.component.tsx similarity index 87% rename from x-pack/plugins/canvas/public/apps/export/export/export_app.component.tsx rename to x-pack/plugins/canvas/public/components/export_app/export_app.component.tsx index 86ebada396109..c3facb9268961 100644 --- a/x-pack/plugins/canvas/public/apps/export/export/export_app.component.tsx +++ b/x-pack/plugins/canvas/public/components/export_app/export_app.component.tsx @@ -10,9 +10,9 @@ import PropTypes from 'prop-types'; // @ts-expect-error untyped library import Style from 'style-it'; // @ts-expect-error untyped local -import { WorkpadPage } from '../../../components/workpad_page'; -import { Link } from '../../../components/link'; -import { CanvasWorkpad } from '../../../../types'; +import { WorkpadPage } from '../workpad_page'; +import { RoutingLink } from '../routing'; +import { CanvasWorkpad } from '../../../types'; export interface Props { workpad: CanvasWorkpad; @@ -31,9 +31,7 @@ export const ExportApp: FC = ({ workpad, selectedPageIndex, initializeWor
- - Edit Workpad - + Edit Workpad
{Style.it( workpad.css, diff --git a/x-pack/plugins/canvas/public/apps/export/export/export_app.scss b/x-pack/plugins/canvas/public/components/export_app/export_app.scss similarity index 100% rename from x-pack/plugins/canvas/public/apps/export/export/export_app.scss rename to x-pack/plugins/canvas/public/components/export_app/export_app.scss diff --git a/x-pack/plugins/canvas/public/apps/export/export/export_app.test.tsx b/x-pack/plugins/canvas/public/components/export_app/export_app.test.tsx similarity index 96% rename from x-pack/plugins/canvas/public/apps/export/export/export_app.test.tsx rename to x-pack/plugins/canvas/public/components/export_app/export_app.test.tsx index 9f019ad70aed3..27ebf520a6380 100644 --- a/x-pack/plugins/canvas/public/apps/export/export/export_app.test.tsx +++ b/x-pack/plugins/canvas/public/components/export_app/export_app.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { ExportApp } from './export_app.component'; -import { CanvasWorkpad } from '../../../../types'; +import { CanvasWorkpad } from '../../../types'; jest.mock('style-it', () => ({ it: (css: string, Component: any) => Component, diff --git a/x-pack/plugins/canvas/public/apps/export/export/export_app.ts b/x-pack/plugins/canvas/public/components/export_app/export_app.ts similarity index 75% rename from x-pack/plugins/canvas/public/apps/export/export/export_app.ts rename to x-pack/plugins/canvas/public/components/export_app/export_app.ts index ba3fa3dd445e8..1b9c603657b10 100644 --- a/x-pack/plugins/canvas/public/apps/export/export/export_app.ts +++ b/x-pack/plugins/canvas/public/components/export_app/export_app.ts @@ -6,10 +6,10 @@ */ import { connect } from 'react-redux'; -import { initializeWorkpad } from '../../../state/actions/workpad'; -import { getWorkpad, getSelectedPageIndex } from '../../../state/selectors/workpad'; +import { initializeWorkpad } from '../../state/actions/workpad'; +import { getWorkpad, getSelectedPageIndex } from '../../state/selectors/workpad'; import { ExportApp as Component } from './export_app.component'; -import { State } from '../../../../types'; +import { State } from '../../../types'; export const ExportApp = connect( (state: State) => ({ diff --git a/x-pack/plugins/canvas/public/apps/export/export/index.ts b/x-pack/plugins/canvas/public/components/export_app/index.ts similarity index 100% rename from x-pack/plugins/canvas/public/apps/export/export/index.ts rename to x-pack/plugins/canvas/public/components/export_app/index.ts diff --git a/x-pack/plugins/canvas/public/components/fullscreen/index.js b/x-pack/plugins/canvas/public/components/fullscreen/index.tsx similarity index 51% rename from x-pack/plugins/canvas/public/components/fullscreen/index.js rename to x-pack/plugins/canvas/public/components/fullscreen/index.tsx index 9a59e142a1a34..90bed715f8d71 100644 --- a/x-pack/plugins/canvas/public/components/fullscreen/index.js +++ b/x-pack/plugins/canvas/public/components/fullscreen/index.tsx @@ -5,12 +5,13 @@ * 2.0. */ -import { connect } from 'react-redux'; -import { getFullscreen } from '../../state/selectors/app'; +import React, { FC, useContext } from 'react'; import { Fullscreen as Component } from './fullscreen'; -const mapStateToProps = (state) => ({ - isFullscreen: getFullscreen(state), -}); +import { WorkpadRoutingContext } from '../../routes/workpad'; -export const Fullscreen = connect(mapStateToProps)(Component); +export const Fullscreen: FC = ({ children }) => { + const { isFullscreen } = useContext(WorkpadRoutingContext); + + return ; +}; diff --git a/x-pack/plugins/canvas/public/apps/home/home_app/home_app.component.tsx b/x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx similarity index 88% rename from x-pack/plugins/canvas/public/apps/home/home_app/home_app.component.tsx rename to x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx index 30e9fbc153750..712b06cb39299 100644 --- a/x-pack/plugins/canvas/public/apps/home/home_app/home_app.component.tsx +++ b/x-pack/plugins/canvas/public/components/home_app/home_app.component.tsx @@ -8,9 +8,9 @@ import React, { FC } from 'react'; import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; // @ts-expect-error untyped local -import { WorkpadManager } from '../../../components/workpad_manager'; +import { WorkpadManager } from '../workpad_manager'; // @ts-expect-error untyped local -import { setDocTitle } from '../../../lib/doc_title'; +import { setDocTitle } from '../../lib/doc_title'; export interface Props { onLoad: () => void; diff --git a/x-pack/plugins/canvas/public/apps/home/home_app/home_app.scss b/x-pack/plugins/canvas/public/components/home_app/home_app.scss similarity index 100% rename from x-pack/plugins/canvas/public/apps/home/home_app/home_app.scss rename to x-pack/plugins/canvas/public/components/home_app/home_app.scss diff --git a/x-pack/plugins/canvas/public/components/home_app/home_app.tsx b/x-pack/plugins/canvas/public/components/home_app/home_app.tsx new file mode 100644 index 0000000000000..fde448a79bf1e --- /dev/null +++ b/x-pack/plugins/canvas/public/components/home_app/home_app.tsx @@ -0,0 +1,25 @@ +/* + * 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, { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { getBaseBreadcrumb } from '../../lib/breadcrumbs'; +import { resetWorkpad } from '../../state/actions/workpad'; +import { HomeApp as Component } from './home_app.component'; +import { useServices } from '../../services'; + +export const HomeApp = () => { + const services = useServices(); + const dispatch = useDispatch(); + const onLoad = () => dispatch(resetWorkpad()); + + useEffect(() => { + services.platform.setBreadcrumbs([getBaseBreadcrumb()]); + }, [services.platform]); + + return ; +}; diff --git a/x-pack/plugins/canvas/public/apps/home/home_app/index.ts b/x-pack/plugins/canvas/public/components/home_app/index.ts similarity index 100% rename from x-pack/plugins/canvas/public/apps/home/home_app/index.ts rename to x-pack/plugins/canvas/public/components/home_app/index.ts diff --git a/x-pack/plugins/canvas/public/components/link/link.tsx b/x-pack/plugins/canvas/public/components/link/link.tsx deleted file mode 100644 index ef78b11654b23..0000000000000 --- a/x-pack/plugins/canvas/public/components/link/link.tsx +++ /dev/null @@ -1,73 +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, { FC, MouseEvent, useContext } from 'react'; -import PropTypes from 'prop-types'; -import { EuiLink, EuiLinkProps } from '@elastic/eui'; -import { RouterContext } from '../router'; - -import { ComponentStrings } from '../../../i18n'; - -const { Link: strings } = ComponentStrings; - -const isModifiedEvent = (ev: MouseEvent) => - !!(ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey); - -interface Props { - name: string; - params: Record; -} - -export const Link: FC = ({ - onClick, - target, - name, - params, - children, - ...linkArgs -}) => { - const router = useContext(RouterContext); - - if (router) { - const navigateTo = (ev: MouseEvent) => { - if (onClick) { - onClick(ev); - } - - if ( - !ev.defaultPrevented && // onClick prevented default - ev.button === 0 && // ignore everything but left clicks - !target && // let browser handle "target=_blank" etc. - !isModifiedEvent(ev) // ignore clicks with modifier keys - ) { - ev.preventDefault(); - router.navigateTo(name, params); - } - }; - - try { - return ( - - {children} - - ); - } catch (e) { - return
{strings.getErrorMessage(e.message)}
; - } - } - - return
{strings.getErrorMessage('Router Undefined')}
; -}; - -Link.contextTypes = { - router: PropTypes.object, -}; - -Link.propTypes = { - name: PropTypes.string.isRequired, - params: PropTypes.object, -}; diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx b/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx index ab455bcdaadf6..a9fcecceb2f37 100644 --- a/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx +++ b/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx @@ -11,9 +11,9 @@ import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elasti import { DragDropContext, Droppable, Draggable, DragDropContextProps } from 'react-beautiful-dnd'; // @ts-expect-error untyped dependency import Style from 'style-it'; - import { ConfirmModal } from '../confirm_modal'; -import { Link } from '../link'; +import { RoutingLink } from '../routing'; +import { WorkpadRoutingContext } from '../../routes/workpad'; import { PagePreview } from '../page_preview'; import { ComponentStrings } from '../../../i18n'; @@ -131,14 +131,10 @@ export class PageManager extends Component { resetRemove = () => this._isMounted && this.setState({ removeId: null }); doRemove = () => { - const { onPreviousPage, onRemovePage, selectedPage } = this.props; + const { onRemovePage } = this.props; const { removeId } = this.state; this.resetRemove(); - if (removeId === selectedPage) { - onPreviousPage(); - } - if (removeId !== null) { onRemovePage(removeId); } @@ -183,18 +179,18 @@ export class PageManager extends Component { - - {Style.it( - workpadCSS, -
- -
+ + {({ getUrl }) => ( + + {Style.it( + workpadCSS, +
+ +
+ )} +
)} - +
diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.ts b/x-pack/plugins/canvas/public/components/page_manager/page_manager.ts deleted file mode 100644 index 64b97fed7e930..0000000000000 --- a/x-pack/plugins/canvas/public/components/page_manager/page_manager.ts +++ /dev/null @@ -1,32 +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 { Dispatch } from 'redux'; -import { connect } from 'react-redux'; -// @ts-expect-error untyped local -import * as pageActions from '../../state/actions/pages'; -import { canUserWrite } from '../../state/selectors/app'; -import { getSelectedPage, getWorkpad, getPages, isWriteable } from '../../state/selectors/workpad'; -import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; -import { PageManager as Component } from './page_manager.component'; -import { State } from '../../../types'; - -const mapStateToProps = (state: State) => ({ - isWriteable: isWriteable(state) && canUserWrite(state), - pages: getPages(state), - selectedPage: getSelectedPage(state), - workpadId: getWorkpad(state).id, - workpadCSS: getWorkpad(state).css || DEFAULT_WORKPAD_CSS, -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - onAddPage: () => dispatch(pageActions.addPage()), - onMovePage: (id: string, position: number) => dispatch(pageActions.movePage(id, position)), - onRemovePage: (id: string) => dispatch(pageActions.removePage(id)), -}); - -export const PageManager = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx b/x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx new file mode 100644 index 0000000000000..53aa68eb51d09 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx @@ -0,0 +1,53 @@ +/* + * 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, useCallback, useContext } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +// @ts-expect-error untyped local +import * as pageActions from '../../state/actions/pages'; +import { canUserWrite } from '../../state/selectors/app'; +import { getSelectedPage, getWorkpad, getPages, isWriteable } from '../../state/selectors/workpad'; +import { DEFAULT_WORKPAD_CSS } from '../../../common/lib/constants'; +import { PageManager as Component } from './page_manager.component'; +import { State } from '../../../types'; +import { WorkpadRoutingContext } from '../../routes/workpad'; + +export const PageManager: FC<{ onPreviousPage: () => void }> = ({ onPreviousPage }) => { + const dispatch = useDispatch(); + const propsFromState = useSelector((state: State) => ({ + isWriteable: isWriteable(state) && canUserWrite(state), + pages: getPages(state), + selectedPage: getSelectedPage(state), + workpadId: getWorkpad(state).id, + workpadCSS: getWorkpad(state).css || DEFAULT_WORKPAD_CSS, + })); + + const { gotoPage } = useContext(WorkpadRoutingContext); + + const onAddPage = useCallback(() => dispatch(pageActions.addPage({ gotoPage })), [ + dispatch, + gotoPage, + ]); + const onMovePage = useCallback( + (id: string, position: number) => dispatch(pageActions.movePage(id, position, gotoPage)), + [dispatch, gotoPage] + ); + const onRemovePage = useCallback( + (id: string) => dispatch(pageActions.removePage({ id, gotoPage })), + [dispatch, gotoPage] + ); + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/page_preview/page_preview.ts b/x-pack/plugins/canvas/public/components/page_preview/page_preview.ts deleted file mode 100644 index c9a866f5182a9..0000000000000 --- a/x-pack/plugins/canvas/public/components/page_preview/page_preview.ts +++ /dev/null @@ -1,25 +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 { Dispatch } from 'redux'; -import { connect } from 'react-redux'; -// @ts-expect-error untyped local -import * as pageActions from '../../state/actions/pages'; -import { canUserWrite } from '../../state/selectors/app'; -import { isWriteable } from '../../state/selectors/workpad'; -import { PagePreview as Component } from './page_preview.component'; -import { State } from '../../../types'; - -const mapStateToProps = (state: State) => ({ - isWriteable: isWriteable(state) && canUserWrite(state), -}); - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - onDuplicate: (id: string) => dispatch(pageActions.duplicatePage(id)), -}); - -export const PagePreview = connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/x-pack/plugins/canvas/public/components/page_preview/page_preview.tsx b/x-pack/plugins/canvas/public/components/page_preview/page_preview.tsx new file mode 100644 index 0000000000000..15d81a2e89d80 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/page_preview/page_preview.tsx @@ -0,0 +1,36 @@ +/* + * 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 } from 'react'; + +import { connect, useDispatch, useSelector } from 'react-redux'; +// @ts-expect-error untyped local +import * as pageActions from '../../state/actions/pages'; +import { canUserWrite } from '../../state/selectors/app'; +import { isWriteable } from '../../state/selectors/workpad'; +import { PagePreview as Component, Props } from './page_preview.component'; +import { State } from '../../../types'; +import { WorkpadRoutingContext } from '../../routes/workpad'; + +export const PagePreview: FC> = (props) => { + const dispatch = useDispatch(); + const stateFromProps = useSelector((state: State) => ({ + isWriteable: isWriteable(state) && canUserWrite(state), + })); + const { gotoPage } = useContext(WorkpadRoutingContext); + + const onDuplicate = useCallback( + (id: string) => { + dispatch(pageActions.duplicatePage({ id, gotoPage })); + }, + [dispatch, gotoPage] + ); + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/router/context.ts b/x-pack/plugins/canvas/public/components/router/context.ts deleted file mode 100644 index 273503d610d18..0000000000000 --- a/x-pack/plugins/canvas/public/components/router/context.ts +++ /dev/null @@ -1,20 +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'; - -// TODO: We should fully build out this interface for our router -// or switch to a different router that is already typed -interface Router { - navigateTo: ( - name: string, - params: Record, - state?: Record - ) => void; -} - -export const RouterContext = React.createContext(undefined); diff --git a/x-pack/plugins/canvas/public/components/router/index.ts b/x-pack/plugins/canvas/public/components/router/index.ts deleted file mode 100644 index 7942fed6ae587..0000000000000 --- a/x-pack/plugins/canvas/public/components/router/index.ts +++ /dev/null @@ -1,64 +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 { connect } from 'react-redux'; -// @ts-expect-error untyped local -import { setFullscreen } from '../../state/actions/transient'; -import { - enableAutoplay, - setRefreshInterval, - setAutoplayInterval, -} from '../../state/actions/workpad'; -// @ts-expect-error untyped local -import { Router as Component } from './router'; -import { State } from '../../../types'; -export * from './context'; - -const mapDispatchToProps = { - enableAutoplay, - setAutoplayInterval, - setFullscreen, - setRefreshInterval, -}; - -const mapStateToProps = (state: State) => ({ - refreshInterval: state.transient.refresh.interval, - autoplayInterval: state.transient.autoplay.interval, - autoplay: state.transient.autoplay.enabled, - fullscreen: state.transient.fullScreen, -}); - -export const Router = connect( - mapStateToProps, - mapDispatchToProps, - (stateProps, dispatchProps, ownProps) => { - return { - ...ownProps, - ...dispatchProps, - setRefreshInterval: (interval: number) => { - if (interval !== stateProps.refreshInterval) { - dispatchProps.setRefreshInterval(interval); - } - }, - setAutoplayInterval: (interval: number) => { - if (interval !== stateProps.autoplayInterval) { - dispatchProps.setRefreshInterval(interval); - } - }, - enableAutoplay: (autoplay: boolean) => { - if (autoplay !== stateProps.autoplay) { - dispatchProps.enableAutoplay(autoplay); - } - }, - setFullscreen: (fullscreen: boolean) => { - if (fullscreen !== stateProps.fullscreen) { - dispatchProps.setFullscreen(fullscreen); - } - }, - }; - } -)(Component); diff --git a/x-pack/plugins/canvas/public/components/router/router.js b/x-pack/plugins/canvas/public/components/router/router.js deleted file mode 100644 index c68fbe9bdd98e..0000000000000 --- a/x-pack/plugins/canvas/public/components/router/router.js +++ /dev/null @@ -1,108 +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 { routerProvider } from '../../lib/router_provider'; -import { getAppState } from '../../lib/app_state'; -import { getTimeInterval } from '../../lib/time_interval'; -import { CanvasLoading } from './canvas_loading'; -import { RouterContext } from './'; - -export class Router extends React.PureComponent { - static propTypes = { - showLoading: PropTypes.bool.isRequired, - onLoad: PropTypes.func.isRequired, - onError: PropTypes.func.isRequired, - routes: PropTypes.array.isRequired, - loadingMessage: PropTypes.string, - onRouteChange: PropTypes.func, - setFullscreen: PropTypes.func.isRequired, - }; - - static childContextTypes = { - router: PropTypes.object.isRequired, - }; - - state = { - router: {}, - activeComponent: CanvasLoading, - }; - - getChildContext() { - const { router } = this.state; - return { router }; - } - - UNSAFE_componentWillMount() { - // routerProvider is a singleton, and will only ever return one instance - const { routes, onRouteChange, onLoad, onError } = this.props; - const router = routerProvider(routes); - let firstLoad = true; - - // when the component in the route changes, render it - router.onPathChange((route) => { - const { pathname } = route.location; - const { component } = route.meta; - - if (!component) { - // TODO: render some kind of 404 page, maybe from a prop? - if (process.env.NODE_ENV !== 'production') { - console.warn(`No component defined on route: ${route.name}`); - } - - return; - } - - // if this is the first load, execute the route - if (firstLoad) { - firstLoad = false; - - // execute the route - router - .execute() - .then(() => onLoad()) - .catch((err) => onError(err)); - } - - const appState = getAppState(); - - if (appState.__fullscreen) { - this.props.setFullscreen(appState.__fullscreen); - } - - if (appState.__refreshInterval) { - this.props.setRefreshInterval(getTimeInterval(appState.__refreshInterval)); - } - - if (!!appState.__autoplayInterval) { - this.props.enableAutoplay(true); - this.props.setAutoplayInterval(getTimeInterval(appState.__autoplayInterval)); - } - - // notify upstream handler of route change - onRouteChange && onRouteChange(pathname); - - this.setState({ activeComponent: component }); - }); - - this.setState({ router }); - } - - render() { - // show loading - if (this.props.showLoading) { - return React.createElement(CanvasLoading, { msg: this.props.loadingMessage }); - } - - return ( - - - - ); - } -} diff --git a/x-pack/plugins/canvas/public/apps/home/index.ts b/x-pack/plugins/canvas/public/components/routing/index.ts similarity index 77% rename from x-pack/plugins/canvas/public/apps/home/index.ts rename to x-pack/plugins/canvas/public/components/routing/index.ts index bfa5245af9e9a..a87401f952103 100644 --- a/x-pack/plugins/canvas/public/apps/home/index.ts +++ b/x-pack/plugins/canvas/public/components/routing/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export { routes } from './routes'; -export { HomeApp } from './home_app'; +export * from './routing_link'; diff --git a/x-pack/plugins/canvas/public/components/routing/routing_link.tsx b/x-pack/plugins/canvas/public/components/routing/routing_link.tsx new file mode 100644 index 0000000000000..91ffe48013e32 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/routing/routing_link.tsx @@ -0,0 +1,39 @@ +/* + * 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, MouseEventHandler } from 'react'; +import { EuiLink, EuiLinkProps, EuiButtonIcon, EuiButtonIconProps } from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; + +interface RoutingProps { + to: string; +} +type RoutingLinkProps = Omit & RoutingProps; + +export const RoutingLink: FC = ({ to, ...rest }) => { + const history = useHistory(); + + // Generate the correct link href (with basename accounted for) + const href = history.createHref({ pathname: to }); + + const props = { ...rest, href } as EuiLinkProps; + + return ; +}; + +type RoutingButtonIconProps = Omit & RoutingProps; + +export const RoutingButtonIcon: FC = ({ to, ...rest }) => { + const history = useHistory(); + + // Generate the correct link href (with basename accounted for) + const href = history.createHref({ pathname: to }); + + const props = { ...rest, href } as EuiButtonIconProps; + + return ; +}; diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx index 6e5c936a113bf..756e3b25daef0 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx @@ -19,13 +19,15 @@ import { // @ts-expect-error untyped local import { WorkpadManager } from '../workpad_manager'; -import { RouterContext } from '../router'; import { PageManager } from '../page_manager'; import { Expression } from '../expression'; import { Tray } from './tray'; import { CanvasElement } from '../../../types'; import { ComponentStrings } from '../../../i18n'; +import { RoutingLink, RoutingButtonIcon } from '../routing'; + +import { WorkpadRoutingContext } from '../../routes/workpad'; const { Toolbar: strings } = ComponentStrings; @@ -50,7 +52,7 @@ export const Toolbar: FC = ({ }) => { const [activeTray, setActiveTray] = useState(null); const [showWorkpadManager, setShowWorkpadManager] = useState(false); - const router = useContext(RouterContext); + const { getUrl, nextPage, previousPage } = useContext(WorkpadRoutingContext); // While the tray doesn't get activated if the workpad isn't writeable, // this effect will ensure that if the tray is open and the workpad @@ -61,20 +63,6 @@ export const Toolbar: FC = ({ } }, [isWriteable, activeTray]); - if (!router) { - return
{strings.getErrorMessage('Router Undefined')}
; - } - - const nextPage = () => { - const page = Math.min(selectedPageNumber + 1, totalPages); - router.navigateTo('loadWorkpad', { id: workpadId, page }); - }; - - const previousPage = () => { - const page = Math.max(1, selectedPageNumber - 1); - router.navigateTo('loadWorkpad', { id: workpadId, page }); - }; - const elementIsSelected = Boolean(selectedElement); const toggleTray = (tray: TrayType) => { @@ -119,11 +107,11 @@ export const Toolbar: FC = ({ - @@ -133,11 +121,11 @@ export const Toolbar: FC = ({ - = totalPages} + isDisabled={selectedPageNumber >= totalPages} aria-label={strings.getNextPageAriaLabel()} /> diff --git a/x-pack/plugins/canvas/public/components/workpad/index.js b/x-pack/plugins/canvas/public/components/workpad/index.js index c24be53418754..0a9e3e176a458 100644 --- a/x-pack/plugins/canvas/public/components/workpad/index.js +++ b/x-pack/plugins/canvas/public/components/workpad/index.js @@ -4,14 +4,13 @@ * 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 { transitionsRegistry } from '../../lib/transitions_registry'; -import { undoHistory, redoHistory } from '../../state/actions/history'; import { fetchAllRenderables } from '../../state/actions/elements'; -import { setZoomScale, setFullscreen } from '../../state/actions/transient'; +import { setZoomScale } from '../../state/actions/transient'; import { getFullscreen, getZoomScale } from '../../state/selectors/app'; import { getSelectedPageIndex, @@ -22,6 +21,7 @@ import { 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 { Workpad as Component } from './workpad'; const mapStateToProps = (state) => { @@ -40,11 +40,8 @@ const mapStateToProps = (state) => { }; const mapDispatchToProps = { - undoHistory, - redoHistory, fetchAllRenderables, setZoomScale, - setFullscreen, }; const mergeProps = (stateProps, dispatchProps, ownProps) => { @@ -52,19 +49,38 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { ...ownProps, ...stateProps, ...dispatchProps, - setFullscreen: (value) => { - dispatchProps.setFullscreen(value); + }; +}; - if (value === true) { +const AddContexts = (props) => { + const { isFullscreen, setFullscreen, undo, redo, autoplayInterval } = useContext( + WorkpadRoutingContext + ); + + const setFullscreenWithEffect = useCallback( + (fullscreen) => { + setFullscreen(fullscreen); + if (fullscreen === true) { trackCanvasUiMetric( METRIC_TYPE.COUNT, - stateProps.autoplayEnabled + autoplayInterval > 0 ? [LAUNCHED_FULLSCREEN, LAUNCHED_FULLSCREEN_AUTOPLAY] : LAUNCHED_FULLSCREEN ); } }, - }; + [setFullscreen, autoplayInterval] + ); + + return ( + + ); }; export const Workpad = compose( @@ -119,4 +135,4 @@ export const Workpad = compose( }, }), withHandlers(zoomHandlerCreators) -)(Component); +)(AddContexts); diff --git a/x-pack/plugins/canvas/public/components/workpad/workpad.js b/x-pack/plugins/canvas/public/components/workpad/workpad.js index 8c5b095c7b105..1e46558c7a377 100644 --- a/x-pack/plugins/canvas/public/components/workpad/workpad.js +++ b/x-pack/plugins/canvas/public/components/workpad/workpad.js @@ -123,14 +123,6 @@ export class Workpad extends React.PureComponent { style={fsStyle} data-shared-items-count={totalElementCount} > - {isFullscreen && ( - - )} {pages.map((page, i) => (
, trackMetric); diff --git a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx b/x-pack/plugins/canvas/public/components/workpad_app/workpad_telemetry.tsx similarity index 94% rename from x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx rename to x-pack/plugins/canvas/public/components/workpad_app/workpad_telemetry.tsx index fa28b65e318b4..0915c757ff893 100644 --- a/x-pack/plugins/canvas/public/apps/workpad/workpad_app/workpad_telemetry.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_app/workpad_telemetry.tsx @@ -7,9 +7,9 @@ import React, { useState, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { trackCanvasUiMetric, METRIC_TYPE } from '../../../lib/ui_metric'; -import { getElementCounts } from '../../../state/selectors/workpad'; -import { getArgs } from '../../../state/selectors/resolved_args'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../lib/ui_metric'; +import { getElementCounts } from '../../state/selectors/workpad'; +import { getArgs } from '../../state/selectors/resolved_args'; const WorkpadLoadedMetric = 'workpad-loaded'; const WorkpadLoadedWithErrorsMetric = 'workpad-loaded-with-errors'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx similarity index 93% rename from x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.ts rename to x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx index a272c4d1aa696..68f72b8124aea 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import React, { FC, useContext } from 'react'; import { connect } from 'react-redux'; import { compose, withHandlers, withProps } from 'recompose'; import { Dispatch } from 'redux'; @@ -35,6 +36,7 @@ import { alignmentDistributionHandlerCreators, } from '../../../lib/element_handler_creators'; import { EditMenu as Component, Props as ComponentProps } from './edit_menu.component'; +import { WorkpadRoutingContext } from '../../../routes/workpad'; type LayoutState = any; @@ -102,8 +104,6 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ elementLayer: (pageId: string, elementId: string, movement: number) => { dispatch(elementLayer({ pageId, elementId, movement })); }, - undoHistory: () => dispatch(undoHistory()), - redoHistory: () => dispatch(redoHistory()), dispatch, }); @@ -123,6 +123,12 @@ const mergeProps = ( }; }; +export const EditMenuWithContext: FC = (props) => { + const { undo, redo } = useContext(WorkpadRoutingContext); + + return ; +}; + export const EditMenu = compose( connect(mapStateToProps, mapDispatchToProps, mergeProps), withProps(() => ({ hasPasteData: Boolean(getClipboardData()) })), @@ -131,4 +137,4 @@ export const EditMenu = compose( withHandlers(layerHandlerCreators), withHandlers(groupHandlerCreators), withHandlers(alignmentDistributionHandlerCreators) -)(Component); +)(EditMenuWithContext); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/fullscreen_control.tsx b/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/fullscreen_control.tsx index 83058209f7255..202a1d3b804bc 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/fullscreen_control.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/fullscreen_control/fullscreen_control.tsx @@ -21,7 +21,7 @@ interface Props { setFullscreen: (fullscreen: boolean) => void; autoplayEnabled: boolean; - enableAutoplay: (autoplay: boolean) => void; + toggleAutoplay: () => void; onPageChange: (pageNumber: number) => void; previousPage: () => void; @@ -44,14 +44,26 @@ export class FullscreenControl extends React.PureComponent { setFullscreen(!isFullscreen); }; + toggleAutoplay = () => { + this.props.toggleAutoplay(); + }; + + nextPage = () => { + this.props.nextPage(); + }; + + previousPage = () => { + this.props.previousPage(); + }; + // handle keypress events for presentation events _keyMap: { [key: string]: (...args: any[]) => void } = { REFRESH: this.props.fetchAllRenderables, - PREV: this.props.previousPage, - NEXT: this.props.nextPage, + PREV: this.previousPage, + NEXT: this.nextPage, FULLSCREEN: this._toggleFullscreen, FULLSCREEN_EXIT: this._toggleFullscreen, - PAGE_CYCLE_TOGGLE: () => this.props.enableAutoplay(!this.props.autoplayEnabled), + PAGE_CYCLE_TOGGLE: this.toggleAutoplay, }; _keyHandler = (action: string, event: KeyboardEvent) => { 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 630f7407f8a8d..e428dbba57b9b 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 @@ -5,18 +5,12 @@ * 2.0. */ -import { connect } from 'react-redux'; +import React, { useContext, useCallback } from 'react'; +import { connect, useDispatch } from 'react-redux'; import PropTypes from 'prop-types'; import { withState, withProps, withHandlers, compose, getContext } from 'recompose'; -import { setFullscreen, selectToplevelNodes } from '../../../state/actions/transient'; -import { enableAutoplay } from '../../../state/actions/workpad'; -import { getFullscreen } from '../../../state/selectors/app'; -import { - getAutoplay, - getSelectedPageIndex, - getPages, - getWorkpad, -} from '../../../state/selectors/workpad'; +import { selectToplevelNodes } from '../../../state/actions/transient'; +import { getSelectedPageIndex, getPages, getWorkpad } from '../../../state/selectors/workpad'; import { trackCanvasUiMetric, METRIC_TYPE } from '../../../lib/ui_metric'; import { LAUNCHED_FULLSCREEN, @@ -24,6 +18,7 @@ import { } from '../../../../common/lib/constants'; import { transitionsRegistry } from '../../../lib/transitions_registry'; import { fetchAllRenderables } from '../../../state/actions/elements'; +import { WorkpadRoutingContext } from '../../../routes/workpad/workpad_routing_context'; import { FullscreenControl as Component } from './fullscreen_control'; // TODO: a lot of this is borrowed code from `/components/workpad/index.js`. @@ -32,44 +27,65 @@ const mapStateToProps = (state) => ({ workpadId: getWorkpad(state).id, pages: getPages(state), selectedPageNumber: getSelectedPageIndex(state) + 1, - isFullscreen: getFullscreen(state), - autoplayEnabled: getAutoplay(state).enabled, }); const mapDispatchToProps = (dispatch) => ({ - setFullscreen: (value) => { - dispatch(setFullscreen(value)); - value && dispatch(selectToplevelNodes([])); - }, - enableAutoplay: (enabled) => dispatch(enableAutoplay(enabled)), fetchAllRenderables: () => dispatch(fetchAllRenderables()), }); -const mergeProps = (stateProps, dispatchProps, ownProps) => { - return { - ...ownProps, - ...stateProps, - ...dispatchProps, - setFullscreen: (value) => { - dispatchProps.setFullscreen(value); +export const FullscreenControlWithContext = (props) => { + const { + isFullscreen, + autoplayInterval, + nextPage, + previousPage, + setFullscreen, + setIsAutoplayPaused, + isAutoplayPaused, + } = useContext(WorkpadRoutingContext); + + const autoplayEnabled = autoplayInterval > 0 ? true : false; + const dispatch = useDispatch(); + + const setFullscreenWithEffects = useCallback( + (value) => { + value && dispatch(selectToplevelNodes([])); + setFullscreen(value); if (value === true) { trackCanvasUiMetric( METRIC_TYPE.COUNT, - stateProps.autoplayEnabled + autoplayEnabled ? [LAUNCHED_FULLSCREEN, LAUNCHED_FULLSCREEN_AUTOPLAY] : LAUNCHED_FULLSCREEN ); } }, - }; + [dispatch, setFullscreen, autoplayEnabled] + ); + + const toggleAutoplay = useCallback(() => { + setIsAutoplayPaused(!isAutoplayPaused); + }, [setIsAutoplayPaused, isAutoplayPaused]); + + return ( + + ); }; export const FullscreenControl = compose( getContext({ router: PropTypes.object, }), - connect(mapStateToProps, mapDispatchToProps, mergeProps), + connect(mapStateToProps, mapDispatchToProps), withState('transition', 'setTransition', null), withState('prevSelectedPageNumber', 'setPrevSelectedPageNumber', 0), withProps(({ selectedPageNumber, prevSelectedPageNumber, transition }) => { @@ -89,29 +105,7 @@ export const FullscreenControl = compose( 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); - }, }) -)(Component); +)(FullscreenControlWithContext); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/auto_refresh_controls.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/auto_refresh_controls.tsx index 88f383bf29c6b..1508f8683b8c1 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/auto_refresh_controls.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/auto_refresh_controls.tsx @@ -32,7 +32,7 @@ const { getSecondsText, getMinutesText, getHoursText } = timeStrings; interface Props { refreshInterval: number; - setRefresh: (interval: number | undefined) => void; + setRefresh: (interval: number) => void; disableInterval: () => void; } diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx index d054475d811c2..35729f8f25fa6 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx @@ -5,13 +5,15 @@ * 2.0. */ -import React, { ReactNode } from 'react'; +import React, { ReactNode, useCallback } from 'react'; import PropTypes from 'prop-types'; import { + EuiButtonIcon, EuiDescriptionList, EuiDescriptionListDescription, EuiDescriptionListTitle, EuiTitle, + EuiToolTip, EuiHorizontalRule, EuiLink, EuiSpacer, @@ -53,6 +55,10 @@ const ListGroup = ({ children, ...rest }: ListGroupProps) => ( const generateId = htmlIdGenerator(); export const KioskControls = ({ autoplayInterval, onSetInterval }: Props) => { + const disableAutoplay = useCallback(() => { + onSetInterval(0); + }, [onSetInterval]); + const RefreshItem = ({ duration, label, descriptionId }: RefreshItemProps) => (
  • onSetInterval(duration)} aria-describedby={descriptionId}> @@ -71,12 +77,37 @@ export const KioskControls = ({ autoplayInterval, onSetInterval }: Props) => { className="canvasViewMenu__kioskSettings" > - - {strings.getTitle()} - - {timeStrings.getCycleTimeText(interval.length, interval.format)} - - + + + + {strings.getTitle()} + + {autoplayInterval > 0 ? ( + <>{timeStrings.getCycleTimeText(interval.length, interval.format)} + ) : ( + <>{strings.getAutoplayListDurationManualText()} + )} + + + + + + + {autoplayInterval > 0 ? ( + + + + + + ) : null} + + + +

    {strings.getCycleFormLabel()}

    diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx index ddac362e9fe50..8f92db4e7f3f4 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.component.tsx @@ -76,7 +76,7 @@ export interface Props { /** * Sets auto refresh interval */ - setRefreshInterval: (interval?: number) => void; + setRefreshInterval: (interval: number) => void; /** * Is autoplay enabled? */ @@ -92,7 +92,7 @@ export interface Props { /** * Sets autoplay interval */ - setAutoplayInterval: (interval?: number) => void; + setAutoplayInterval: (interval: number) => void; } export const ViewMenu: FunctionComponent = ({ @@ -113,7 +113,7 @@ export const ViewMenu: FunctionComponent = ({ enableAutoplay, setAutoplayInterval, }) => { - const setRefresh = (val: number | undefined) => setRefreshInterval(val); + const setRefresh = (val: number) => setRefreshInterval(val); const disableInterval = () => { setRefresh(0); @@ -196,16 +196,6 @@ export const ViewMenu: FunctionComponent = ({ closePopover(); }, }, - { - name: autoplayEnabled - ? strings.getAutoplayOffMenuItemLabel() - : strings.getAutoplayOnMenuItemLabel(), - icon: autoplayEnabled ? 'stop' : 'play', - onClick: () => { - enableAutoplay(!autoplayEnabled); - closePopover(); - }, - }, { name: strings.getAutoplaySettingsMenuItemLabel(), icon: 'empty', diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.ts b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx similarity index 70% rename from x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.ts rename to x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx index cd143ff240463..350fa2dc0e20c 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx @@ -4,8 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { connect } from 'react-redux'; +import React, { FC, useCallback, useContext } from 'react'; +import { connect, useDispatch } from 'react-redux'; import { compose, withHandlers } from 'recompose'; import { Dispatch } from 'redux'; import { zoomHandlerCreators } from '../../../lib/app_handler_creators'; @@ -13,22 +13,16 @@ import { State, CanvasWorkpadBoundingBox } from '../../../../types'; // @ts-expect-error untyped local import { fetchAllRenderables } from '../../../state/actions/elements'; // @ts-expect-error untyped local -import { setZoomScale, setFullscreen, selectToplevelNodes } from '../../../state/actions/transient'; -import { - setWriteable, - setRefreshInterval, - enableAutoplay, - setAutoplayInterval, -} from '../../../state/actions/workpad'; +import { setZoomScale, selectToplevelNodes } from '../../../state/actions/transient'; +import { setWriteable, enableAutoplay } from '../../../state/actions/workpad'; import { getZoomScale, canUserWrite } from '../../../state/selectors/app'; import { getWorkpadBoundingBox, getWorkpadWidth, getWorkpadHeight, isWriteable, - getRefreshInterval, - getAutoplay, } from '../../../state/selectors/workpad'; +import { WorkpadRoutingContext } from '../../../routes/workpad'; import { ViewMenu as Component, Props as ComponentProps } from './view_menu.component'; import { getFitZoomScale } from './lib/get_fit_zoom_scale'; @@ -47,34 +41,20 @@ interface DispatchProps { } const mapStateToProps = (state: State) => { - const { enabled, interval } = getAutoplay(state); - return { zoomScale: getZoomScale(state), boundingBox: getWorkpadBoundingBox(state), workpadWidth: getWorkpadWidth(state), workpadHeight: getWorkpadHeight(state), isWriteable: isWriteable(state) && canUserWrite(state), - refreshInterval: getRefreshInterval(state), - autoplayEnabled: enabled, - autoplayInterval: interval, }; }; const mapDispatchToProps = (dispatch: Dispatch) => ({ setZoomScale: (scale: number) => dispatch(setZoomScale(scale)), setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), - setFullscreen: (value: boolean) => { - dispatch(setFullscreen(value)); - - if (value) { - dispatch(selectToplevelNodes([])); - } - }, doRefresh: () => dispatch(fetchAllRenderables()), - setRefreshInterval: (interval: number) => dispatch(setRefreshInterval(interval)), enableAutoplay: (autoplay: number) => dispatch(enableAutoplay(!!autoplay)), - setAutoplayInterval: (interval: number) => dispatch(setAutoplayInterval(interval)), }); const mergeProps = ( @@ -89,13 +69,40 @@ const mergeProps = ( ...dispatchProps, ...ownProps, toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable), - enterFullscreen: () => dispatchProps.setFullscreen(true), fitToWindow: () => dispatchProps.setZoomScale(getFitZoomScale(boundingBox, workpadWidth, workpadHeight)), }; }; +const ViewMenuWithContext: FC = (props) => { + const dispatch = useDispatch(); + const { + autoplayInterval, + setAutoplayInterval, + setFullscreen, + setRefreshInterval, + refreshInterval, + } = useContext(WorkpadRoutingContext); + + const enterFullscreen = useCallback(() => { + dispatch(selectToplevelNodes([])); + setFullscreen(true); + }, [dispatch, setFullscreen]); + + return ( + + ); +}; + export const ViewMenu = compose( connect(mapStateToProps, mapDispatchToProps, mergeProps), withHandlers(zoomHandlerCreators) -)(Component); +)(ViewMenuWithContext); diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/index.js b/x-pack/plugins/canvas/public/components/workpad_loader/index.js deleted file mode 100644 index b279000837dd5..0000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_loader/index.js +++ /dev/null @@ -1,145 +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 PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { compose, withState, getContext, withHandlers, withProps } from 'recompose'; -import moment from 'moment'; -import * as workpadService from '../../lib/workpad_service'; -import { canUserWrite } from '../../state/selectors/app'; -import { getWorkpad } from '../../state/selectors/workpad'; -import { getId } from '../../lib/get_id'; -import { downloadWorkpad } from '../../lib/download_workpad'; -import { ComponentStrings, ErrorStrings } from '../../../i18n'; -import { withServices } from '../../services'; -import { WorkpadLoader as Component } from './workpad_loader'; - -const { WorkpadLoader: strings } = ComponentStrings; -const { WorkpadLoader: errors } = ErrorStrings; - -const mapStateToProps = (state) => ({ - workpadId: getWorkpad(state).id, - canUserWrite: canUserWrite(state), -}); - -export const WorkpadLoader = compose( - getContext({ - router: PropTypes.object, - }), - connect(mapStateToProps), - withState('workpads', 'setWorkpads', null), - withServices, - withProps(({ services }) => ({ - notify: services.notify, - })), - withHandlers(({ services }) => ({ - // Workpad creation via navigation - createWorkpad: (props) => async (workpad) => { - // workpad data uploaded, create and load it - if (workpad != null) { - try { - await workpadService.create(workpad); - props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 }); - } catch (err) { - services.notify.error(err, { - title: errors.getUploadFailureErrorMessage(), - }); - } - return; - } - - props.router.navigateTo('createWorkpad'); - }, - - // Workpad search - findWorkpads: ({ setWorkpads }) => async (text) => { - try { - const workpads = await workpadService.find(text); - setWorkpads(workpads); - } catch (err) { - services.notify.error(err, { title: errors.getFindFailureErrorMessage() }); - } - }, - - // Workpad import/export methods - downloadWorkpad: () => (workpadId) => downloadWorkpad(workpadId), - - // Clone workpad given an id - cloneWorkpad: (props) => async (workpadId) => { - try { - const workpad = await workpadService.get(workpadId); - workpad.name = strings.getClonedWorkpadName(workpad.name); - workpad.id = getId('workpad'); - await workpadService.create(workpad); - props.router.navigateTo('loadWorkpad', { id: workpad.id, page: 1 }); - } catch (err) { - services.notify.error(err, { title: errors.getCloneFailureErrorMessage() }); - } - }, - - // Remove workpad given an array of id - removeWorkpads: (props) => async (workpadIds) => { - const { setWorkpads, workpads, workpadId: loadedWorkpad } = props; - - const removeWorkpads = workpadIds.map((id) => - workpadService - .remove(id) - .then(() => ({ id, err: null })) - .catch((err) => ({ - id, - err, - })) - ); - - return Promise.all(removeWorkpads).then((results) => { - let redirectHome = false; - - const [passes, errored] = results.reduce( - ([passes, errors], result) => { - if (result.id === loadedWorkpad && !result.err) { - redirectHome = true; - } - - if (result.err) { - errors.push(result.id); - } else { - passes.push(result.id); - } - - return [passes, errors]; - }, - [[], []] - ); - - const remainingWorkpads = workpads.workpads.filter(({ id }) => !passes.includes(id)); - - const workpadState = { - total: remainingWorkpads.length, - workpads: remainingWorkpads, - }; - - if (errored.length > 0) { - services.notify.error(errors.getDeleteFailureErrorMessage()); - } - - setWorkpads(workpadState); - - if (redirectHome) { - props.router.navigateTo('home'); - } - - return errored.map(({ id }) => id); - }); - }, - })), - withProps((props) => ({ - formatDate: (date) => { - const dateFormat = props.services.platform.getUISetting('dateFormat'); - return date && moment(date).format(dateFormat); - }, - })) -)(Component); diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx b/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx new file mode 100644 index 0000000000000..fe5aae4577fa9 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx @@ -0,0 +1,167 @@ +/* + * 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, useState, useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; +import { useSelector } from 'react-redux'; +import moment from 'moment'; +// @ts-expect-error +import { getDefaultWorkpad } from '../../state/defaults'; +import { canUserWrite } from '../../state/selectors/app'; +import { getWorkpad } from '../../state/selectors/workpad'; +import { getId } from '../../lib/get_id'; +import { downloadWorkpad } from '../../lib/download_workpad'; +import { ComponentStrings, ErrorStrings } from '../../../i18n'; +import { State, CanvasWorkpad } from '../../../types'; +import { useServices } from '../../services'; +// @ts-expect-error +import { WorkpadLoader as Component } from './workpad_loader'; + +const { WorkpadLoader: strings } = ComponentStrings; +const { WorkpadLoader: errors } = ErrorStrings; + +type WorkpadStatePromise = ReturnType['workpad']['find']>; +type WorkpadState = WorkpadStatePromise extends PromiseLike ? U : never; + +export const WorkpadLoader: FC<{ onClose: () => void }> = ({ onClose }) => { + const fromState = useSelector((state: State) => ({ + workpadId: getWorkpad(state).id, + canUserWrite: canUserWrite(state), + })); + + const [workpadsState, setWorkpadsState] = useState(null); + const services = useServices(); + const history = useHistory(); + + const createWorkpad = useCallback( + async (_workpad: CanvasWorkpad | null | undefined) => { + const workpad = _workpad || getDefaultWorkpad(); + if (workpad != null) { + try { + await services.workpad.create(workpad); + history.push(`/workpad/${workpad.id}/page/1`); + } catch (err) { + services.notify.error(err, { + title: errors.getUploadFailureErrorMessage(), + }); + } + return; + } + }, + [services.workpad, services.notify, history] + ); + + const findWorkpads = useCallback( + async (text) => { + try { + const fetchedWorkpads = await services.workpad.find(text); + setWorkpadsState(fetchedWorkpads); + } catch (err) { + services.notify.error(err, { title: errors.getFindFailureErrorMessage() }); + } + }, + [services.notify, services.workpad] + ); + + const onDownloadWorkpad = useCallback((workpadId: string) => downloadWorkpad(workpadId), []); + + const cloneWorkpad = useCallback( + async (workpadId: string) => { + try { + const workpad = await services.workpad.get(workpadId); + workpad.name = strings.getClonedWorkpadName(workpad.name); + workpad.id = getId('workpad'); + await services.workpad.create(workpad); + history.push(`/workpad/${workpad.id}/page/1`); + } catch (err) { + services.notify.error(err, { title: errors.getCloneFailureErrorMessage() }); + } + }, + [services.notify, services.workpad, history] + ); + + const removeWorkpads = useCallback( + (workpadIds: string[]) => { + if (workpadsState === null) { + return; + } + + const removedWorkpads = workpadIds.map(async (id) => { + try { + await services.workpad.remove(id); + return { id, err: null }; + } catch (err) { + return { id, err }; + } + }); + + return Promise.all(removedWorkpads).then((results) => { + let redirectHome = false; + + const [passes, errored] = results.reduce<[string[], string[]]>( + ([passesArr, errorsArr], result) => { + if (result.id === fromState.workpadId && !result.err) { + redirectHome = true; + } + + if (result.err) { + errorsArr.push(result.id); + } else { + passesArr.push(result.id); + } + + return [passesArr, errorsArr]; + }, + [[], []] + ); + + const remainingWorkpads = workpadsState.workpads.filter(({ id }) => !passes.includes(id)); + + const workpadState = { + total: remainingWorkpads.length, + workpads: remainingWorkpads, + }; + + if (errored.length > 0) { + services.notify.error(errors.getDeleteFailureErrorMessage()); + } + + setWorkpadsState(workpadState); + + if (redirectHome) { + history.push('/'); + } + + return errored; + }); + }, + [history, services.workpad, fromState.workpadId, workpadsState, services.notify] + ); + + const formatDate = useCallback( + (date: any) => { + const dateFormat = services.platform.getUISetting('dateFormat'); + return date && moment(date).format(dateFormat); + }, + [services.platform] + ); + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js index 25c17fabe9fad..9c232ab43ec8d 100644 --- a/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js +++ b/x-pack/plugins/canvas/public/components/workpad_loader/workpad_loader.js @@ -22,7 +22,7 @@ import { } from '@elastic/eui'; import { orderBy } from 'lodash'; import { ConfirmModal } from '../confirm_modal'; -import { Link } from '../link'; +import { RoutingLink } from '../routing'; import { Paginate } from '../paginate'; import { ComponentStrings } from '../../../i18n'; import { WorkpadDropzone } from './workpad_dropzone'; @@ -186,14 +186,13 @@ export class WorkpadLoader extends React.PureComponent { const workpadName = getDisplayName(name, workpad, loadedWorkpad); return ( - {workpadName} - + ); }, }, diff --git a/x-pack/plugins/canvas/public/components/workpad_page/index.js b/x-pack/plugins/canvas/public/components/workpad_page/index.js index 04ceb1266f7c9..6839dc0a128ed 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/index.js +++ b/x-pack/plugins/canvas/public/components/workpad_page/index.js @@ -5,13 +5,15 @@ * 2.0. */ +import React, { useContext } from 'react'; import isEqual from 'react-fast-compare'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { branch, compose, shouldUpdate, withProps } from 'recompose'; -import { canUserWrite, getFullscreen } from '../../state/selectors/app'; +import { canUserWrite } from '../../state/selectors/app'; import { getNodes, getPageById, isWriteable } from '../../state/selectors/workpad'; import { not } from '../../lib/aeroelastic/functional'; +import { WorkpadRoutingContext } from '../../routes/workpad'; import { StaticPage } from './workpad_static_page'; import { InteractivePage } from './workpad_interactive_page'; @@ -26,19 +28,25 @@ const animationProps = ({ animation, isSelected }) => } : { className: isSelected ? 'isActive' : 'isInactive', animationStyle: {} }; -const mapStateToProps = (state, { isSelected, pageId }) => ({ - isInteractive: isSelected && !getFullscreen(state) && isWriteable(state) && canUserWrite(state), +const mapStateToProps = (state, { isSelected, pageId, isFullscreen }) => ({ + isInteractive: isSelected && !isFullscreen && isWriteable(state) && canUserWrite(state), elements: getNodes(state, pageId), pageStyle: getPageById(state, pageId).style, }); -export const WorkpadPage = compose( +export const ComposedWorkpadPage = compose( shouldUpdate(not(isEqual)), // this is critical, else random unrelated rerenders in the parent cause glitches here withProps(animationProps), connect(mapStateToProps), branch(({ isInteractive }) => isInteractive, InteractivePage, StaticPage) )(); +export const WorkpadPage = (props) => { + const { isFullscreen } = useContext(WorkpadRoutingContext); + + return ; +}; + WorkpadPage.propTypes = { pageId: PropTypes.string.isRequired, }; diff --git a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interaction_boundary.tsx b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interaction_boundary.tsx index a3ef8b03bdf1d..a15458f4c7276 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interaction_boundary.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interaction_boundary.tsx @@ -7,7 +7,7 @@ import React, { CSSProperties, PureComponent } from 'react'; // @ts-expect-error untyped local -import { WORKPAD_CONTAINER_ID } from '../../../apps/workpad/workpad_app'; +import { WORKPAD_CONTAINER_ID } from '../../workpad_app'; interface State { height: string; diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx b/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx index 91520c8c479cd..6a87b7be916ef 100644 --- a/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx @@ -5,9 +5,11 @@ * 2.0. */ -import React, { useContext, useState, useEffect, FunctionComponent } from 'react'; +import React, { useCallback, useState, useEffect, FunctionComponent } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; +import { useHistory } from 'react-router-dom'; import { RouterContext } from '../router'; + import { ComponentStrings } from '../../../i18n/components'; // @ts-expect-error import * as workpadService from '../../lib/workpad_service'; @@ -15,7 +17,7 @@ import { WorkpadTemplates as Component } from './workpad_templates'; import { CanvasTemplate } from '../../../types'; import { list } from '../../lib/template_service'; import { applyTemplateStrings } from '../../../i18n/templates/apply_strings'; -import { useNotifyService } from '../../services'; +import { useNotifyService, useServices } from '../../services'; interface WorkpadTemplatesProps { onClose: () => void; @@ -28,7 +30,9 @@ const Creating: FunctionComponent<{ name: string }> = ({ name }) => (
  • ); export const WorkpadTemplates: FunctionComponent = ({ onClose }) => { - const router = useContext(RouterContext); + const history = useHistory(); + const services = useServices(); + const [templates, setTemplates] = useState(undefined); const [creatingFromTemplateName, setCreatingFromTemplateName] = useState( undefined @@ -53,20 +57,21 @@ export const WorkpadTemplates: FunctionComponent = ({ onC }, {}); } - const createFromTemplate = async (template: CanvasTemplate) => { - setCreatingFromTemplateName(template.name); - try { - const result = await workpadService.createFromTemplate(template.id); - if (router) { - router.navigateTo('loadWorkpad', { id: result.data.id, page: 1 }); + const createFromTemplate = useCallback( + async (template: CanvasTemplate) => { + setCreatingFromTemplateName(template.name); + try { + const result = await services.workpad.createFromTemplate(template.id); + history.push(`/workpad/${result.id}/page/1`); + } catch (e) { + setCreatingFromTemplateName(undefined); + error(e, { + title: `Couldn't create workpad from template`, + }); } - } catch (e) { - setCreatingFromTemplateName(undefined); - error(e, { - title: `Couldn't create workpad from template`, - }); - } - }; + }, + [services.workpad, error, history] + ); if (creatingFromTemplateName) { return ; diff --git a/x-pack/plugins/canvas/public/lib/app_state.ts b/x-pack/plugins/canvas/public/lib/app_state.ts deleted file mode 100644 index 3b9b6b5f5419d..0000000000000 --- a/x-pack/plugins/canvas/public/lib/app_state.ts +++ /dev/null @@ -1,124 +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 { parse } from 'query-string'; -import { get } from 'lodash'; -// @ts-expect-error untyped local -import { getInitialState } from '../state/initial_state'; -import { getWindow } from './get_window'; -// @ts-expect-error untyped local -import { historyProvider } from './history_provider'; -// @ts-expect-error untyped local -import { routerProvider } from './router_provider'; -import { createTimeInterval, isValidTimeInterval, getTimeInterval } from './time_interval'; -import { AppState, AppStateKeys } from '../../types'; - -export function getDefaultAppState(): AppState { - const transientState = getInitialState('transient'); - const state: AppState = {}; - - if (transientState.fullscreen) { - state[AppStateKeys.FULLSCREEN] = true; - } - - if (transientState.refresh.interval > 0) { - state[AppStateKeys.REFRESH_INTERVAL] = createTimeInterval(transientState.refresh.interval); - } - - if (transientState.autoplay.enabled) { - state[AppStateKeys.AUTOPLAY_INTERVAL] = createTimeInterval(transientState.autoplay.interval); - } - - return state; -} - -export function getCurrentAppState(): AppState { - const history = historyProvider(getWindow()); - const { search } = history.getLocation(); - const qs = !!search ? parse(search.replace(/^\?/, ''), { sort: false }) : {}; - const appState = assignAppState({}, qs); - - return appState; -} - -export function getAppState(key?: string): AppState { - const appState = { ...getDefaultAppState(), ...getCurrentAppState() }; - return key ? get(appState, key) : appState; -} - -export function assignAppState(obj: AppState & { [key: string]: any }, appState: AppState) { - const fullscreen = appState[AppStateKeys.FULLSCREEN]; - const refreshKey = appState[AppStateKeys.REFRESH_INTERVAL]; - const autoplayKey = appState[AppStateKeys.AUTOPLAY_INTERVAL]; - - if (fullscreen) { - obj[AppStateKeys.FULLSCREEN] = true; - } else { - delete obj[AppStateKeys.FULLSCREEN]; - } - - const refresh = Array.isArray(refreshKey) ? refreshKey[0] : refreshKey; - - if (refresh && isValidTimeInterval(refresh)) { - obj[AppStateKeys.REFRESH_INTERVAL] = refresh; - } else { - delete obj[AppStateKeys.REFRESH_INTERVAL]; - } - - const autoplay = Array.isArray(autoplayKey) ? autoplayKey[0] : autoplayKey; - - if (autoplay && isValidTimeInterval(autoplay)) { - obj[AppStateKeys.AUTOPLAY_INTERVAL] = autoplay; - } else { - delete obj[AppStateKeys.AUTOPLAY_INTERVAL]; - } - - return obj; -} - -export function setFullscreen(payload: boolean) { - const appState = getAppState(); - const appValue = appState[AppStateKeys.FULLSCREEN]; - - if (payload === false && appValue) { - delete appState[AppStateKeys.FULLSCREEN]; - routerProvider().updateAppState(appState); - } else if (payload === true && !appValue) { - appState[AppStateKeys.FULLSCREEN] = true; - routerProvider().updateAppState(appState); - } -} - -export function setAutoplayInterval(payload: string | null) { - const appState = getAppState(); - const appValue = appState[AppStateKeys.AUTOPLAY_INTERVAL]; - - if (payload !== appValue) { - if (!payload && appValue) { - delete appState[AppStateKeys.AUTOPLAY_INTERVAL]; - routerProvider().updateAppState(appState); - } else if (payload) { - appState[AppStateKeys.AUTOPLAY_INTERVAL] = payload; - routerProvider().updateAppState(appState); - } - } -} - -export function setRefreshInterval(payload: string) { - const appState = getAppState(); - const appValue = appState[AppStateKeys.REFRESH_INTERVAL]; - - if (payload !== appValue) { - if (getTimeInterval(payload)) { - appState[AppStateKeys.REFRESH_INTERVAL] = payload; - routerProvider().updateAppState(appState); - } else { - delete appState[AppStateKeys.REFRESH_INTERVAL]; - routerProvider().updateAppState(appState); - } - } -} diff --git a/x-pack/plugins/canvas/public/lib/breadcrumbs.ts b/x-pack/plugins/canvas/public/lib/breadcrumbs.ts index f1dc00f777703..35a17eda8c165 100644 --- a/x-pack/plugins/canvas/public/lib/breadcrumbs.ts +++ b/x-pack/plugins/canvas/public/lib/breadcrumbs.ts @@ -6,7 +6,6 @@ */ import { ChromeBreadcrumb } from '../../../../../src/core/public'; -import { platformService } from '../services'; export const getBaseBreadcrumb = () => ({ text: 'Canvas', @@ -23,7 +22,3 @@ export const getWorkpadBreadcrumb = ({ } return output; }; - -export const setBreadcrumb = (paths: ChromeBreadcrumb | ChromeBreadcrumb[]) => { - platformService.getService().setBreadcrumbs(Array.isArray(paths) ? paths : [paths]); -}; diff --git a/x-pack/plugins/canvas/public/lib/history_provider.js b/x-pack/plugins/canvas/public/lib/history_provider.js deleted file mode 100644 index 09d1dde814560..0000000000000 --- a/x-pack/plugins/canvas/public/lib/history_provider.js +++ /dev/null @@ -1,173 +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 lzString from 'lz-string'; -import { createMemoryHistory, parsePath, createPath } from 'history'; -import createHashStateHistory from 'history-extra/dist/createHashStateHistory'; -import { getWindow } from './get_window'; - -function wrapHistoryInstance(history) { - const historyState = { - onChange: [], - prevLocation: {}, - changeUnlisten: null, - }; - - const locationFormat = (location, action, parser) => ({ - pathname: location.pathname, - hash: location.hash, - search: location.search, - state: parser(location.state), - action: action.toLowerCase(), - }); - - const wrappedHistory = { - undo() { - history.goBack(); - }, - - redo() { - history.goForward(); - }, - - go(idx) { - history.go(idx); - }, - - parse(payload) { - try { - const stateJSON = lzString.decompress(payload); - return JSON.parse(stateJSON); - } catch (e) { - return null; - } - }, - - encode(state) { - try { - const stateJSON = JSON.stringify(state); - return lzString.compress(stateJSON); - } catch (e) { - throw new Error('Could not encode state: ', e.message); - } - }, - - getLocation() { - const location = history.location; - return { - ...location, - state: this.parse(location.state), - }; - }, - - getPath(path) { - if (path != null) { - return createPath(parsePath(path)); - } - return createPath(this.getLocation()); - }, - - getFullPath(path) { - if (path != null) { - return history.createHref(parsePath(path)); - } - return history.createHref(this.getLocation()); - }, - - push(state, path) { - history.push(path || this.getPath(), this.encode(state)); - }, - - replace(state, path) { - history.replace(path || this.getPath(), this.encode(state)); - }, - - onChange(fn) { - // if no handler fn passed, do nothing - if (fn == null) { - return; - } - - // push onChange function onto listener stack and return a function to remove it - const pushedIndex = historyState.onChange.push(fn) - 1; - return (() => { - // only allow the unlisten function to be called once - let called = false; - return () => { - if (called) { - return; - } - historyState.onChange.splice(pushedIndex, 1); - called = true; - }; - })(); - }, - - resetOnChange() { - // splice to clear the onChange array, and remove listener for each fn - historyState.onChange.splice(0); - }, - - get historyInstance() { - // getter to get access to the underlying history instance - return history; - }, - }; - - // track the initial history location and create update listener - historyState.prevLocation = wrappedHistory.getLocation(); - historyState.changeUnlisten = history.listen((location, action) => { - const { prevLocation } = historyState; - const locationObj = locationFormat(location, action, wrappedHistory.parse); - const prevLocationObj = locationFormat(prevLocation, action, wrappedHistory.parse); - - // execute all listeners - historyState.onChange.forEach((fn) => fn.call(null, locationObj, prevLocationObj)); - - // track the updated location - historyState.prevLocation = wrappedHistory.getLocation(); - }); - - return wrappedHistory; -} - -const instances = new WeakMap(); - -const getHistoryInstance = (win) => { - // if no window object, use memory module - if (typeof win === 'undefined' || !win.history) { - return createMemoryHistory(); - } - return createHashStateHistory(); -}; - -export const createHistory = (win = getWindow()) => { - // create and cache wrapped history instance - const historyInstance = getHistoryInstance(win); - const wrappedInstance = wrapHistoryInstance(historyInstance); - instances.set(win, wrappedInstance); - - return wrappedInstance; -}; - -export const historyProvider = (win = getWindow()) => { - // return cached instance if one exists - const instance = instances.get(win); - if (instance) { - return instance; - } - - return createHistory(win); -}; - -export const destroyHistory = (win = getWindow()) => { - const instance = instances.get(win); - - if (instance) { - instance.resetOnChange(); - } -}; diff --git a/x-pack/plugins/canvas/public/lib/router_provider.js b/x-pack/plugins/canvas/public/lib/router_provider.js deleted file mode 100644 index 89abc52361e87..0000000000000 --- a/x-pack/plugins/canvas/public/lib/router_provider.js +++ /dev/null @@ -1,129 +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 createRouter from '@scant/router'; -import { getWindow } from './get_window'; -import { historyProvider } from './history_provider'; -import { getCurrentAppState, assignAppState } from './app_state'; -import { modifyUrl } from './modify_url'; - -// used to make this provider a singleton -let router; - -export function routerProvider(routes) { - if (router) { - return router; - } - - const baseRouter = createRouter(routes); - const history = historyProvider(getWindow()); - const componentListeners = []; - - // assume any string starting with a / is a path - const isPath = (str) => typeof str === 'string' && str.substr(0, 1) === '/'; - - // helper to get the current state in history - const getState = (name, params, state) => { - // given a path, assuming params is the state - if (isPath(name)) { - return params || history.getLocation().state; - } - return state || history.getLocation().state; - }; - - // helper to append appState to a given url path - const appendAppState = (path, appState = getCurrentAppState()) => { - const newUrl = modifyUrl(path, (parts) => { - parts.query = assignAppState(parts.query, appState); - }); - - return newUrl; - }; - - // add or replace history with new url, either from path or derived path via name and params - const updateLocation = (name, params, state, replace = false) => { - const currentState = getState(name, params, state); - const method = replace ? 'replace' : 'push'; - - // given a path, go there directly - if (isPath(name)) { - return history[method](currentState, appendAppState(name)); - } - - history[method](currentState, appendAppState(baseRouter.create(name, params))); - }; - - // our router is an extended version of the imported router - // which mixes in history methods for navigation - router = { - ...baseRouter, - execute(path = history.getPath()) { - return this.parse(path); - }, - getPath: () => history.getPath(), - getFullPath: () => history.getFullPath(), - navigateTo(name, params, state) { - updateLocation(name, params, state); - }, - redirectTo(name, params, state) { - updateLocation(name, params, state, true); - }, - updateAppState(appState, replace = true) { - const method = replace ? 'replace' : 'push'; - const newPath = appendAppState(this.getPath(), appState); - const currentState = history.getLocation().state; - history[method](currentState, newPath); - }, - onPathChange(fn) { - const execOnMatch = (location) => { - const { pathname } = location; - const match = this.match(pathname); - - if (!match) { - // TODO: show some kind of error, or redirect somewhere; maybe home? - console.error('No route found for path: ', pathname); - return; - } - - fn({ ...match, location }); - }; - - // on path changes, fire the path change handler - const unlisten = history.onChange((locationObj, prevLocationObj) => { - if ( - locationObj.pathname !== prevLocationObj.pathname || - locationObj.search !== prevLocationObj.search - ) { - execOnMatch(locationObj); - } - }); - - // keep track of all change handler removal functions, for cleanup - // TODO: clean up listeners when baseRounter.stop is called - componentListeners.push(unlisten); - - // initially fire the path change handler - execOnMatch(history.getLocation()); - - return unlisten; // return function to remove change handler - }, - stop: () => { - for (const listener of componentListeners) { - listener(); - } - }, - }; - - return router; -} - -export const stopRouter = () => { - if (router) { - router.stop(); - router = undefined; - } -}; diff --git a/x-pack/plugins/canvas/public/apps/workpad/index.ts b/x-pack/plugins/canvas/public/routes/home/home_route.tsx similarity index 55% rename from x-pack/plugins/canvas/public/apps/workpad/index.ts rename to x-pack/plugins/canvas/public/routes/home/home_route.tsx index 90ccc98bef5ec..ee331d914f0c7 100644 --- a/x-pack/plugins/canvas/public/apps/workpad/index.ts +++ b/x-pack/plugins/canvas/public/routes/home/home_route.tsx @@ -4,6 +4,12 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import React from 'react'; +import { Route } from 'react-router-dom'; +import { HomeApp } from '../../components/home_app'; -export { routes } from './routes'; -export { WorkpadApp } from './workpad_app'; +export const HomeRoute = () => ( + + + +); diff --git a/x-pack/plugins/canvas/public/components/link/index.ts b/x-pack/plugins/canvas/public/routes/home/index.tsx similarity index 89% rename from x-pack/plugins/canvas/public/components/link/index.ts rename to x-pack/plugins/canvas/public/routes/home/index.tsx index 33d8b39b4bcae..7d247c7b9b12e 100644 --- a/x-pack/plugins/canvas/public/components/link/index.ts +++ b/x-pack/plugins/canvas/public/routes/home/index.tsx @@ -5,4 +5,4 @@ * 2.0. */ -export { Link } from './link'; +export * from './home_route'; diff --git a/x-pack/plugins/canvas/public/routes/index.tsx b/x-pack/plugins/canvas/public/routes/index.tsx new file mode 100644 index 0000000000000..fd09aeae3fa9a --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/index.tsx @@ -0,0 +1,22 @@ +/* + * 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 { Router, Switch } from 'react-router-dom'; +import { History } from 'history'; +import { HomeRoute } from './home'; +import { WorkpadRoute, ExportWorkpadRoute } from './workpad'; + +export const CanvasRouter: FC<{ history: History }> = ({ history }) => ( + + + {ExportWorkpadRoute()} + {WorkpadRoute()} + {HomeRoute()} + + +); diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_autoplay_helper.test.tsx b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_autoplay_helper.test.tsx new file mode 100644 index 0000000000000..0fa347e8e0605 --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_autoplay_helper.test.tsx @@ -0,0 +1,81 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { useAutoplayHelper } from './use_autoplay_helper'; +import { WorkpadRoutingContext, WorkpadRoutingContextType } from '../workpad_routing_context'; + +const getMockedContext = (context: any) => + ({ + nextPage: jest.fn(), + isFullscreen: false, + autoplayInterval: 0, + isAutoplayPaused: false, + ...context, + } as WorkpadRoutingContextType); + +const getContextWrapper: (context: WorkpadRoutingContextType) => FC = (context) => ({ + children, +}) => {children}; + +describe('useAutoplayHelper', () => { + beforeEach(() => jest.useFakeTimers()); + test('starts the timer when fullscreen and autoplay is on', () => { + const context = getMockedContext({ + isFullscreen: true, + autoplayInterval: 1, + }); + + renderHook(useAutoplayHelper, { wrapper: getContextWrapper(context) }); + + jest.runAllTimers(); + + expect(context.nextPage).toHaveBeenCalled(); + }); + + test('stops the timer when autoplay pauses', () => { + const context = getMockedContext({ + isFullscreen: true, + autoplayInterval: 1000, + }); + + const { rerender } = renderHook(useAutoplayHelper, { wrapper: getContextWrapper(context) }); + + jest.runTimersToTime(context.autoplayInterval - 1); + + context.isAutoplayPaused = true; + + rerender(); + + jest.runAllTimers(); + + expect(context.nextPage).not.toHaveBeenCalled(); + }); + + test('starts the timer when autoplay unpauses', () => { + const context = getMockedContext({ + isFullscreen: true, + autoplayInterval: 1000, + isAutoplayPaused: true, + }); + + const { rerender } = renderHook(useAutoplayHelper, { wrapper: getContextWrapper(context) }); + + jest.runAllTimers(); + + expect(context.nextPage).not.toHaveBeenCalled(); + + context.isAutoplayPaused = false; + + rerender(); + + jest.runAllTimers(); + + expect(context.nextPage).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_autoplay_helper.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_autoplay_helper.ts new file mode 100644 index 0000000000000..5015da495e47c --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_autoplay_helper.ts @@ -0,0 +1,29 @@ +/* + * 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 { useContext, useEffect, useRef } from 'react'; +import { WorkpadRoutingContext } from '../workpad_routing_context'; + +export const useAutoplayHelper = () => { + const { nextPage, isFullscreen, autoplayInterval, isAutoplayPaused } = useContext( + WorkpadRoutingContext + ); + const timer = useRef(undefined); + + useEffect(() => { + if (timer.current || !isFullscreen || isAutoplayPaused) { + clearTimeout(timer.current); + } + + if (isFullscreen && !isAutoplayPaused && autoplayInterval > 0) { + timer.current = window.setTimeout(() => { + nextPage(); + }, autoplayInterval); + } + + return () => clearTimeout(timer.current); + }, [isFullscreen, nextPage, autoplayInterval, isAutoplayPaused]); +}; diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_fullscreen_presentation_helper.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_fullscreen_presentation_helper.ts new file mode 100644 index 0000000000000..ab26625038bc5 --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_fullscreen_presentation_helper.ts @@ -0,0 +1,31 @@ +/* + * 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 { useContext, useEffect } from 'react'; +import { useServices } from '../../../services'; +import { WorkpadRoutingContext } from '..'; + +const fullscreenClass = 'canvas-isFullscreen'; + +export const useFullscreenPresentationHelper = () => { + const { isFullscreen } = useContext(WorkpadRoutingContext); + const services = useServices(); + const { setFullscreen } = services.platform; + + useEffect(() => { + const body = document.querySelector('body'); + const bodyClassList = body!.classList; + const hasFullscreenClass = bodyClassList.contains(fullscreenClass); + + if (isFullscreen && !hasFullscreenClass) { + setFullscreen(false); + bodyClassList.add(fullscreenClass); + } else if (!isFullscreen && hasFullscreenClass) { + bodyClassList.remove(fullscreenClass); + setFullscreen(true); + } + }, [isFullscreen, setFullscreen]); +}; diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_page_sync.test.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_page_sync.test.ts new file mode 100644 index 0000000000000..6d4c99cf618fb --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_page_sync.test.ts @@ -0,0 +1,83 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { usePageSync } from './use_page_sync'; + +const mockDispatch = jest.fn(); +const mockGetParams = jest.fn(); +const mockGetState = jest.fn(); + +// Mock the hooks and actions used by the UseWorkpad hook +jest.mock('react-redux', () => ({ + useDispatch: () => mockDispatch, + useSelector: (selector: any) => selector(mockGetState()), +})); + +jest.mock('react-router-dom', () => ({ + useParams: () => mockGetParams(), +})); + +describe('usePageSync', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('dispatches page index to match the pagenumber param', () => { + const pageParam = '1'; + const state = { + persistent: { + workpad: { + page: 5, + }, + }, + }; + + mockGetParams.mockReturnValue({ pageNumber: pageParam }); + mockGetState.mockReturnValue(state); + + renderHook(() => usePageSync()); + + expect(mockDispatch).toHaveBeenCalledWith({ type: 'setPage', payload: 0 }); + }); + + test('no dispatch if pageNumber matches page index', () => { + const pageParam = '6'; // Page number 6 is index 5 + const state = { + persistent: { + workpad: { + page: 5, + }, + }, + }; + + mockGetParams.mockReturnValue({ pageNumber: pageParam }); + mockGetState.mockReturnValue(state); + + renderHook(() => usePageSync()); + + expect(mockDispatch).not.toHaveBeenCalled(); + }); + + test('pageNumber that is NaN does not dispatch', () => { + const pageParam = 'A'; + const state = { + persistent: { + workpad: { + page: 5, + }, + }, + }; + + mockGetParams.mockReturnValue({ pageNumber: pageParam }); + mockGetState.mockReturnValue(state); + + renderHook(() => usePageSync()); + + expect(mockDispatch).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_page_sync.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_page_sync.ts new file mode 100644 index 0000000000000..e54e4d5065564 --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_page_sync.ts @@ -0,0 +1,32 @@ +/* + * 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 { useEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { useParams } from 'react-router-dom'; +import { WorkpadPageRouteParams } from '../'; +import { getWorkpad } from '../../../state/selectors/workpad'; +// @ts-expect-error +import { setPage } from '../../../state/actions/pages'; + +export const usePageSync = () => { + const params = useParams(); + const workpad = useSelector(getWorkpad); + const dispatch = useDispatch(); + + const pageNumber = parseInt(params.pageNumber, 10); + let pageIndex = workpad.page; + if (!isNaN(pageNumber)) { + pageIndex = pageNumber - 1; + } + + useEffect(() => { + if (pageIndex !== workpad.page) { + dispatch(setPage(pageIndex)); + } + }, [pageIndex, workpad.page, dispatch]); +}; diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_refresh_helper.test.tsx b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_refresh_helper.test.tsx new file mode 100644 index 0000000000000..59c4821d82a72 --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_refresh_helper.test.tsx @@ -0,0 +1,84 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { useRefreshHelper } from './use_refresh_helper'; +import { WorkpadRoutingContext, WorkpadRoutingContextType } from '../workpad_routing_context'; + +const mockDispatch = jest.fn(); +const mockGetState = jest.fn(); +const refreshAction = { type: 'fetchAllRenderables' }; + +jest.mock('react-redux', () => ({ + useDispatch: () => mockDispatch, + useSelector: (selector: any) => selector(mockGetState()), +})); + +jest.mock('../../../state/actions/elements', () => ({ + fetchAllRenderables: () => refreshAction, +})); + +const getMockedContext = (context: any) => + ({ + refreshInterval: 0, + ...context, + } as WorkpadRoutingContextType); + +const getContextWrapper: (context: WorkpadRoutingContextType) => FC = (context) => ({ + children, +}) => {children}; + +describe('useRefreshHelper', () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.useFakeTimers(); + }); + + test('starts a timer to refresh', () => { + const context = getMockedContext({ + refreshInterval: 1, + }); + const state = { + transient: { + inFlight: false, + }, + }; + + mockGetState.mockReturnValue(state); + + renderHook(useRefreshHelper, { wrapper: getContextWrapper(context) }); + expect(mockDispatch).not.toHaveBeenCalledWith(refreshAction); + + jest.runAllTimers(); + expect(mockDispatch).toHaveBeenCalledWith(refreshAction); + }); + + test('cancels a timer when inflight is active', () => { + const context = getMockedContext({ + refreshInterval: 100, + }); + + const state = { + transient: { + inFlight: false, + }, + }; + + mockGetState.mockReturnValue(state); + const { rerender } = renderHook(useRefreshHelper, { wrapper: getContextWrapper(context) }); + + jest.runTimersToTime(context.refreshInterval - 1); + expect(mockDispatch).not.toHaveBeenCalledWith(refreshAction); + + state.transient.inFlight = true; + rerender(useRefreshHelper); + + jest.runAllTimers(); + expect(mockDispatch).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_refresh_helper.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_refresh_helper.ts new file mode 100644 index 0000000000000..e1d593644cc65 --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_refresh_helper.ts @@ -0,0 +1,36 @@ +/* + * 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 { useEffect, useContext, useRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { WorkpadRoutingContext } from '../workpad_routing_context'; +import { getInFlight } from '../../../state/selectors/resolved_args'; +// @ts-expect-error untyped local +import { fetchAllRenderables } from '../../../state/actions/elements'; + +export const useRefreshHelper = () => { + const dispatch = useDispatch(); + const { refreshInterval } = useContext(WorkpadRoutingContext); + const timer = useRef(undefined); + const inFlight = useSelector(getInFlight); + + useEffect(() => { + // We got here because inFlight or refreshInterval changed. + // Either way, we want to cancel existing refresh timer + clearTimeout(timer.current); + + if (refreshInterval > 0 && !inFlight) { + timer.current = window.setTimeout(() => { + dispatch(fetchAllRenderables()); + }, refreshInterval); + } + + return () => { + clearTimeout(timer.current); + }; + }, [inFlight, dispatch, refreshInterval]); +}; diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_restore_history.test.tsx b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_restore_history.test.tsx new file mode 100644 index 0000000000000..0504368be05ac --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_restore_history.test.tsx @@ -0,0 +1,78 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { useRestoreHistory } from './use_restore_history'; +import { encode } from '../route_state'; + +const mockDispatch = jest.fn(); +const mockGetLocation = jest.fn(); +const mockGetHistory = jest.fn(); + +const location = { state: undefined }; +const history = { action: 'POP' }; + +// Mock the hooks and actions +jest.mock('react-redux', () => ({ + useDispatch: () => mockDispatch, +})); + +jest.mock('react-router-dom', () => ({ + useLocation: () => mockGetLocation(), + useHistory: () => mockGetHistory(), +})); + +jest.mock('../../../state/actions/workpad', () => ({ + initializeWorkpad: () => ({ type: 'initialize' }), +})); + +describe('useRestoreHistory', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('dispatches nothing on initial run', () => { + mockGetLocation.mockReturnValue(location); + mockGetHistory.mockReturnValue(history); + renderHook(() => useRestoreHistory()); + + expect(mockDispatch).not.toBeCalled(); + }); + + test('dispatches nothing on a non pop event', () => { + mockGetLocation.mockReturnValue(location); + mockGetHistory.mockReturnValue({ action: 'not-pop' }); + const { rerender } = renderHook(() => useRestoreHistory()); + + expect(mockDispatch).not.toBeCalled(); + + mockGetLocation.mockReturnValue({ state: encode({ some: 'state' }) }); + rerender(); + + expect(mockDispatch).not.toBeCalled(); + }); + + test('dispatches restore history if state changes on a POP action', () => { + const oldState = { a: 'a', b: 'b' }; + const newState = { c: 'c', d: 'd' }; + + mockGetHistory.mockReturnValue(history); + mockGetLocation.mockReturnValue({ + state: encode(oldState), + }); + + const { rerender } = renderHook(() => useRestoreHistory()); + + mockGetLocation.mockReturnValue({ + state: encode(newState), + }); + + rerender(); + + expect(mockDispatch).toHaveBeenCalledWith({ type: 'restoreHistory', payload: newState }); + }); +}); diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_restore_history.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_restore_history.ts new file mode 100644 index 0000000000000..a1fe74975e2be --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_restore_history.ts @@ -0,0 +1,35 @@ +/* + * 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 { useRef, useEffect } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; +// @ts-expect-error +import { restoreHistory } from '../../../state/actions/history'; +import { initializeWorkpad } from '../../../state/actions/workpad'; +import { decode } from '../route_state'; + +export const useRestoreHistory = () => { + const history = useHistory(); + const location = useLocation(); + const dispatch = useDispatch(); + + const { state: historyState } = location; + const previousState = useRef(historyState); + const historyAction = history.action.toLowerCase(); + + useEffect(() => { + const isBrowserNav = historyAction === 'pop' && historyState != null; + if (isBrowserNav && historyState !== previousState.current) { + previousState.current = historyState; + dispatch(restoreHistory(decode(historyState))); + dispatch(initializeWorkpad()); + } + + previousState.current = historyState; + }, [dispatch, historyAction, historyState]); +}; diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_routing_context.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_routing_context.ts new file mode 100644 index 0000000000000..e8f5a17df23fa --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_routing_context.ts @@ -0,0 +1,160 @@ +/* + * 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 { useCallback, useMemo, useState } from 'react'; +import { useHistory, useParams } from 'react-router-dom'; +import { useSelector } from 'react-redux'; +import { getWorkpad } from '../../../state/selectors/workpad'; +import { WorkpadPageRouteParams, WorkpadRoutingContextType } from '..'; +import { + createTimeInterval, + isValidTimeInterval, + getTimeInterval, +} from '../../../lib/time_interval'; + +export const useRoutingContext: () => WorkpadRoutingContextType = () => { + const [isAutoplayPaused, setIsAutoplayPaused] = useState(false); + const history = useHistory(); + const { search } = history.location; + const params = useParams(); + const workpad = useSelector(getWorkpad); + const searchParams = new URLSearchParams(search); + const parsedPage = parseInt(params.pageNumber!, 10); + const pageNumber = isNaN(parsedPage) ? workpad.page + 1 : parsedPage; + const workpadPages = workpad.pages.length; + + const getUrl = useCallback( + (page: number) => `/workpad/${params.id}/page/${page}${history.location.search}`, + [params.id, history.location.search] + ); + + const gotoPage = useCallback( + (page: number) => { + history.push(getUrl(page)); + }, + [getUrl, history] + ); + + const nextPage = useCallback(() => { + let newPage = pageNumber + 1; + if (newPage > workpadPages) { + newPage = 1; + } + + gotoPage(newPage); + }, [pageNumber, workpadPages, gotoPage]); + + const previousPage = useCallback(() => { + let newPage = pageNumber - 1; + if (newPage < 1) { + newPage = workpadPages; + } + + gotoPage(newPage); + }, [pageNumber, workpadPages, gotoPage]); + + const isFullscreen = searchParams.get('__fullScreen') === 'true'; + + const autoplayValue = searchParams.get('__autoplayInterval'); + const autoplayInterval = + autoplayValue && isValidTimeInterval(autoplayValue) ? getTimeInterval(autoplayValue) || 0 : 0; + + const refreshValue = searchParams.get('__refreshInterval'); + const refreshInterval = + refreshValue && isValidTimeInterval(refreshValue) ? getTimeInterval(refreshValue) || 0 : 0; + + const setFullscreen = useCallback( + (enable: boolean) => { + const newQuery = new URLSearchParams(history.location.search); + + if (enable) { + newQuery.set('__fullScreen', 'true'); + } else { + setIsAutoplayPaused(false); + newQuery.delete('__fullScreen'); + } + + history.push(`${history.location.pathname}?${newQuery.toString()}`); + }, + [history, setIsAutoplayPaused] + ); + + const setAutoplayInterval = useCallback( + (interval: number) => { + const newQuery = new URLSearchParams(history.location.search); + + if (interval > 0) { + newQuery.set('__autoplayInterval', createTimeInterval(interval)); + } else { + newQuery.delete('__autoplayInterval'); + } + + history.push(`${history.location.pathname}?${newQuery.toString()}`); + }, + [history] + ); + + const setRefreshInterval = useCallback( + (interval: number) => { + const newQuery = new URLSearchParams(history.location.search); + + if (interval > 0) { + newQuery.set('__refreshInterval', createTimeInterval(interval)); + } else { + newQuery.delete('__refreshInterval'); + } + + history.push(`${history.location.pathname}?${newQuery.toString()}`); + }, + [history] + ); + + const undo = useCallback(() => { + history.goBack(); + }, [history]); + + const redo = useCallback(() => { + history.goForward(); + }, [history]); + + const getRoutingContext = useCallback( + () => ({ + gotoPage, + getUrl, + isFullscreen, + setFullscreen, + autoplayInterval, + setAutoplayInterval, + nextPage, + previousPage, + refreshInterval, + setRefreshInterval, + isAutoplayPaused, + setIsAutoplayPaused, + undo, + redo, + }), + [ + gotoPage, + getUrl, + isFullscreen, + setFullscreen, + autoplayInterval, + setAutoplayInterval, + nextPage, + previousPage, + refreshInterval, + setRefreshInterval, + isAutoplayPaused, + setIsAutoplayPaused, + undo, + redo, + ] + ); + + return useMemo(() => getRoutingContext(), [getRoutingContext]); +}; diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.test.tsx b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.test.tsx new file mode 100644 index 0000000000000..e77b878359d11 --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.test.tsx @@ -0,0 +1,67 @@ +/* + * 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 { waitFor } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import { useWorkpad } from './use_workpad'; + +const mockDispatch = jest.fn(); +const mockSelector = jest.fn(); +const mockGetWorkpad = jest.fn(); + +const workpad = { + id: 'someworkpad', + pages: [], +}; + +const assets = [{ id: 'asset-id' }]; + +const workpadResponse = { + ...workpad, + assets, +}; + +// Mock the hooks and actions used by the UseWorkpad hook +jest.mock('react-redux', () => ({ + useDispatch: () => mockDispatch, + useSelector: () => mockSelector, +})); + +jest.mock('../../../services', () => ({ + useServices: () => ({ + workpad: { + get: mockGetWorkpad, + }, + }), +})); + +jest.mock('../../../state/actions/workpad', () => ({ + setWorkpad: (payload: any) => ({ + type: 'setWorkpad', + payload, + }), +})); + +describe('useWorkpad', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('fires request to load workpad and dispatches results', async () => { + const workpadId = 'someworkpad'; + mockGetWorkpad.mockResolvedValue(workpadResponse); + + renderHook(() => useWorkpad(workpadId)); + + await waitFor(() => expect(mockGetWorkpad).toHaveBeenCalledWith(workpadId)); + + expect(mockGetWorkpad).toHaveBeenCalledWith(workpadId); + expect(mockDispatch).toHaveBeenCalledWith({ type: 'setAssets', payload: assets }); + expect(mockDispatch).toHaveBeenCalledWith({ type: 'setWorkpad', payload: workpad }); + expect(mockDispatch).toHaveBeenCalledWith({ type: 'setZoomScale', payload: 1 }); + }); +}); diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts new file mode 100644 index 0000000000000..29b869b46e416 --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts @@ -0,0 +1,42 @@ +/* + * 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 { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useServices } from '../../../services'; +import { getWorkpad } from '../../../state/selectors/workpad'; +import { setWorkpad } from '../../../state/actions/workpad'; +// @ts-expect-error +import { setAssets } from '../../../state/actions/assets'; +// @ts-expect-error +import { setZoomScale } from '../../../state/actions/transient'; +import { CanvasWorkpad } from '../../../../types'; + +export const useWorkpad = ( + workpadId: string, + loadPages: boolean = true +): [CanvasWorkpad | undefined, string | Error | undefined] => { + const services = useServices(); + const dispatch = useDispatch(); + const storedWorkpad = useSelector(getWorkpad); + const [error, setError] = useState(undefined); + + useEffect(() => { + (async () => { + try { + const { assets, ...workpad } = await services.workpad.get(workpadId); + dispatch(setAssets(assets)); + dispatch(setWorkpad(workpad, { loadPages })); + dispatch(setZoomScale(1)); + } catch (e) { + setError(e); + } + })(); + }, [workpadId, services.workpad, dispatch, setError, loadPages]); + + return [storedWorkpad.id === workpadId ? storedWorkpad : undefined, error]; +}; diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts new file mode 100644 index 0000000000000..b5b9c038cfd2d --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { useWorkpadHistory } from './use_workpad_history'; +import { encode } from '../route_state'; + +const mockGetState = jest.fn(); +const mockGetHistory = jest.fn(); + +// Mock the hooks and actions used by the UseWorkpad hook +jest.mock('react-router-dom', () => ({ + useHistory: () => mockGetHistory(), +})); + +jest.mock('react-redux', () => ({ + useSelector: (selector: any) => selector(mockGetState()), +})); + +describe('useRestoreHistory', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + test('replaces undefined state with current state', () => { + const history = { + location: { + state: undefined, + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; + + const state = { + persistent: { some: 'state' }, + }; + + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + renderHook(() => useWorkpadHistory()); + + expect(history.replace).toBeCalledWith(history.location.pathname, encode(state.persistent)); + }); + + test('does not do a push on initial render if states do not match', () => { + const history = { + location: { + state: encode({ old: 'state' }), + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; + + const state = { + persistent: { some: 'state' }, + }; + + mockGetState.mockReturnValue(state); + mockGetHistory.mockReturnValue(history); + + renderHook(() => useWorkpadHistory()); + + expect(history.push).not.toBeCalled(); + }); + + test('rerender does a push if location state does not match store state', () => { + const history = { + location: { + state: encode({ old: 'state' }), + pathname: 'somepath', + }, + push: jest.fn(), + replace: jest.fn(), + }; + + const oldState = { + persistent: { some: 'state' }, + }; + + const newState = { + persistent: { new: 'state' }, + }; + + mockGetState.mockReturnValue(oldState); + mockGetHistory.mockReturnValue(history); + + const { rerender } = renderHook(() => useWorkpadHistory()); + + mockGetState.mockReturnValue(newState); + rerender(); + + expect(history.push).toBeCalledWith(history.location.pathname, encode(newState.persistent)); + }); +}); diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts new file mode 100644 index 0000000000000..1f563f7147330 --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad_history.ts @@ -0,0 +1,41 @@ +/* + * 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 { useEffect, useRef } from 'react'; +import { useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import { isEqual } from 'lodash'; +import { createPath } from 'history'; +import { encode, decode } from '../route_state'; +import { State } from '../../../../types'; + +export const useWorkpadHistory = () => { + const history = useHistory(); + const historyState = useSelector((state: State) => state.persistent); + const hasRun = useRef(false); + + useEffect(() => { + const isInitialRun = !hasRun.current; + const locationState = history.location.state; + const decodedState = locationState ? decode(locationState) : {}; + const doesStateMatchLocationState = isEqual(historyState, decodedState); + const fullPath = createPath(history.location); + + hasRun.current = true; + + // If there is no location state, then let's replace the curent route with the location state + // This will happen when navigating directly to a url (there will be no state on that link click) + if (locationState === undefined) { + history.replace(fullPath, encode(historyState)); + } else if (!doesStateMatchLocationState && !isInitialRun) { + // There was a state change here + + // If the state of the route that we are on does not match this new state, then we are going to push + history.push(fullPath, encode(historyState)); + } + }, [history, historyState]); +}; diff --git a/x-pack/plugins/canvas/public/routes/workpad/index.tsx b/x-pack/plugins/canvas/public/routes/workpad/index.tsx new file mode 100644 index 0000000000000..4c98511baad0b --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/index.tsx @@ -0,0 +1,23 @@ +/* + * 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 { RouteComponentProps } from 'react-router-dom'; + +export { WorkpadRoute, ExportWorkpadRoute } from './workpad_route'; + +export { WorkpadRoutingContext, WorkpadRoutingContextType } from './workpad_routing_context'; + +export interface WorkpadRouteParams { + id: string; +} + +export interface WorkpadPageRouteParams extends WorkpadRouteParams { + pageNumber: string; +} + +export type WorkpadRouteProps = RouteComponentProps; +export type WorkpadPageRouteProps = RouteComponentProps; diff --git a/x-pack/plugins/canvas/public/routes/workpad/route_state.ts b/x-pack/plugins/canvas/public/routes/workpad/route_state.ts new file mode 100644 index 0000000000000..c224af8c3123b --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/route_state.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +// @ts-expect-error +import lzString from 'lz-string'; + +export const encode = (state: any) => { + try { + const stateJSON = JSON.stringify(state); + return lzString.compress(stateJSON); + } catch (e) { + throw new Error(`Could not encode state: ${e.message}`); + } +}; + +export const decode = (payload: string) => { + try { + const stateJSON = lzString.decompress(payload); + return JSON.parse(stateJSON); + } catch (e) { + return null; + } +}; diff --git a/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx b/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx new file mode 100644 index 0000000000000..cecb8a376c242 --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx @@ -0,0 +1,38 @@ +/* + * 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, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { getBaseBreadcrumb, getWorkpadBreadcrumb } from '../../lib/breadcrumbs'; +// @ts-expect-error +import { setDocTitle } from '../../lib/doc_title'; +import { getWorkpad } from '../../state/selectors/workpad'; +import { useFullscreenPresentationHelper } from './hooks/use_fullscreen_presentation_helper'; +import { useAutoplayHelper } from './hooks/use_autoplay_helper'; +import { useRefreshHelper } from './hooks/use_refresh_helper'; +import { useServices } from '../../services'; + +export const WorkpadPresentationHelper: FC = ({ children }) => { + const services = useServices(); + const workpad = useSelector(getWorkpad); + useFullscreenPresentationHelper(); + useAutoplayHelper(); + useRefreshHelper(); + + useEffect(() => { + services.platform.setBreadcrumbs([ + getBaseBreadcrumb(), + getWorkpadBreadcrumb({ name: workpad.name, id: workpad.id }), + ]); + }, [workpad.name, workpad.id, services.platform]); + + useEffect(() => { + setDocTitle(workpad.name); + }, [workpad.name]); + + return <>{children}; +}; diff --git a/x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx b/x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx new file mode 100644 index 0000000000000..f302fa4f4d623 --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx @@ -0,0 +1,120 @@ +/* + * 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, useEffect } from 'react'; +import { Route, Switch, Redirect, useParams } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; +import { WorkpadApp } from '../../components/workpad_app'; +import { ExportApp } from '../../components/export_app'; +import { CanvasLoading } from '../../components/canvas_loading'; +// @ts-expect-error +import { fetchAllRenderables } from '../../state/actions/elements'; +import { useServices } from '../../services'; +import { CanvasWorkpad } from '../../../types'; +import { ErrorStrings } from '../../../i18n'; +import { useWorkpad } from './hooks/use_workpad'; +import { useRestoreHistory } from './hooks/use_restore_history'; +import { useWorkpadHistory } from './hooks/use_workpad_history'; +import { usePageSync } from './hooks/use_page_sync'; +import { WorkpadPageRouteProps, WorkpadRouteProps, WorkpadPageRouteParams } from '.'; +import { WorkpadRoutingContextComponent } from './workpad_routing_context'; +import { WorkpadPresentationHelper } from './workpad_presentation_helper'; + +const { workpadRoutes: strings } = ErrorStrings; + +export const WorkpadRoute = () => ( + { + return [ + + {(workpad: CanvasWorkpad) => ( + + ( + + + + + + + + )} + /> + + + + + )} + , + ]; + }} + /> +); + +export const ExportWorkpadRoute = () => ( + ( + + {() => ( + + + + )} + + )} + /> +); + +export const ExportRouteManager: FC = ({ children }) => { + const params = useParams(); + usePageSync(); + + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(fetchAllRenderables({ onlyActivePage: true })); + }, [dispatch, params.pageNumber]); + + return <>{children}; +}; + +export const WorkpadHistoryManager: FC = ({ children }) => { + useRestoreHistory(); + useWorkpadHistory(); + usePageSync(); + + return <>{children}; +}; + +const WorkpadLoaderComponent: FC<{ + params: WorkpadRouteProps['match']['params']; + loadPages?: boolean; + children: (workpad: CanvasWorkpad) => JSX.Element; +}> = ({ params, children, loadPages }) => { + const [workpad, error] = useWorkpad(params.id, loadPages); + const services = useServices(); + + useEffect(() => { + if (error) { + services.notify.error(error, { title: strings.getLoadFailureErrorMessage() }); + } + }, [error, services.notify]); + + if (error) { + return ; + } + + if (!workpad) { + return ; + } + + return children(workpad); +}; diff --git a/x-pack/plugins/canvas/public/routes/workpad/workpad_routing_context.tsx b/x-pack/plugins/canvas/public/routes/workpad/workpad_routing_context.tsx new file mode 100644 index 0000000000000..fe90a9e325b7d --- /dev/null +++ b/x-pack/plugins/canvas/public/routes/workpad/workpad_routing_context.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, createContext } from 'react'; +import { useRoutingContext } from './hooks/use_routing_context'; + +export interface WorkpadRoutingContextType { + gotoPage: (page: number) => void; + getUrl: (page: number) => string; + isFullscreen: boolean; + setFullscreen: (fullscreen: boolean) => void; + autoplayInterval: number; + setAutoplayInterval: (interval: number) => void; + isAutoplayPaused: boolean; + setIsAutoplayPaused: (isPaused: boolean) => void; + nextPage: () => void; + previousPage: () => void; + refreshInterval: number; + setRefreshInterval: (interval: number) => void; + undo: () => void; + redo: () => void; +} + +const basicWorkpadRoutingContext = { + gotoPage: (page: number) => undefined, + getUrl: (page: number) => '', + isFullscreen: false, + setFullscreen: (fullscreen: boolean) => undefined, + autoplayInterval: 0, + setAutoplayInterval: (interval: number) => undefined, + isAutoplayPaused: true, + setIsAutoplayPaused: (isPaused: boolean) => undefined, + nextPage: () => undefined, + previousPage: () => undefined, + refreshInterval: 0, + setRefreshInterval: (interval: number) => undefined, + undo: () => undefined, + redo: () => undefined, +}; + +export const WorkpadRoutingContext = createContext( + basicWorkpadRoutingContext +); + +export const WorkpadRoutingContextComponent: FC = ({ children }) => { + const routingContext = useRoutingContext(); + + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/canvas/public/services/context.tsx b/x-pack/plugins/canvas/public/services/context.tsx index e078efe18b542..15885c01f4137 100644 --- a/x-pack/plugins/canvas/public/services/context.tsx +++ b/x-pack/plugins/canvas/public/services/context.tsx @@ -58,6 +58,7 @@ export const ServicesProvider: FC<{ search: specifiedProviders.search.getService(), reporting: specifiedProviders.reporting.getService(), labs: specifiedProviders.labs.getService(), + workpad: specifiedProviders.workpad.getService(), }; return {children}; }; diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index cbe7de43eff95..6c039660c64c7 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -16,6 +16,7 @@ import { expressionsServiceFactory } from './expressions'; import { searchServiceFactory } from './search'; import { labsServiceFactory } from './labs'; import { reportingServiceFactory } from './reporting'; +import { workpadServiceFactory } from './workpad'; export { NotifyService } from './notify'; export { SearchService } from './search'; @@ -85,6 +86,7 @@ export const services = { search: new CanvasServiceProvider(searchServiceFactory), reporting: new CanvasServiceProvider(reportingServiceFactory), labs: new CanvasServiceProvider(labsServiceFactory), + workpad: new CanvasServiceProvider(workpadServiceFactory), }; export type CanvasServiceProviders = typeof services; @@ -98,6 +100,7 @@ export interface CanvasServices { search: ServiceFromProvider; reporting: ServiceFromProvider; labs: ServiceFromProvider; + workpad: ServiceFromProvider; } export const startServices = async ( diff --git a/x-pack/plugins/canvas/public/services/notify.ts b/x-pack/plugins/canvas/public/services/notify.ts index a65d9fe02a9c2..6ee5eec6291ab 100644 --- a/x-pack/plugins/canvas/public/services/notify.ts +++ b/x-pack/plugins/canvas/public/services/notify.ts @@ -12,7 +12,8 @@ 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 errBody = get(err, 'body', undefined); + const errMsg = formatMsg(errBody !== undefined ? err : errData); const { title, ...rest } = opts; let text; diff --git a/x-pack/plugins/canvas/public/services/stubs/index.ts b/x-pack/plugins/canvas/public/services/stubs/index.ts index 7246a34d7f491..3b00e0e6195f3 100644 --- a/x-pack/plugins/canvas/public/services/stubs/index.ts +++ b/x-pack/plugins/canvas/public/services/stubs/index.ts @@ -14,6 +14,7 @@ import { notifyService } from './notify'; import { labsService } from './labs'; import { platformService } from './platform'; import { searchService } from './search'; +import { workpadService } from './workpad'; export const stubs: CanvasServices = { embeddables: embeddablesService, @@ -24,6 +25,7 @@ export const stubs: CanvasServices = { platform: platformService, search: searchService, labs: labsService, + workpad: workpadService, }; export const startServices = async (providedServices: Partial = {}) => { diff --git a/x-pack/plugins/canvas/public/services/stubs/workpad.ts b/x-pack/plugins/canvas/public/services/stubs/workpad.ts new file mode 100644 index 0000000000000..857831c92a8a6 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/stubs/workpad.ts @@ -0,0 +1,21 @@ +/* + * 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 { WorkpadService } from '../workpad'; +import { CanvasWorkpad } from '../../../types'; + +export const workpadService: WorkpadService = { + get: (id: string) => Promise.resolve({} as CanvasWorkpad), + create: (workpad) => Promise.resolve({} as CanvasWorkpad), + createFromTemplate: (templateId: string) => Promise.resolve({} as CanvasWorkpad), + find: (term: string) => + Promise.resolve({ + total: 0, + workpads: [], + }), + remove: (id: string) => Promise.resolve(undefined), +}; diff --git a/x-pack/plugins/canvas/public/services/workpad.ts b/x-pack/plugins/canvas/public/services/workpad.ts new file mode 100644 index 0000000000000..8b4f7023316e1 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/workpad.ts @@ -0,0 +1,120 @@ +/* + * 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 { + API_ROUTE_WORKPAD, + API_ROUTE_WORKPAD_ASSETS, + API_ROUTE_WORKPAD_STRUCTURES, + DEFAULT_WORKPAD_CSS, +} from '../../common/lib/constants'; +import { CanvasWorkpad } from '../../types'; +import { CanvasServiceFactory } from './'; + +/* + Remove any top level keys from the workpad which will be rejected by validation +*/ +const validKeys = [ + '@created', + '@timestamp', + 'assets', + 'colors', + 'css', + 'variables', + 'height', + 'id', + 'isWriteable', + 'name', + 'page', + 'pages', + 'width', +]; + +const sanitizeWorkpad = function (workpad: CanvasWorkpad) { + const workpadKeys = Object.keys(workpad); + + for (const key of workpadKeys) { + if (!validKeys.includes(key)) { + delete (workpad as { [key: string]: any })[key]; + } + } + + return workpad; +}; + +interface WorkpadFindResponse { + total: number; + workpads: Array>; +} + +export interface WorkpadService { + get: (id: string) => Promise; + create: (workpad: CanvasWorkpad) => Promise; + createFromTemplate: (templateId: string) => Promise; + find: (term: string) => Promise; + remove: (id: string) => Promise; +} + +export const workpadServiceFactory: CanvasServiceFactory = ( + _coreSetup, + coreStart, + _setupPlugins, + startPlugins +): WorkpadService => { + const getApiPath = function () { + return coreStart.http.basePath.prepend(`${API_ROUTE_WORKPAD}`); + }; + return { + get: async (id: string) => { + const workpad = await coreStart.http.get(`${getApiPath()}/${id}`); + + return { css: DEFAULT_WORKPAD_CSS, variables: [], ...workpad }; + }, + create: (workpad: CanvasWorkpad) => { + return coreStart.http.post(getApiPath(), { + body: JSON.stringify({ + ...sanitizeWorkpad({ ...workpad }), + assets: workpad.assets || {}, + variables: workpad.variables || [], + }), + }); + }, + createFromTemplate: (templateId: string) => { + return coreStart.http.post(getApiPath(), { + body: JSON.stringify({ templateId }), + }); + }, + find: (searchTerm: string) => { + const validSearchTerm = typeof searchTerm === 'string' && searchTerm.length > 0; + + return coreStart.http.get(`${getApiPath()}/find`, { + query: { + perPage: 10000, + name: validSearchTerm ? searchTerm : '', + }, + }); + }, + remove: (id: string) => { + return coreStart.http.delete(`${getApiPath()}/${id}`); + }, + }; + + const { reporting } = startPlugins; + if (!reporting) { + // Reporting is not enabled + return { includeReporting: () => false }; + } + + if (reporting.usesUiCapabilities()) { + // Canvas has declared Reporting as a subfeature with the `generatePdf` UI Capability + return { + includeReporting: () => coreStart.application.capabilities.canvas?.generatePdf === true, + }; + } + + // Reporting is enabled as an Elasticsearch feature (Legacy/Deprecated) + return { includeReporting: () => true }; +}; diff --git a/x-pack/plugins/canvas/public/state/actions/pages.js b/x-pack/plugins/canvas/public/state/actions/pages.js index 64910e7b8a4a1..478fa0f52df65 100644 --- a/x-pack/plugins/canvas/public/state/actions/pages.js +++ b/x-pack/plugins/canvas/public/state/actions/pages.js @@ -9,7 +9,11 @@ import { createAction } from 'redux-actions'; export const addPage = createAction('addPage'); export const duplicatePage = createAction('duplicatePage'); -export const movePage = createAction('movePage', (id, position) => ({ id, position })); +export const movePage = createAction('movePage', (id, position, gotoPage) => ({ + id, + position, + gotoPage, +})); export const removePage = createAction('removePage'); export const stylePage = createAction('stylePage', (pageId, style) => ({ pageId, style })); export const setPage = createAction('setPage'); diff --git a/x-pack/plugins/canvas/public/state/middleware/app_ready.js b/x-pack/plugins/canvas/public/state/middleware/app_ready.js deleted file mode 100644 index c18896710bd0e..0000000000000 --- a/x-pack/plugins/canvas/public/state/middleware/app_ready.js +++ /dev/null @@ -1,27 +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 { isAppReady } from '../selectors/app'; -import { appReady as readyAction } from '../actions/app'; - -export const appReady = ({ dispatch, getState }) => (next) => (action) => { - // execute the action - next(action); - - // read the new state - const state = getState(); - - // if app is already ready, there's nothing more to do here - if (state.app.ready) { - return; - } - - // check for all conditions in the state that indicate that the app is ready - if (isAppReady(state)) { - dispatch(readyAction()); - } -}; diff --git a/x-pack/plugins/canvas/public/state/middleware/breadcrumbs.js b/x-pack/plugins/canvas/public/state/middleware/breadcrumbs.js deleted file mode 100644 index 820396fb87ec0..0000000000000 --- a/x-pack/plugins/canvas/public/state/middleware/breadcrumbs.js +++ /dev/null @@ -1,25 +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 { getWorkpad } from '../selectors/workpad'; -import { getBaseBreadcrumb, getWorkpadBreadcrumb, setBreadcrumb } from '../../lib/breadcrumbs'; - -export const breadcrumbs = ({ getState }) => (next) => (action) => { - // capture the current workpad - const currentWorkpad = getWorkpad(getState()); - - // execute the default action - next(action); - - // capture the workpad after the action completes - const updatedWorkpad = getWorkpad(getState()); - - // if the workpad name changed, update the breadcrumb data - if (currentWorkpad.name !== updatedWorkpad.name) { - setBreadcrumb([getBaseBreadcrumb(), getWorkpadBreadcrumb(updatedWorkpad)]); - } -}; diff --git a/x-pack/plugins/canvas/public/state/middleware/fullscreen.js b/x-pack/plugins/canvas/public/state/middleware/fullscreen.js deleted file mode 100644 index f40558c21f84a..0000000000000 --- a/x-pack/plugins/canvas/public/state/middleware/fullscreen.js +++ /dev/null @@ -1,23 +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 { setFullscreen } from '../../lib/fullscreen'; -import { setFullscreen as setAppStateFullscreen } from '../../lib/app_state'; -import { setFullscreen as setFullscreenAction } from '../actions/transient'; -import { getFullscreen } from '../selectors/app'; - -export const fullscreen = ({ getState }) => (next) => (action) => { - // execute the default action - next(action); - - // pass current state's fullscreen info to the fullscreen service - if (action.type === setFullscreenAction.toString()) { - const fullscreen = getFullscreen(getState()); - setFullscreen(fullscreen); - setAppStateFullscreen(fullscreen); - } -}; diff --git a/x-pack/plugins/canvas/public/state/middleware/history.js b/x-pack/plugins/canvas/public/state/middleware/history.js deleted file mode 100644 index 677f538a97294..0000000000000 --- a/x-pack/plugins/canvas/public/state/middleware/history.js +++ /dev/null @@ -1,142 +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 { isEqual } from 'lodash'; -import { routes } from '../../apps'; -import { historyProvider } from '../../lib/history_provider'; -import { routerProvider } from '../../lib/router_provider'; -import { get as fetchWorkpad } from '../../lib/workpad_service'; -import { restoreHistory, undoHistory, redoHistory } from '../actions/history'; -import { initializeWorkpad } from '../actions/workpad'; -import { setAssets } from '../actions/assets'; -import { isAppReady } from '../selectors/app'; -import { getWorkpad } from '../selectors/workpad'; - -function getHistoryState(state) { - // this is what gets written to browser history - return state.persistent; -} - -export const historyMiddleware = ({ dispatch, getState }) => { - // iterate over routes, injecting redux to action handlers - const reduxInject = (routes) => { - return routes.map((route) => { - if (route.children) { - return { - ...route, - children: reduxInject(route.children), - }; - } - - if (!route.action) { - return route; - } - - return { - ...route, - action: route.action(dispatch, getState), - }; - }); - }; - - const handlerState = { - pendingCount: 0, - }; - - // wrap up the application route actions in redux - const router = routerProvider(reduxInject(routes)); - const history = historyProvider(); - - // wire up history change handler (this only happens once) - const handleHistoryChanges = async (location, prevLocation) => { - const { pathname, state: historyState, action: historyAction } = location; - // pop state will fire on any hash-based url change, but only back/forward will have state - const isBrowserNav = historyAction === 'pop' && historyState != null; - const isUrlChange = - (!isBrowserNav && historyAction === 'pop') || - ((historyAction === 'push' || historyAction === 'replace') && - prevLocation.pathname !== pathname); - - // only restore the history on popState events with state - // this only happens when using back/forward with popState objects - if (isBrowserNav) { - // TODO: oof, this sucks. we can't just shove assets into history state because - // firefox is limited to 640k (wat!). so, when we see that the workpad id is changing, - // we instead just restore the assets, which ensures the overall state is correct. - // there must be a better way to handle history though... - const currentWorkpadId = getWorkpad(getState()).id; - if (currentWorkpadId !== historyState.workpad.id) { - const newWorkpad = await fetchWorkpad(historyState.workpad.id); - dispatch(setAssets(newWorkpad.assets)); - } - - return dispatch(restoreHistory(historyState)); - } - - // execute route action on pushState and popState events - if (isUrlChange) { - return await router.parse(pathname); - } - }; - - history.onChange(async (...args) => { - // use history replace until any async handlers are completed - handlerState.pendingCount += 1; - - try { - await handleHistoryChanges(...args); - } catch (e) { - // TODO: handle errors here - } finally { - // restore default history method - handlerState.pendingCount -= 1; - } - }); - - return (next) => (action) => { - const oldState = getState(); - - // deal with history actions - switch (action.type) { - case undoHistory.toString(): - return history.undo(); - case redoHistory.toString(): - return history.redo(); - case restoreHistory.toString(): - // skip state compare, simply execute the action - next(action); - // TODO: we shouldn't need to reset the entire workpad for undo/redo - dispatch(initializeWorkpad()); - return; - } - - // execute the action like normal - next(action); - const newState = getState(); - - // if the app is not ready, don't persist anything - if (!isAppReady(newState)) { - return; - } - - // if app switched from not ready to ready, replace current state - // this allows the back button to work correctly all the way to first page load - if (!isAppReady(oldState) && isAppReady(newState)) { - history.replace(getHistoryState(newState)); - return; - } - - // if the persistent state changed, push it into the history - const oldHistoryState = getHistoryState(oldState); - const historyState = getHistoryState(newState); - if (!isEqual(historyState, oldHistoryState)) { - // if there are pending route changes, just replace current route (to avoid extra back/forth history entries) - const useReplaceState = handlerState.pendingCount !== 0; - useReplaceState ? history.replace(historyState) : history.push(historyState); - } - }; -}; diff --git a/x-pack/plugins/canvas/public/state/middleware/index.js b/x-pack/plugins/canvas/public/state/middleware/index.js index 1aeaaa6bcf907..713232543fab1 100644 --- a/x-pack/plugins/canvas/public/state/middleware/index.js +++ b/x-pack/plugins/canvas/public/state/middleware/index.js @@ -8,15 +8,9 @@ import { applyMiddleware, compose as reduxCompose } from 'redux'; import thunkMiddleware from 'redux-thunk'; import { getWindow } from '../../lib/get_window'; -import { breadcrumbs } from './breadcrumbs'; import { esPersistMiddleware } from './es_persist'; -import { fullscreen } from './fullscreen'; -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'; @@ -26,14 +20,8 @@ const middlewares = [ elementStats, resolvedArgs, esPersistMiddleware, - historyMiddleware, - breadcrumbs, - fullscreen, inFlight, - appReady, - workpadUpdate, - workpadRefresh, - workpadAutoplay + workpadUpdate ), ]; diff --git a/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.test.ts b/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.test.ts deleted file mode 100644 index 41d1d7d2777fd..0000000000000 --- a/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.test.ts +++ /dev/null @@ -1,146 +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. - */ - -jest.mock('../../lib/app_state'); -jest.mock('../../lib/router_provider'); - -import { workpadAutoplay } from './workpad_autoplay'; -import { setAutoplayInterval } from '../../lib/app_state'; -import { createTimeInterval } from '../../lib/time_interval'; -// @ts-expect-error untyped local -import { routerProvider } from '../../lib/router_provider'; - -const next = jest.fn(); -const dispatch = jest.fn(); -const getState = jest.fn(); -const routerMock = { navigateTo: jest.fn() }; -routerProvider.mockReturnValue(routerMock); - -const middleware = workpadAutoplay({ dispatch, getState })(next); - -const workpadState = { - persistent: { - workpad: { - id: 'workpad-id', - pages: ['page1', 'page2', 'page3'], - page: 0, - }, - }, -}; - -const autoplayState = { - ...workpadState, - transient: { - autoplay: { - inFlight: false, - enabled: true, - interval: 5000, - }, - fullscreen: true, - }, -}; - -const autoplayDisabledState = { - ...workpadState, - transient: { - autoplay: { - inFlight: false, - enabled: false, - interval: 5000, - }, - }, -}; - -const action = {}; - -describe('workpad autoplay middleware', () => { - beforeEach(() => { - dispatch.mockClear(); - jest.resetAllMocks(); - }); - - describe('app state', () => { - it('sets the app state to the interval from state when enabled', () => { - getState.mockReturnValue(autoplayState); - middleware(action); - - expect(setAutoplayInterval).toBeCalledWith( - createTimeInterval(autoplayState.transient.autoplay.interval) - ); - }); - - it('sets the app state to null when not enabled', () => { - getState.mockReturnValue(autoplayDisabledState); - middleware(action); - - expect(setAutoplayInterval).toBeCalledWith(null); - }); - }); - - describe('autoplay navigation', () => { - it('navigates forward after interval', () => { - jest.useFakeTimers(); - getState.mockReturnValue(autoplayState); - middleware(action); - - jest.advanceTimersByTime(autoplayState.transient.autoplay.interval + 1); - - expect(routerMock.navigateTo).toBeCalledWith('loadWorkpad', { - id: workpadState.persistent.workpad.id, - page: workpadState.persistent.workpad.page + 2, // (index + 1) + 1 more for 1 indexed page number - }); - - jest.useRealTimers(); - }); - - it('navigates from last page back to front', () => { - jest.useFakeTimers(); - const onLastPageState = { ...autoplayState }; - onLastPageState.persistent.workpad.page = onLastPageState.persistent.workpad.pages.length - 1; - - getState.mockReturnValue(autoplayState); - middleware(action); - - jest.advanceTimersByTime(autoplayState.transient.autoplay.interval + 1); - - expect(routerMock.navigateTo).toBeCalledWith('loadWorkpad', { - id: workpadState.persistent.workpad.id, - page: 1, - }); - - jest.useRealTimers(); - }); - - it('continues autoplaying', () => { - jest.useFakeTimers(); - getState.mockReturnValue(autoplayState); - middleware(action); - - jest.advanceTimersByTime(autoplayState.transient.autoplay.interval * 2 + 1); - expect(routerMock.navigateTo).toBeCalledTimes(2); - jest.useRealTimers(); - }); - - it('does not reset timer between middleware calls', () => { - jest.useFakeTimers(); - - getState.mockReturnValue(autoplayState); - middleware(action); - - // Advance until right before timeout - jest.advanceTimersByTime(autoplayState.transient.autoplay.interval - 1); - - // Run middleware again - middleware(action); - - // Advance timer - jest.advanceTimersByTime(1); - - expect(routerMock.navigateTo).toBeCalled(); - }); - }); -}); diff --git a/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.ts b/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.ts deleted file mode 100644 index ba32457aab642..0000000000000 --- a/x-pack/plugins/canvas/public/state/middleware/workpad_autoplay.ts +++ /dev/null @@ -1,94 +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 { Middleware } from 'redux'; -import { State } from '../../../types'; -import { getFullscreen } from '../selectors/app'; -import { getInFlight } from '../selectors/resolved_args'; -import { getWorkpad, getPages, getSelectedPageIndex, getAutoplay } from '../selectors/workpad'; -// @ts-expect-error untyped local -import { appUnload } from '../actions/app'; -// @ts-expect-error untyped local -import { routerProvider } from '../../lib/router_provider'; -import { setAutoplayInterval } from '../../lib/app_state'; -import { createTimeInterval } from '../../lib/time_interval'; - -export const workpadAutoplay: Middleware<{}, State> = ({ getState }) => (next) => { - let playTimeout: number | undefined; - 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 }); - } - } - - stopAutoUpdate(); - startDelayedUpdate(); - } - - function stopAutoUpdate() { - clearTimeout(playTimeout); // cancel any pending update requests - playTimeout = undefined; - } - - function startDelayedUpdate() { - if (!playTimeout) { - stopAutoUpdate(); - playTimeout = window.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; - - // update appState - if (autoplay.enabled) { - setAutoplayInterval(createTimeInterval(autoplay.interval)); - } else { - setAutoplayInterval(null); - } - - // if interval is larger than 0, start the delayed update - if (shouldPlay) { - startDelayedUpdate(); - } else { - stopAutoUpdate(); - } - - if (action.type === appUnload.toString()) { - stopAutoUpdate(); - } - }; -}; diff --git a/x-pack/plugins/canvas/public/state/middleware/workpad_refresh.test.ts b/x-pack/plugins/canvas/public/state/middleware/workpad_refresh.test.ts deleted file mode 100644 index 76a75e248efe9..0000000000000 --- a/x-pack/plugins/canvas/public/state/middleware/workpad_refresh.test.ts +++ /dev/null @@ -1,159 +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. - */ - -jest.mock('../../lib/app_state'); - -import { workpadRefresh } from './workpad_refresh'; -import { inFlightComplete } from '../actions/resolved_args'; -import { setRefreshInterval } from '../actions/workpad'; -import { setRefreshInterval as setAppStateRefreshInterval } from '../../lib/app_state'; - -import { createTimeInterval } from '../../lib/time_interval'; - -const next = jest.fn(); -const dispatch = jest.fn(); -const getState = jest.fn(); - -const middleware = workpadRefresh({ dispatch, getState })(next); - -const refreshState = { - transient: { - refresh: { - interval: 5000, - }, - }, -}; - -const noRefreshState = { - transient: { - refresh: { - interval: 0, - }, - }, -}; - -const inFlightState = { - transient: { - refresh: { - interval: 5000, - }, - inFlight: true, - }, -}; - -describe('workpad refresh middleware', () => { - beforeEach(() => { - jest.resetAllMocks(); - }); - - describe('onInflightComplete', () => { - it('refreshes if interval gt 0', () => { - jest.useFakeTimers(); - getState.mockReturnValue(refreshState); - - middleware(inFlightComplete()); - - jest.runAllTimers(); - - expect(dispatch).toHaveBeenCalled(); - }); - - it('does not reset interval if another action occurs', () => { - jest.useFakeTimers(); - getState.mockReturnValue(refreshState); - - middleware(inFlightComplete()); - - jest.advanceTimersByTime(refreshState.transient.refresh.interval - 1); - - expect(dispatch).not.toHaveBeenCalled(); - middleware(inFlightComplete()); - - jest.advanceTimersByTime(1); - - expect(dispatch).toHaveBeenCalled(); - }); - - it('does not refresh if interval is 0', () => { - jest.useFakeTimers(); - getState.mockReturnValue(noRefreshState); - - middleware(inFlightComplete()); - - jest.runAllTimers(); - expect(dispatch).not.toHaveBeenCalled(); - }); - }); - - describe('setRefreshInterval', () => { - it('does nothing if refresh interval is unchanged', () => { - getState.mockReturnValue(refreshState); - - jest.useFakeTimers(); - const interval = 1; - middleware(setRefreshInterval(interval)); - jest.runAllTimers(); - - expect(setAppStateRefreshInterval).not.toBeCalled(); - }); - - it('sets the app refresh interval', () => { - getState.mockReturnValue(noRefreshState); - next.mockImplementation(() => { - getState.mockReturnValue(refreshState); - }); - - jest.useFakeTimers(); - const interval = 1; - middleware(setRefreshInterval(interval)); - - expect(setAppStateRefreshInterval).toBeCalledWith(createTimeInterval(interval)); - jest.runAllTimers(); - }); - - it('starts a refresh for the new interval', () => { - getState.mockReturnValue(refreshState); - jest.useFakeTimers(); - - const interval = 1000; - - middleware(inFlightComplete()); - - jest.runTimersToTime(refreshState.transient.refresh.interval - 1); - expect(dispatch).not.toBeCalled(); - - getState.mockReturnValue(noRefreshState); - next.mockImplementation(() => { - getState.mockReturnValue(refreshState); - }); - middleware(setRefreshInterval(interval)); - jest.runTimersToTime(1); - - expect(dispatch).not.toBeCalled(); - - jest.runTimersToTime(interval); - expect(dispatch).toBeCalled(); - }); - }); - - describe('inFlight in progress', () => { - it('requeues the refresh when inflight is active', () => { - jest.useFakeTimers(); - getState.mockReturnValue(inFlightState); - - middleware(inFlightComplete()); - jest.runTimersToTime(refreshState.transient.refresh.interval); - - expect(dispatch).not.toBeCalled(); - - getState.mockReturnValue(refreshState); - jest.runAllTimers(); - - expect(dispatch).toBeCalled(); - }); - }); -}); diff --git a/x-pack/plugins/canvas/public/state/middleware/workpad_refresh.ts b/x-pack/plugins/canvas/public/state/middleware/workpad_refresh.ts deleted file mode 100644 index 026ac736f87b5..0000000000000 --- a/x-pack/plugins/canvas/public/state/middleware/workpad_refresh.ts +++ /dev/null @@ -1,91 +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 { Middleware } from 'redux'; -import { State } from '../../../types'; -// @ts-expect-error untyped local -import { fetchAllRenderables } from '../actions/elements'; -import { setRefreshInterval } from '../actions/workpad'; -// @ts-expect-error untyped local -import { appUnload } from '../actions/app'; -import { inFlightComplete } from '../actions/resolved_args'; -import { getInFlight } from '../selectors/resolved_args'; -import { getRefreshInterval } from '../selectors/workpad'; -import { setRefreshInterval as setAppStateRefreshInterval } from '../../lib/app_state'; -import { createTimeInterval } from '../../lib/time_interval'; - -export const workpadRefresh: Middleware<{}, State> = ({ dispatch, getState }) => (next) => { - let refreshTimeout: number | undefined; - let refreshInterval = 0; - - function updateWorkpad() { - cancelDelayedUpdate(); - - if (refreshInterval === 0) { - return; - } - - // check the request in flight status - const inFlightActive = getInFlight(getState()); - if (inFlightActive) { - // if requests are in-flight, start the refresh delay again - startDelayedUpdate(); - } else { - // update the elements on the workpad - dispatch(fetchAllRenderables()); - } - } - - function cancelDelayedUpdate() { - clearTimeout(refreshTimeout); - refreshTimeout = undefined; - } - - function startDelayedUpdate() { - if (!refreshTimeout) { - clearTimeout(refreshTimeout); // cancel any pending update requests - refreshTimeout = window.setTimeout(() => { - updateWorkpad(); - }, refreshInterval); - } - } - - return (action) => { - const previousRefreshInterval = getRefreshInterval(getState()); - next(action); - - refreshInterval = getRefreshInterval(getState()); - - // when in-flight requests are finished, update the workpad after a given delay - if (action.type === inFlightComplete.toString() && refreshInterval > 0) { - startDelayedUpdate(); - } // create new update request - - // This middleware creates or destroys an interval that will cause workpad elements to update - if ( - action.type === setRefreshInterval.toString() && - previousRefreshInterval !== refreshInterval - ) { - // update the refresh interval - refreshInterval = action.payload; - - setAppStateRefreshInterval(createTimeInterval(refreshInterval)); - - // clear any pending timeout - cancelDelayedUpdate(); - - // if interval is larger than 0, start the delayed update - if (refreshInterval > 0) { - startDelayedUpdate(); - } - } - - if (action.type === appUnload.toString()) { - cancelDelayedUpdate(); - } - }; -}; diff --git a/x-pack/plugins/canvas/public/state/reducers/pages.js b/x-pack/plugins/canvas/public/state/reducers/pages.js index bec0019486d46..78ec0addd970e 100644 --- a/x-pack/plugins/canvas/public/state/reducers/pages.js +++ b/x-pack/plugins/canvas/public/state/reducers/pages.js @@ -9,7 +9,6 @@ import { handleActions } from 'redux-actions'; import immutable from 'object-path-immutable'; import { cloneSubgraphs } from '../../lib/clone_subgraphs'; import { getId } from '../../lib/get_id'; -import { routerProvider } from '../../lib/router_provider'; import { getDefaultPage } from '../defaults'; import * as actions from '../actions/pages'; import { getSelectedPageIndex } from '../selectors/workpad'; @@ -47,20 +46,19 @@ function clonePage(page) { export const pagesReducer = handleActions( { - [actions.addPage]: (workpadState, { payload }) => { + [actions.addPage]: (workpadState, { payload: { gotoPage } }) => { const { page: activePage } = workpadState; - const withNewPage = addPage(workpadState, payload, activePage); + const withNewPage = addPage(workpadState, undefined, activePage); const newState = setPageIndex(withNewPage, activePage + 1); // changes to the page require navigation - const router = routerProvider(); - router.navigateTo('loadWorkpad', { id: newState.id, page: newState.page + 1 }); + gotoPage(newState.page + 1); return newState; }, - [actions.duplicatePage]: (workpadState, { payload }) => { - const srcPage = workpadState.pages.find((page) => page.id === payload); + [actions.duplicatePage]: (workpadState, { payload: { gotoPage, id } }) => { + const srcPage = workpadState.pages.find((page) => page.id === id); // if the page id is invalid, don't change the state if (!srcPage) { @@ -73,8 +71,7 @@ export const pagesReducer = handleActions( const newState = setPageIndex(insertedWorkpadState, newPageIndex); // changes to the page require navigation - const router = routerProvider(); - router.navigateTo('loadWorkpad', { id: newState.id, page: newPageIndex + 1 }); + gotoPage(newPageIndex + 1); return newState; }, @@ -83,7 +80,7 @@ export const pagesReducer = handleActions( return setPageIndex(workpadState, payload); }, - [actions.movePage]: (workpadState, { payload }) => { + [actions.movePage]: (workpadState, { payload: { gotoPage, ...payload } }) => { const { id, position } = payload; const pageIndex = getPageIndexById(workpadState, id); const newIndex = pageIndex + position; @@ -108,38 +105,32 @@ export const pagesReducer = handleActions( newState = set(newState, 'page', newSelectedIndex); // changes to the page require navigation - const router = routerProvider(); - router.navigateTo('loadWorkpad', { id: newState.id, page: newState.page + 1 }); + gotoPage(newState.page + 1); return newState; }, - [actions.removePage]: (workpadState, { payload }) => { + [actions.removePage]: (workpadState, { payload: { id, gotoPage } }) => { const curIndex = workpadState.page; - const delIndex = getPageIndexById(workpadState, payload); + const delIndex = getPageIndexById(workpadState, id); if (delIndex >= 0) { let newState = del(workpadState, `pages.${delIndex}`); - const router = routerProvider(); const wasSelected = curIndex === delIndex; const wasOnlyPage = newState.pages.length === 0; - const newSelectedPage = curIndex >= delIndex ? curIndex - 1 : curIndex; // if we removed the only page, create a new empty one if (wasOnlyPage) { newState = addPage(newState); } - if (wasOnlyPage || wasSelected) { - // if we removed the only page or the selected one, select the first one + if (wasOnlyPage) { newState = set(newState, 'page', 0); - } else { - // set the adjusted selected page on new state - newState = set(newState, 'page', newSelectedPage); + gotoPage(1); + } else if (wasSelected || delIndex < curIndex) { + newState = set(newState, 'page', curIndex - 1); + gotoPage(curIndex); } - // changes to the page require navigation - router.navigateTo('loadWorkpad', { id: newState.id, page: newState.page + 1 }); - return newState; } }, diff --git a/x-pack/plugins/canvas/public/style/index.scss b/x-pack/plugins/canvas/public/style/index.scss index 6c883b832737f..a79e07a7d0016 100644 --- a/x-pack/plugins/canvas/public/style/index.scss +++ b/x-pack/plugins/canvas/public/style/index.scss @@ -2,12 +2,10 @@ @import 'hackery'; @import 'main'; -// Canvas apps -@import '../apps/home/home_app/home_app'; -@import '../apps/workpad/workpad_app/workpad_app'; -@import '../apps/export/export/export_app'; - // Canvas components +@import '../components/home_app/home_app'; +@import '../components/workpad_app/workpad_app'; +@import '../components/export_app/export_app'; @import '../components/arg_add/arg_add'; @import '../components/arg_add_popover/arg_add_popover'; @import '../components/arg_form/arg_form'; diff --git a/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx index 253f7634ebf08..1d0dacac64b17 100644 --- a/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx +++ b/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx @@ -6,9 +6,8 @@ */ import React from 'react'; - -import { RouterContext } from '../../public/components/router'; +import { MemoryRouter } from 'react-router-dom'; export const routerContextDecorator = (story: Function) => ( - {} }}>{story()} + {story()} ); From ceede077463d4c23374cedede19e28ff6cc4cc72 Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Tue, 25 May 2021 16:07:46 -0400 Subject: [PATCH 2/9] Fix typescript errors --- .../public/components/fullscreen/index.tsx | 1 + .../page_manager/page_manager.component.tsx | 2 +- .../components/page_preview/page_preview.tsx | 2 +- .../components/routing/routing_link.tsx | 2 +- .../components/toolbar/toolbar.component.tsx | 5 ++-- .../workpad_header/edit_menu/edit_menu.tsx | 2 +- .../view_menu/kiosk_controls.tsx | 2 +- .../workpad_header/view_menu/view_menu.tsx | 17 ++++++++++---- .../components/workpad_templates/index.tsx | 1 - .../plugins/canvas/public/services/workpad.ts | 23 +------------------ .../canvas/public/state/actions/workpad.ts | 2 +- 11 files changed, 22 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/canvas/public/components/fullscreen/index.tsx b/x-pack/plugins/canvas/public/components/fullscreen/index.tsx index 90bed715f8d71..dbf5c378ffa1c 100644 --- a/x-pack/plugins/canvas/public/components/fullscreen/index.tsx +++ b/x-pack/plugins/canvas/public/components/fullscreen/index.tsx @@ -6,6 +6,7 @@ */ import React, { FC, useContext } from 'react'; +// @ts-expect-error import { Fullscreen as Component } from './fullscreen'; import { WorkpadRoutingContext } from '../../routes/workpad'; diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx b/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx index a9fcecceb2f37..06968d2e4be0a 100644 --- a/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx +++ b/x-pack/plugins/canvas/public/components/page_manager/page_manager.component.tsx @@ -152,7 +152,7 @@ export class PageManager extends Component { }; renderPage = (page: CanvasPage, i: number) => { - const { isWriteable, selectedPage, workpadId, workpadCSS } = this.props; + const { isWriteable, selectedPage, workpadCSS } = this.props; const pageNumber = i + 1; return ( diff --git a/x-pack/plugins/canvas/public/components/page_preview/page_preview.tsx b/x-pack/plugins/canvas/public/components/page_preview/page_preview.tsx index 15d81a2e89d80..0dde8ffdae54b 100644 --- a/x-pack/plugins/canvas/public/components/page_preview/page_preview.tsx +++ b/x-pack/plugins/canvas/public/components/page_preview/page_preview.tsx @@ -7,7 +7,7 @@ import React, { FC, useContext, useCallback } from 'react'; -import { connect, useDispatch, useSelector } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; // @ts-expect-error untyped local import * as pageActions from '../../state/actions/pages'; import { canUserWrite } from '../../state/selectors/app'; diff --git a/x-pack/plugins/canvas/public/components/routing/routing_link.tsx b/x-pack/plugins/canvas/public/components/routing/routing_link.tsx index 91ffe48013e32..773fdb1ad6cb7 100644 --- a/x-pack/plugins/canvas/public/components/routing/routing_link.tsx +++ b/x-pack/plugins/canvas/public/components/routing/routing_link.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, MouseEventHandler } from 'react'; +import React, { FC } from 'react'; import { EuiLink, EuiLinkProps, EuiButtonIcon, EuiButtonIconProps } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; diff --git a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx index 756e3b25daef0..baafbdafcc549 100644 --- a/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx +++ b/x-pack/plugins/canvas/public/components/toolbar/toolbar.component.tsx @@ -9,7 +9,6 @@ import React, { FC, useState, useContext, useEffect } from 'react'; import PropTypes from 'prop-types'; import { EuiButtonEmpty, - EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiModal, @@ -25,7 +24,7 @@ import { Tray } from './tray'; import { CanvasElement } from '../../../types'; import { ComponentStrings } from '../../../i18n'; -import { RoutingLink, RoutingButtonIcon } from '../routing'; +import { RoutingButtonIcon } from '../routing'; import { WorkpadRoutingContext } from '../../routes/workpad'; @@ -52,7 +51,7 @@ export const Toolbar: FC = ({ }) => { const [activeTray, setActiveTray] = useState(null); const [showWorkpadManager, setShowWorkpadManager] = useState(false); - const { getUrl, nextPage, previousPage } = useContext(WorkpadRoutingContext); + const { getUrl, previousPage } = useContext(WorkpadRoutingContext); // While the tray doesn't get activated if the workpad isn't writeable, // this effect will ensure that if the tray is open and the workpad diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx index 68f72b8124aea..00f4810be3608 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/edit_menu.tsx @@ -123,7 +123,7 @@ const mergeProps = ( }; }; -export const EditMenuWithContext: FC = (props) => { +export const EditMenuWithContext: FC = (props) => { const { undo, redo } = useContext(WorkpadRoutingContext); return ; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx index 35729f8f25fa6..9247b461047c6 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx @@ -32,7 +32,7 @@ const { getSecondsText, getMinutesText } = timeStrings; interface Props { autoplayInterval: number; - onSetInterval: (interval: number | undefined) => void; + onSetInterval: (interval: number) => void; } interface ListGroupProps { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx index 350fa2dc0e20c..7b9c5b767aba0 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/view_menu.tsx @@ -14,7 +14,7 @@ import { State, CanvasWorkpadBoundingBox } from '../../../../types'; import { fetchAllRenderables } from '../../../state/actions/elements'; // @ts-expect-error untyped local import { setZoomScale, selectToplevelNodes } from '../../../state/actions/transient'; -import { setWriteable, enableAutoplay } from '../../../state/actions/workpad'; +import { setWriteable } from '../../../state/actions/workpad'; import { getZoomScale, canUserWrite } from '../../../state/selectors/app'; import { getWorkpadBoundingBox, @@ -37,9 +37,17 @@ interface StateProps { interface DispatchProps { setWriteable: (isWorkpadWriteable: boolean) => void; setZoomScale: (scale: number) => void; - setFullscreen: (showFullscreen: boolean) => void; + doRefresh: () => void; } +type PropsFromContext = + | 'enterFullscreen' + | 'setAutoplayInterval' + | 'autoplayEnabled' + | 'autoplayInterval' + | 'setRefreshInterval' + | 'refreshInterval'; + const mapStateToProps = (state: State) => { return { zoomScale: getZoomScale(state), @@ -54,7 +62,6 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({ setZoomScale: (scale: number) => dispatch(setZoomScale(scale)), setWriteable: (isWorkpadWriteable: boolean) => dispatch(setWriteable(isWorkpadWriteable)), doRefresh: () => dispatch(fetchAllRenderables()), - enableAutoplay: (autoplay: number) => dispatch(enableAutoplay(!!autoplay)), }); const mergeProps = ( @@ -74,7 +81,7 @@ const mergeProps = ( }; }; -const ViewMenuWithContext: FC = (props) => { +const ViewMenuWithContext: FC> = (props) => { const dispatch = useDispatch(); const { autoplayInterval, @@ -102,7 +109,7 @@ const ViewMenuWithContext: FC = (props) => { ); }; -export const ViewMenu = compose( +export const ViewMenu = compose, {}>( connect(mapStateToProps, mapDispatchToProps, mergeProps), withHandlers(zoomHandlerCreators) )(ViewMenuWithContext); diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx b/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx index 6a87b7be916ef..7e007b1253464 100644 --- a/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_templates/index.tsx @@ -8,7 +8,6 @@ import React, { useCallback, useState, useEffect, FunctionComponent } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; -import { RouterContext } from '../router'; import { ComponentStrings } from '../../../i18n/components'; // @ts-expect-error diff --git a/x-pack/plugins/canvas/public/services/workpad.ts b/x-pack/plugins/canvas/public/services/workpad.ts index 8b4f7023316e1..ce75ba4a802b6 100644 --- a/x-pack/plugins/canvas/public/services/workpad.ts +++ b/x-pack/plugins/canvas/public/services/workpad.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { - API_ROUTE_WORKPAD, - API_ROUTE_WORKPAD_ASSETS, - API_ROUTE_WORKPAD_STRUCTURES, - DEFAULT_WORKPAD_CSS, -} from '../../common/lib/constants'; +import { API_ROUTE_WORKPAD, DEFAULT_WORKPAD_CSS } from '../../common/lib/constants'; import { CanvasWorkpad } from '../../types'; import { CanvasServiceFactory } from './'; @@ -101,20 +96,4 @@ export const workpadServiceFactory: CanvasServiceFactory = ( return coreStart.http.delete(`${getApiPath()}/${id}`); }, }; - - const { reporting } = startPlugins; - if (!reporting) { - // Reporting is not enabled - return { includeReporting: () => false }; - } - - if (reporting.usesUiCapabilities()) { - // Canvas has declared Reporting as a subfeature with the `generatePdf` UI Capability - return { - includeReporting: () => coreStart.application.capabilities.canvas?.generatePdf === true, - }; - } - - // Reporting is enabled as an Elasticsearch feature (Legacy/Deprecated) - return { includeReporting: () => true }; }; diff --git a/x-pack/plugins/canvas/public/state/actions/workpad.ts b/x-pack/plugins/canvas/public/state/actions/workpad.ts index 648aed4245fbd..675c9867d87bc 100644 --- a/x-pack/plugins/canvas/public/state/actions/workpad.ts +++ b/x-pack/plugins/canvas/public/state/actions/workpad.ts @@ -52,7 +52,7 @@ export const setWorkpad = createThunk( 'setWorkpad', ( { dispatch, type }, - workpad: CanvasWorkpad, + workpad: Omit, { loadPages = true }: { loadPages?: boolean } = {} ) => { dispatch(createAction(type)(workpad)); // set the workpad object in state From edb8340683170098516795366107385830d7c3eb Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Tue, 25 May 2021 16:23:24 -0400 Subject: [PATCH 3/9] Remove @scant/router from package.json --- package.json | 9 ++++----- yarn.lock | 12 ------------ 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 73f3e5585faf7..7ec5c7a5eee02 100644 --- a/package.json +++ b/package.json @@ -130,20 +130,21 @@ "@kbn/config": "link:bazel-bin/packages/kbn-config/npm_module", "@kbn/config-schema": "link:bazel-bin/packages/kbn-config-schema/npm_module", "@kbn/crypto": "link:bazel-bin/packages/kbn-crypto/npm_module", - "@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl/npm_module", "@kbn/i18n": "link:bazel-bin/packages/kbn-i18n/npm_module", "@kbn/interpreter": "link:packages/kbn-interpreter", "@kbn/io-ts-utils": "link:packages/kbn-io-ts-utils", "@kbn/legacy-logging": "link:bazel-bin/packages/kbn-legacy-logging/npm_module", "@kbn/logging": "link:bazel-bin/packages/kbn-logging/npm_module", + "@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl/npm_module", "@kbn/monaco": "link:packages/kbn-monaco", - "@kbn/securitysolution-list-constants": "link:bazel-bin/packages/kbn-securitysolution-list-constants/npm_module", + "@kbn/rule-data-utils": "link:packages/kbn-rule-data-utils", "@kbn/securitysolution-es-utils": "link:bazel-bin/packages/kbn-securitysolution-es-utils/npm_module", - "@kbn/securitysolution-io-ts-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-types/npm_module", "@kbn/securitysolution-io-ts-alerting-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-alerting-types/npm_module", "@kbn/securitysolution-io-ts-list-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-list-types/npm_module", + "@kbn/securitysolution-io-ts-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-types/npm_module", "@kbn/securitysolution-io-ts-utils": "link:bazel-bin/packages/kbn-securitysolution-io-ts-utils/npm_module", "@kbn/securitysolution-list-api": "link:bazel-bin/packages/kbn-securitysolution-list-api/npm_module", + "@kbn/securitysolution-list-constants": "link:bazel-bin/packages/kbn-securitysolution-list-constants/npm_module", "@kbn/securitysolution-list-hooks": "link:bazel-bin/packages/kbn-securitysolution-list-hooks/npm_module", "@kbn/securitysolution-list-utils": "link:bazel-bin/packages/kbn-securitysolution-list-utils/npm_module", "@kbn/securitysolution-utils": "link:bazel-bin/packages/kbn-securitysolution-utils/npm_module", @@ -161,7 +162,6 @@ "@mapbox/mapbox-gl-draw": "^1.2.0", "@mapbox/mapbox-gl-rtl-text": "^0.2.3", "@mapbox/vector-tile": "1.3.1", - "@scant/router": "^0.1.1", "@slack/webhook": "^5.0.4", "@turf/along": "6.0.1", "@turf/area": "6.0.1", @@ -272,7 +272,6 @@ "json-stringify-safe": "5.0.1", "jsonwebtoken": "^8.5.1", "jsts": "^1.6.2", - "@kbn/rule-data-utils": "link:packages/kbn-rule-data-utils", "kea": "^2.4.2", "leaflet": "1.5.1", "leaflet-draw": "0.4.14", diff --git a/yarn.lock b/yarn.lock index 9967cedea9fde..1646436f32df4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3607,13 +3607,6 @@ dependencies: any-observable "^0.3.0" -"@scant/router@^0.1.1": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@scant/router/-/router-0.1.1.tgz#df3c9ae1796efce02f41c95c35166d708d88ef4a" - integrity sha512-9MiMXmEVFLm7jalyyVOtCvUN5dsIF9UgZW62BRw74vxEauLmeI0BTtJBmFdD4hMDL/QfHJc8egBPm2+f4s58wg== - dependencies: - url-pattern "^1.0.3" - "@sideway/address@^4.1.0": version "4.1.0" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.0.tgz#0b301ada10ac4e0e3fa525c90615e0b61a72b78d" @@ -27978,11 +27971,6 @@ url-parse@^1.4.3, url-parse@^1.5.0: querystringify "^2.1.1" requires-port "^1.0.0" -url-pattern@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/url-pattern/-/url-pattern-1.0.3.tgz#0409292471b24f23c50d65a47931793d2b5acfc1" - integrity sha1-BAkpJHGyTyPFDWWkeTF5PStaz8E= - url-template@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21" From e811e3d067399878ec3c37e2c2dfab94c0128a23 Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Wed, 26 May 2021 09:51:59 -0400 Subject: [PATCH 4/9] Fix tests --- .../__snapshots__/export_app.test.tsx.snap | 22 +++++-------------- .../components/export_app/export_app.test.tsx | 6 ++--- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/canvas/public/components/export_app/__snapshots__/export_app.test.tsx.snap b/x-pack/plugins/canvas/public/components/export_app/__snapshots__/export_app.test.tsx.snap index 19e9000c3bffc..80a36b238b7da 100644 --- a/x-pack/plugins/canvas/public/components/export_app/__snapshots__/export_app.test.tsx.snap +++ b/x-pack/plugins/canvas/public/components/export_app/__snapshots__/export_app.test.tsx.snap @@ -38,18 +38,13 @@ exports[` renders as expected 1`] = `
    -
    Link
    - +
    renders as expected 2`] = `
    -
    Link
    - +
    ({ it: (css: string, Component: any) => Component, })); -jest.mock('../../../components/workpad_page', () => ({ +jest.mock('../workpad_page', () => ({ WorkpadPage: (props: any) =>
    Page
    , })); -jest.mock('../../../components/link', () => ({ - Link: (props: any) =>
    Link
    , +jest.mock('../routing', () => ({ + RoutingLink: (props: any) =>
    Link
    , })); describe('', () => { From 65f7f76a432b1a489ea4b7248579bb661f5783af Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Wed, 26 May 2021 12:49:44 -0400 Subject: [PATCH 5/9] Fix functional test --- .../apps/canvas/feature_controls/canvas_security.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts index 7f5f5d09f28db..cb2f3f72a8312 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts @@ -14,6 +14,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'canvas', 'error', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); const globalNav = getService('globalNav'); + const testSubjects = getService('testSubjects'); describe('security feature controls', function () { this.tags(['skipFirefox']); @@ -84,10 +85,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`allows a workpad to be created`, async () => { - await PageObjects.common.navigateToActualUrl('canvas', 'workpad/create', { - ensureCurrentUrl: true, - shouldLoginIfPrompted: false, - }); + await PageObjects.common.navigateToActualUrl('canvas'); + + await testSubjects.click('create-workpad-button'); await PageObjects.canvas.expectAddElementButton(); }); From 58dcd850aa791bc1970060be213b2d10431e83cc Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Wed, 26 May 2021 14:12:08 -0400 Subject: [PATCH 6/9] Fix functional tests --- .../apps/canvas/feature_controls/canvas_spaces.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts index 3a0c2f3fcbfaa..8c6f7d5fee3a4 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_spaces.ts @@ -13,6 +13,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const spacesService = getService('spaces'); const PageObjects = getPageObjects(['common', 'canvas', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); + const testSubjects = getService('testSubjects'); describe('spaces feature controls', function () { this.tags(['skipFirefox']); @@ -55,11 +56,13 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`allows a workpad to be created`, async () => { - await PageObjects.common.navigateToActualUrl('canvas', 'workpad/create', { + await PageObjects.common.navigateToActualUrl('canvas', '', { ensureCurrentUrl: true, shouldLoginIfPrompted: false, }); + await testSubjects.click('create-workpad-button'); + await PageObjects.canvas.expectAddElementButton(); }); @@ -103,7 +106,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it(`create new workpad returns a 404`, async () => { - await PageObjects.common.navigateToActualUrl('canvas', 'workpad/create', { + await PageObjects.common.navigateToActualUrl('canvas', '', { basePath: '/s/custom_space', ensureCurrentUrl: false, shouldLoginIfPrompted: false, From 311d7a813f450040fe6bb0cbaace1d7ad38be57e Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Fri, 28 May 2021 13:31:01 -0400 Subject: [PATCH 7/9] Fix bad merge in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9428d250b714e..36827cff49e9b 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "@kbn/legacy-logging": "link:bazel-bin/packages/kbn-legacy-logging/npm_module", "@kbn/logging": "link:bazel-bin/packages/kbn-logging/npm_module", "@kbn/mapbox-gl": "link:bazel-bin/packages/kbn-mapbox-gl/npm_module", - "@kbn/monaco": "link:packages/kbn-monaco", + "@kbn/monaco": "link:bazel-bin/packages/kbn-monaco/npm_module", "@kbn/rule-data-utils": "link:packages/kbn-rule-data-utils", "@kbn/securitysolution-es-utils": "link:bazel-bin/packages/kbn-securitysolution-es-utils/npm_module", "@kbn/securitysolution-io-ts-alerting-types": "link:bazel-bin/packages/kbn-securitysolution-io-ts-alerting-types/npm_module", From fd2e79b3e6103394233575c665a0a6a704f98927 Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Fri, 28 May 2021 16:09:11 -0400 Subject: [PATCH 8/9] Cleanup from code review comments --- x-pack/plugins/canvas/i18n/components.ts | 6 ++ .../canvas_loading.component.tsx | 7 +- .../public/components/home_app/home_app.tsx | 8 +-- .../components/page_manager/page_manager.tsx | 2 + .../components/routing/routing_link.tsx | 1 + .../canvas/public/components/workpad/index.js | 4 +- .../fullscreen_control/fullscreen_control.tsx | 6 ++ .../view_menu/kiosk_controls.tsx | 2 +- .../components/workpad_loader/index.tsx | 66 ++++++++++--------- .../public/routes/workpad/workpad_route.tsx | 48 +++++++------- .../canvas/public/services/context.tsx | 2 + 11 files changed, 89 insertions(+), 63 deletions(-) diff --git a/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts index 9b09fe809c4a7..a797a8bda061b 100644 --- a/x-pack/plugins/canvas/i18n/components.ts +++ b/x-pack/plugins/canvas/i18n/components.ts @@ -175,6 +175,12 @@ export const ComponentStrings = { defaultMessage: 'Asset thumbnail', }), }, + CanvasLoading: { + getLoadingLabel: () => + i18n.translate('xpack.canvas.canvasLoading.loadingMessage', { + defaultMessage: 'Loading', + }), + }, ColorManager: { getAddAriaLabel: () => i18n.translate('xpack.canvas.colorManager.addAriaLabel', { diff --git a/x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx b/x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx index 17d1391497f51..38e62f46c945a 100644 --- a/x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx +++ b/x-pack/plugins/canvas/public/components/canvas_loading/canvas_loading.component.tsx @@ -7,8 +7,13 @@ import React, { FC } from 'react'; import { EuiPanel, EuiLoadingChart, EuiSpacer, EuiText } from '@elastic/eui'; +import { ComponentStrings } from '../../../i18n/components'; -export const CanvasLoading: FC<{ msg?: string }> = ({ msg = 'Loading...' }) => ( +const { CanvasLoading: strings } = ComponentStrings; + +export const CanvasLoading: FC<{ msg?: string }> = ({ + msg = `${strings.getLoadingLabel()}...`, +}) => (
    diff --git a/x-pack/plugins/canvas/public/components/home_app/home_app.tsx b/x-pack/plugins/canvas/public/components/home_app/home_app.tsx index fde448a79bf1e..b288612450bf7 100644 --- a/x-pack/plugins/canvas/public/components/home_app/home_app.tsx +++ b/x-pack/plugins/canvas/public/components/home_app/home_app.tsx @@ -10,16 +10,16 @@ import { useDispatch } from 'react-redux'; import { getBaseBreadcrumb } from '../../lib/breadcrumbs'; import { resetWorkpad } from '../../state/actions/workpad'; import { HomeApp as Component } from './home_app.component'; -import { useServices } from '../../services'; +import { usePlatformService } from '../../services'; export const HomeApp = () => { - const services = useServices(); + const { setBreadcrumbs } = usePlatformService(); const dispatch = useDispatch(); const onLoad = () => dispatch(resetWorkpad()); useEffect(() => { - services.platform.setBreadcrumbs([getBaseBreadcrumb()]); - }, [services.platform]); + setBreadcrumbs([getBaseBreadcrumb()]); + }, [setBreadcrumbs]); return ; }; diff --git a/x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx b/x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx index 53aa68eb51d09..5452fe8eae295 100644 --- a/x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx +++ b/x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx @@ -32,10 +32,12 @@ export const PageManager: FC<{ onPreviousPage: () => void }> = ({ onPreviousPage dispatch, gotoPage, ]); + const onMovePage = useCallback( (id: string, position: number) => dispatch(pageActions.movePage(id, position, gotoPage)), [dispatch, gotoPage] ); + const onRemovePage = useCallback( (id: string) => dispatch(pageActions.removePage({ id, gotoPage })), [dispatch, gotoPage] diff --git a/x-pack/plugins/canvas/public/components/routing/routing_link.tsx b/x-pack/plugins/canvas/public/components/routing/routing_link.tsx index 773fdb1ad6cb7..bb3123de3fec8 100644 --- a/x-pack/plugins/canvas/public/components/routing/routing_link.tsx +++ b/x-pack/plugins/canvas/public/components/routing/routing_link.tsx @@ -12,6 +12,7 @@ import { useHistory } from 'react-router-dom'; interface RoutingProps { to: string; } + type RoutingLinkProps = Omit & RoutingProps; export const RoutingLink: FC = ({ to, ...rest }) => { diff --git a/x-pack/plugins/canvas/public/components/workpad/index.js b/x-pack/plugins/canvas/public/components/workpad/index.js index 0a9e3e176a458..2ebefa5e0e322 100644 --- a/x-pack/plugins/canvas/public/components/workpad/index.js +++ b/x-pack/plugins/canvas/public/components/workpad/index.js @@ -22,7 +22,7 @@ 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 { Workpad as Component } from './workpad'; +import { Workpad as WorkpadComponent } from './workpad'; const mapStateToProps = (state) => { const { width, height, id: workpadId, css: workpadCss } = getWorkpad(state); @@ -73,7 +73,7 @@ const AddContexts = (props) => { ); return ( - { children: PropTypes.func.isRequired, }; + /* + We need these instance functions because ReactShortcuts bind the handlers on it's mount, + but then does no rebinding if it's props change. Using these instance functions will + properly handle changes to incoming props since the instance functions are bound to the components + "this" context + */ _toggleFullscreen = () => { const { setFullscreen, isFullscreen } = this.props; setFullscreen(!isFullscreen); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx index 9247b461047c6..55373d7a3515c 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/view_menu/kiosk_controls.tsx @@ -99,7 +99,7 @@ export const KioskControls = ({ autoplayInterval, onSetInterval }: Props) => { diff --git a/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx b/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx index fe5aae4577fa9..2afd5fe70abe1 100644 --- a/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx @@ -11,30 +11,32 @@ import { useSelector } from 'react-redux'; import moment from 'moment'; // @ts-expect-error import { getDefaultWorkpad } from '../../state/defaults'; -import { canUserWrite } from '../../state/selectors/app'; +import { canUserWrite as canUserWriteSelector } from '../../state/selectors/app'; import { getWorkpad } from '../../state/selectors/workpad'; import { getId } from '../../lib/get_id'; import { downloadWorkpad } from '../../lib/download_workpad'; import { ComponentStrings, ErrorStrings } from '../../../i18n'; import { State, CanvasWorkpad } from '../../../types'; -import { useServices } from '../../services'; +import { useNotifyService, useWorkpadService, usePlatformService } from '../../services'; // @ts-expect-error import { WorkpadLoader as Component } from './workpad_loader'; const { WorkpadLoader: strings } = ComponentStrings; const { WorkpadLoader: errors } = ErrorStrings; -type WorkpadStatePromise = ReturnType['workpad']['find']>; +type WorkpadStatePromise = ReturnType['find']>; type WorkpadState = WorkpadStatePromise extends PromiseLike ? U : never; export const WorkpadLoader: FC<{ onClose: () => void }> = ({ onClose }) => { const fromState = useSelector((state: State) => ({ workpadId: getWorkpad(state).id, - canUserWrite: canUserWrite(state), + canUserWrite: canUserWriteSelector(state), })); const [workpadsState, setWorkpadsState] = useState(null); - const services = useServices(); + const workpadService = useWorkpadService(); + const notifyService = useNotifyService(); + const platformService = usePlatformService(); const history = useHistory(); const createWorkpad = useCallback( @@ -42,29 +44,29 @@ export const WorkpadLoader: FC<{ onClose: () => void }> = ({ onClose }) => { const workpad = _workpad || getDefaultWorkpad(); if (workpad != null) { try { - await services.workpad.create(workpad); + await workpadService.create(workpad); history.push(`/workpad/${workpad.id}/page/1`); } catch (err) { - services.notify.error(err, { + notifyService.error(err, { title: errors.getUploadFailureErrorMessage(), }); } return; } }, - [services.workpad, services.notify, history] + [workpadService, notifyService, history] ); const findWorkpads = useCallback( async (text) => { try { - const fetchedWorkpads = await services.workpad.find(text); + const fetchedWorkpads = await workpadService.find(text); setWorkpadsState(fetchedWorkpads); } catch (err) { - services.notify.error(err, { title: errors.getFindFailureErrorMessage() }); + notifyService.error(err, { title: errors.getFindFailureErrorMessage() }); } }, - [services.notify, services.workpad] + [notifyService, workpadService] ); const onDownloadWorkpad = useCallback((workpadId: string) => downloadWorkpad(workpadId), []); @@ -72,16 +74,16 @@ export const WorkpadLoader: FC<{ onClose: () => void }> = ({ onClose }) => { const cloneWorkpad = useCallback( async (workpadId: string) => { try { - const workpad = await services.workpad.get(workpadId); + const workpad = await workpadService.get(workpadId); workpad.name = strings.getClonedWorkpadName(workpad.name); workpad.id = getId('workpad'); - await services.workpad.create(workpad); + await workpadService.create(workpad); history.push(`/workpad/${workpad.id}/page/1`); } catch (err) { - services.notify.error(err, { title: errors.getCloneFailureErrorMessage() }); + notifyService.error(err, { title: errors.getCloneFailureErrorMessage() }); } }, - [services.notify, services.workpad, history] + [notifyService, workpadService, history] ); const removeWorkpads = useCallback( @@ -92,7 +94,7 @@ export const WorkpadLoader: FC<{ onClose: () => void }> = ({ onClose }) => { const removedWorkpads = workpadIds.map(async (id) => { try { - await services.workpad.remove(id); + await workpadService.remove(id); return { id, err: null }; } catch (err) { return { id, err }; @@ -127,7 +129,7 @@ export const WorkpadLoader: FC<{ onClose: () => void }> = ({ onClose }) => { }; if (errored.length > 0) { - services.notify.error(errors.getDeleteFailureErrorMessage()); + notifyService.error(errors.getDeleteFailureErrorMessage()); } setWorkpadsState(workpadState); @@ -139,29 +141,33 @@ export const WorkpadLoader: FC<{ onClose: () => void }> = ({ onClose }) => { return errored; }); }, - [history, services.workpad, fromState.workpadId, workpadsState, services.notify] + [history, workpadService, fromState.workpadId, workpadsState, notifyService] ); const formatDate = useCallback( (date: any) => { - const dateFormat = services.platform.getUISetting('dateFormat'); + const dateFormat = platformService.getUISetting('dateFormat'); return date && moment(date).format(dateFormat); }, - [services.platform] + [platformService] ); + const { workpadId, canUserWrite } = fromState; + return ( ); }; diff --git a/x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx b/x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx index f302fa4f4d623..7683b3413f681 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx +++ b/x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx @@ -30,31 +30,29 @@ export const WorkpadRoute = () => ( { - return [ - - {(workpad: CanvasWorkpad) => ( - - ( - - - - - - - - )} - /> - - - - - )} - , - ]; - }} + children={(route: WorkpadRouteProps) => ( + + {(workpad: CanvasWorkpad) => ( + + ( + + + + + + + + )} + /> + + + + + )} + + )} /> ); diff --git a/x-pack/plugins/canvas/public/services/context.tsx b/x-pack/plugins/canvas/public/services/context.tsx index 15885c01f4137..3a78e314b9635 100644 --- a/x-pack/plugins/canvas/public/services/context.tsx +++ b/x-pack/plugins/canvas/public/services/context.tsx @@ -26,6 +26,7 @@ const defaultContextValue = { platform: {}, navLink: {}, search: {}, + workpad: {}, }; const context = createContext(defaultContextValue as CanvasServices); @@ -37,6 +38,7 @@ export const useExpressionsService = () => useServices().expressions; export const useNotifyService = () => useServices().notify; export const useNavLinkService = () => useServices().navLink; export const useLabsService = () => useServices().labs; +export const useWorkpadService = () => useServices().workpad; export const withServices = (type: ComponentType) => { const EnhancedType: FC = (props) => From 4417680189c258ea127f6fcb8efd54ba5713039b Mon Sep 17 00:00:00 2001 From: Corey Robertson Date: Tue, 1 Jun 2021 15:45:58 -0400 Subject: [PATCH 9/9] Fix double basepath append --- x-pack/plugins/canvas/public/services/workpad.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/canvas/public/services/workpad.ts b/x-pack/plugins/canvas/public/services/workpad.ts index ce75ba4a802b6..11690ca4c0c45 100644 --- a/x-pack/plugins/canvas/public/services/workpad.ts +++ b/x-pack/plugins/canvas/public/services/workpad.ts @@ -60,7 +60,7 @@ export const workpadServiceFactory: CanvasServiceFactory = ( startPlugins ): WorkpadService => { const getApiPath = function () { - return coreStart.http.basePath.prepend(`${API_ROUTE_WORKPAD}`); + return `${API_ROUTE_WORKPAD}`; }; return { get: async (id: string) => {