diff --git a/package.json b/package.json
index e5b9ca1ef98cc6..489e9e31048b60 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:bazel-bin/packages/kbn-io-ts-utils/npm_module",
"@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:bazel-bin/packages/kbn-monaco/npm_module",
- "@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",
@@ -162,7 +163,6 @@
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mapbox/vector-tile": "1.3.1",
"@reduxjs/toolkit": "^1.5.1",
- "@scant/router": "^0.1.1",
"@slack/webhook": "^5.0.4",
"@turf/along": "6.0.1",
"@turf/area": "6.0.1",
@@ -275,7 +275,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/x-pack/plugins/canvas/i18n/components.ts b/x-pack/plugins/canvas/i18n/components.ts
index b60f8db5b25b44..a797a8bda061b6 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', {
@@ -1384,6 +1390,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 154beb6faa7b03..4163cb88d5fef4 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 2887c780f308cb..00000000000000
--- 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 78012eed2c5875..00000000000000
--- 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 13d0d7d758f6d9..00000000000000
--- 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 74defd3533ae0e..00000000000000
--- 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 8ecba5e1833434..00000000000000
--- 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 7a2c6ea5c6b73b..00000000000000
--- 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 e872f9a1a5ae54..00000000000000
--- 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 00000000000000..c2d13ea537f622
--- /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 68%
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 994367d5973c11..38e62f46c945ab 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,15 @@
* 2.0.
*/
-import React from 'react';
-import PropTypes from 'prop-types';
+import React, { FC } from 'react';
import { EuiPanel, EuiLoadingChart, EuiSpacer, EuiText } from '@elastic/eui';
+import { ComponentStrings } from '../../../i18n/components';
-export const CanvasLoading = ({ msg }) => (
+const { CanvasLoading: strings } = ComponentStrings;
+
+export const CanvasLoading: FC<{ msg?: string }> = ({
+ msg = `${strings.getLoadingLabel()}...`,
+}) => (
@@ -20,11 +24,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 898d61d935a8da..80efef1915082e 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 87%
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
index 19e9000c3bffc7..80a36b238b7da8 100644
--- 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
@@ -38,18 +38,13 @@ exports[` renders as expected 1`] = `
renders as expected 2`] = `
= ({ 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 85%
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 9f019ad70aed35..7bd85616294990 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,18 +8,18 @@
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,
}));
-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('
', () => {
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 ba3fa3dd445e8b..1b9c603657b106 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.js
deleted file mode 100644
index 9a59e142a1a34d..00000000000000
--- a/x-pack/plugins/canvas/public/components/fullscreen/index.js
+++ /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 { getFullscreen } from '../../state/selectors/app';
-import { Fullscreen as Component } from './fullscreen';
-
-const mapStateToProps = (state) => ({
- isFullscreen: getFullscreen(state),
-});
-
-export const Fullscreen = connect(mapStateToProps)(Component);
diff --git a/x-pack/plugins/canvas/public/components/fullscreen/index.tsx b/x-pack/plugins/canvas/public/components/fullscreen/index.tsx
new file mode 100644
index 00000000000000..dbf5c378ffa1c3
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/fullscreen/index.tsx
@@ -0,0 +1,18 @@
+/*
+ * 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 } from 'react';
+// @ts-expect-error
+import { Fullscreen as Component } from './fullscreen';
+
+import { WorkpadRoutingContext } from '../../routes/workpad';
+
+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 30e9fbc1537504..712b06cb39299d 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 00000000000000..b288612450bf75
--- /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 { usePlatformService } from '../../services';
+
+export const HomeApp = () => {
+ const { setBreadcrumbs } = usePlatformService();
+ const dispatch = useDispatch();
+ const onLoad = () => dispatch(resetWorkpad());
+
+ useEffect(() => {
+ setBreadcrumbs([getBaseBreadcrumb()]);
+ }, [setBreadcrumbs]);
+
+ 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 ef78b11654b23c..00000000000000
--- 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 ab455bcdaadf62..06968d2e4be0a3 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);
}
@@ -156,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 (
@@ -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 64b97fed7e930f..00000000000000
--- 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 00000000000000..5452fe8eae2957
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/page_manager/page_manager.tsx
@@ -0,0 +1,55 @@
+/*
+ * 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 c9a866f5182a94..00000000000000
--- 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 00000000000000..0dde8ffdae54b4
--- /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 { 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 273503d610d187..00000000000000
--- 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 7942fed6ae5873..00000000000000
--- 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 c68fbe9bdd98e8..00000000000000
--- 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 bfa5245af9e9a9..a87401f9521039 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 00000000000000..bb3123de3fec8e
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/routing/routing_link.tsx
@@ -0,0 +1,40 @@
+/*
+ * 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 { 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 6e5c936a113bf8..baafbdafcc549c 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,
@@ -19,13 +18,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 { RoutingButtonIcon } from '../routing';
+
+import { WorkpadRoutingContext } from '../../routes/workpad';
const { Toolbar: strings } = ComponentStrings;
@@ -50,7 +51,7 @@ export const Toolbar: FC = ({
}) => {
const [activeTray, setActiveTray] = useState(null);
const [showWorkpadManager, setShowWorkpadManager] = useState(false);
- const router = useContext(RouterContext);
+ 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
@@ -61,20 +62,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 +106,11 @@ export const Toolbar: FC = ({
-
@@ -133,11 +120,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 c24be53418754c..2ebefa5e0e3228 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,7 +21,8 @@ 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 { Workpad as Component } from './workpad';
+import { WorkpadRoutingContext } from '../../routes/workpad';
+import { Workpad as WorkpadComponent } from './workpad';
const mapStateToProps = (state) => {
const { width, height, id: workpadId, css: workpadCss } = getWorkpad(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 8c5b095c7b1052..1e46558c7a377b 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 fa28b65e318b41..0915c757ff8936 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 a272c4d1aa6969..00f4810be36084 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 83058209f72554..5c35fa75989cf5 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;
@@ -39,19 +39,37 @@ export class FullscreenControl extends React.PureComponent {
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);
};
+ 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 630f7407f8a8d3..e428dbba57b9b4 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 88f383bf29c6bf..1508f8683b8c1e 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 d054475d811c25..55373d7a3515c3 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,
@@ -30,7 +32,7 @@ const { getSecondsText, getMinutesText } = timeStrings;
interface Props {
autoplayInterval: number;
- onSetInterval: (interval: number | undefined) => void;
+ onSetInterval: (interval: number) => void;
}
interface ListGroupProps {
@@ -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 ddac362e9fe500..8f92db4e7f3f46 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 64%
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 cd143ff240463b..7b9c5b767aba0f 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 } 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';
@@ -43,38 +37,31 @@ interface StateProps {
interface DispatchProps {
setWriteable: (isWorkpadWriteable: boolean) => void;
setZoomScale: (scale: number) => void;
- setFullscreen: (showFullscreen: boolean) => void;
+ doRefresh: () => void;
}
-const mapStateToProps = (state: State) => {
- const { enabled, interval } = getAutoplay(state);
+type PropsFromContext =
+ | 'enterFullscreen'
+ | 'setAutoplayInterval'
+ | 'autoplayEnabled'
+ | 'autoplayInterval'
+ | 'setRefreshInterval'
+ | 'refreshInterval';
+const mapStateToProps = (state: 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 +76,40 @@ const mergeProps = (
...dispatchProps,
...ownProps,
toggleWriteable: () => dispatchProps.setWriteable(!stateProps.isWriteable),
- enterFullscreen: () => dispatchProps.setFullscreen(true),
fitToWindow: () =>
dispatchProps.setZoomScale(getFitZoomScale(boundingBox, workpadWidth, workpadHeight)),
};
};
-export const ViewMenu = compose(
+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 b279000837dd50..00000000000000
--- 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 00000000000000..2afd5fe70abe12
--- /dev/null
+++ b/x-pack/plugins/canvas/public/components/workpad_loader/index.tsx
@@ -0,0 +1,173 @@
+/*
+ * 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 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 { 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['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: canUserWriteSelector(state),
+ }));
+
+ const [workpadsState, setWorkpadsState] = useState(null);
+ const workpadService = useWorkpadService();
+ const notifyService = useNotifyService();
+ const platformService = usePlatformService();
+ const history = useHistory();
+
+ const createWorkpad = useCallback(
+ async (_workpad: CanvasWorkpad | null | undefined) => {
+ const workpad = _workpad || getDefaultWorkpad();
+ if (workpad != null) {
+ try {
+ await workpadService.create(workpad);
+ history.push(`/workpad/${workpad.id}/page/1`);
+ } catch (err) {
+ notifyService.error(err, {
+ title: errors.getUploadFailureErrorMessage(),
+ });
+ }
+ return;
+ }
+ },
+ [workpadService, notifyService, history]
+ );
+
+ const findWorkpads = useCallback(
+ async (text) => {
+ try {
+ const fetchedWorkpads = await workpadService.find(text);
+ setWorkpadsState(fetchedWorkpads);
+ } catch (err) {
+ notifyService.error(err, { title: errors.getFindFailureErrorMessage() });
+ }
+ },
+ [notifyService, workpadService]
+ );
+
+ const onDownloadWorkpad = useCallback((workpadId: string) => downloadWorkpad(workpadId), []);
+
+ const cloneWorkpad = useCallback(
+ async (workpadId: string) => {
+ try {
+ const workpad = await workpadService.get(workpadId);
+ workpad.name = strings.getClonedWorkpadName(workpad.name);
+ workpad.id = getId('workpad');
+ await workpadService.create(workpad);
+ history.push(`/workpad/${workpad.id}/page/1`);
+ } catch (err) {
+ notifyService.error(err, { title: errors.getCloneFailureErrorMessage() });
+ }
+ },
+ [notifyService, workpadService, history]
+ );
+
+ const removeWorkpads = useCallback(
+ (workpadIds: string[]) => {
+ if (workpadsState === null) {
+ return;
+ }
+
+ const removedWorkpads = workpadIds.map(async (id) => {
+ try {
+ await workpadService.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) {
+ notifyService.error(errors.getDeleteFailureErrorMessage());
+ }
+
+ setWorkpadsState(workpadState);
+
+ if (redirectHome) {
+ history.push('/');
+ }
+
+ return errored;
+ });
+ },
+ [history, workpadService, fromState.workpadId, workpadsState, notifyService]
+ );
+
+ const formatDate = useCallback(
+ (date: any) => {
+ const dateFormat = platformService.getUISetting('dateFormat');
+ return date && moment(date).format(dateFormat);
+ },
+ [platformService]
+ );
+
+ const { workpadId, canUserWrite } = fromState;
+
+ 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 25c17fabe9fad8..9c232ab43ec8d0 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 04ceb1266f7c96..6839dc0a128ed7 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 a3ef8b03bdf1dd..a15458f4c7276a 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 91520c8c479cd8..7e007b1253464e 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,10 @@
* 2.0.
*/
-import React, { useContext, useState, useEffect, FunctionComponent } from 'react';
+import React, { useCallback, useState, useEffect, FunctionComponent } from 'react';
import { EuiLoadingSpinner } from '@elastic/eui';
-import { RouterContext } from '../router';
+import { useHistory } from 'react-router-dom';
+
import { ComponentStrings } from '../../../i18n/components';
// @ts-expect-error
import * as workpadService from '../../lib/workpad_service';
@@ -15,7 +16,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 +29,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 +56,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 3b9b6b5f5419d1..00000000000000
--- 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 f1dc00f777703c..35a17eda8c165f 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 09d1dde8145606..00000000000000
--- 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 89abc52361e876..00000000000000
--- 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 90ccc98bef5ec2..ee331d914f0c7b 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 33d8b39b4bcae3..7d247c7b9b12e8 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 00000000000000..fd09aeae3fa9a5
--- /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 00000000000000..0fa347e8e06055
--- /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 00000000000000..5015da495e47c1
--- /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 00000000000000..ab26625038bc58
--- /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 00000000000000..6d4c99cf618fb6
--- /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 00000000000000..e54e4d50655645
--- /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 00000000000000..59c4821d82a72c
--- /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 00000000000000..e1d593644cc65c
--- /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 00000000000000..0504368be05ac9
--- /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 00000000000000..a1fe74975e2be1
--- /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 00000000000000..e8f5a17df23fa1
--- /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 00000000000000..e77b878359d110
--- /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 00000000000000..29b869b46e4162
--- /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 00000000000000..b5b9c038cfd2d8
--- /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 00000000000000..1f563f71473301
--- /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 00000000000000..4c98511baad0b7
--- /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 00000000000000..c224af8c3123b4
--- /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 00000000000000..cecb8a376c2424
--- /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 00000000000000..7683b3413f6818
--- /dev/null
+++ b/x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx
@@ -0,0 +1,118 @@
+/*
+ * 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 = () => (
+ (
+
+ {(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 00000000000000..fe90a9e325b7d6
--- /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 e078efe18b5425..3a78e314b9635c 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) =>
@@ -58,6 +60,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 cbe7de43eff951..6c039660c64c7a 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 a65d9fe02a9c2d..6ee5eec6291abe 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 7246a34d7f4916..3b00e0e6195f3d 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 00000000000000..857831c92a8a61
--- /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 00000000000000..11690ca4c0c450
--- /dev/null
+++ b/x-pack/plugins/canvas/public/services/workpad.ts
@@ -0,0 +1,99 @@
+/*
+ * 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, 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 `${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}`);
+ },
+ };
+};
diff --git a/x-pack/plugins/canvas/public/state/actions/pages.js b/x-pack/plugins/canvas/public/state/actions/pages.js
index 64910e7b8a4a1e..478fa0f52df65d 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/actions/workpad.ts b/x-pack/plugins/canvas/public/state/actions/workpad.ts
index 648aed4245fbd1..675c9867d87bc7 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
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 c18896710bd0e0..00000000000000
--- 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 820396fb87ec04..00000000000000
--- 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 f40558c21f84a8..00000000000000
--- 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 677f538a972947..00000000000000
--- 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 1aeaaa6bcf9076..713232543fab1e 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 41d1d7d2777fd5..00000000000000
--- 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 ba32457aab6425..00000000000000
--- 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 76a75e248efe9d..00000000000000
--- 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 026ac736f87b5a..00000000000000
--- 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 bec0019486d463..78ec0addd970eb 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 6c883b832737fb..a79e07a7d00168 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 253f7634ebf08a..1d0dacac64b170 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()}
);
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 7f5f5d09f28dbc..cb2f3f72a83129 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();
});
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 3a0c2f3fcbfaa2..8c6f7d5fee3a42 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,
diff --git a/yarn.lock b/yarn.lock
index 1e0f9a8821d1f1..e05dc161b0f13b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3604,13 +3604,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"
@@ -27941,11 +27934,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"