mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Canvas] Switch Canvas to use React Router (#100579)
* Switch Canvas to use React Router * Fix typescript errors * Remove @scant/router from package.json * Fix tests * Fix functional test * Fix functional tests * Fix bad merge in package.json * Cleanup from code review comments * Fix double basepath append Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
72d5b8a388
commit
b62848ce8a
107 changed files with 2071 additions and 2311 deletions
|
@ -131,20 +131,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",
|
||||
|
@ -163,7 +164,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",
|
||||
|
@ -276,7 +276,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",
|
||||
|
|
|
@ -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: () =>
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
|
@ -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);
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
];
|
|
@ -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];
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
|
@ -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 (
|
||||
<div>
|
||||
<div>{strings.getLoadErrorTitle()}</div>
|
||||
<div>{strings.getLoadErrorMessage(this.props.appState.messgae)}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.props.appState instanceof Error) {
|
||||
return this.renderError();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="canvas canvasContainer">
|
||||
<Router
|
||||
routes={routes}
|
||||
showLoading={this.props.appState.ready === false}
|
||||
loadingMessage={strings.getLoadingMessage()}
|
||||
onRouteChange={this.props.onRouteChange}
|
||||
onLoad={() => this.props.setAppReady(true)}
|
||||
onError={(err) => this.props.setAppError(err)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
49
x-pack/plugins/canvas/public/components/app/index.tsx
Normal file
49
x-pack/plugins/canvas/public/components/app/index.tsx
Normal file
|
@ -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<History>(createHashStateHistory() as History);
|
||||
const services = useServices();
|
||||
|
||||
useEffect(() => {
|
||||
return historyRef.current.listen(({ pathname }) => {
|
||||
services.navLink.updatePath(pathname);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<ShortcutManagerContextWrapper>
|
||||
<div className="canvas canvasContainer">
|
||||
<CanvasRouter history={historyRef.current} />
|
||||
</div>
|
||||
</ShortcutManagerContextWrapper>
|
||||
);
|
||||
};
|
|
@ -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()}...`,
|
||||
}) => (
|
||||
<div className="canvasContainer--loading">
|
||||
<EuiPanel>
|
||||
<EuiLoadingChart size="m" />
|
||||
|
@ -20,11 +24,3 @@ export const CanvasLoading = ({ msg }) => (
|
|||
</EuiPanel>
|
||||
</div>
|
||||
);
|
||||
|
||||
CanvasLoading.propTypes = {
|
||||
msg: PropTypes.string,
|
||||
};
|
||||
|
||||
CanvasLoading.defaultProps = {
|
||||
msg: 'Loading...',
|
||||
};
|
|
@ -5,5 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { routes } from './routes';
|
||||
export { ExportApp } from './export';
|
||||
export * from './canvas_loading.component';
|
|
@ -38,18 +38,13 @@ exports[`<ExportApp /> renders as expected 1`] = `
|
|||
<div
|
||||
className="canvasLayout__stageHeader"
|
||||
>
|
||||
<Link
|
||||
name="loadWorkpad"
|
||||
params={
|
||||
Object {
|
||||
"id": "my-workpad-abcd",
|
||||
}
|
||||
}
|
||||
<RoutingLink
|
||||
to="/workpad/my-workpad-abcd"
|
||||
>
|
||||
<div>
|
||||
Link
|
||||
</div>
|
||||
</Link>
|
||||
</RoutingLink>
|
||||
</div>
|
||||
<div
|
||||
className="canvasExport__stageContent"
|
||||
|
@ -108,18 +103,13 @@ exports[`<ExportApp /> renders as expected 2`] = `
|
|||
<div
|
||||
className="canvasLayout__stageHeader"
|
||||
>
|
||||
<Link
|
||||
name="loadWorkpad"
|
||||
params={
|
||||
Object {
|
||||
"id": "my-workpad-abcd",
|
||||
}
|
||||
}
|
||||
<RoutingLink
|
||||
to="/workpad/my-workpad-abcd"
|
||||
>
|
||||
<div>
|
||||
Link
|
||||
</div>
|
||||
</Link>
|
||||
</RoutingLink>
|
||||
</div>
|
||||
<div
|
||||
className="canvasExport__stageContent"
|
|
@ -10,9 +10,9 @@ import PropTypes from 'prop-types';
|
|||
// @ts-expect-error untyped library
|
||||
import Style from 'style-it';
|
||||
// @ts-expect-error untyped local
|
||||
import { WorkpadPage } from '../../../components/workpad_page';
|
||||
import { Link } from '../../../components/link';
|
||||
import { CanvasWorkpad } from '../../../../types';
|
||||
import { WorkpadPage } from '../workpad_page';
|
||||
import { RoutingLink } from '../routing';
|
||||
import { CanvasWorkpad } from '../../../types';
|
||||
|
||||
export interface Props {
|
||||
workpad: CanvasWorkpad;
|
||||
|
@ -31,9 +31,7 @@ export const ExportApp: FC<Props> = ({ workpad, selectedPageIndex, initializeWor
|
|||
<div className="canvasExport" data-shared-page={selectedPageIndex + 1}>
|
||||
<div className="canvasExport__stage">
|
||||
<div className="canvasLayout__stageHeader">
|
||||
<Link name="loadWorkpad" params={{ id }}>
|
||||
Edit Workpad
|
||||
</Link>
|
||||
<RoutingLink to={`/workpad/${id}`}>Edit Workpad</RoutingLink>
|
||||
</div>
|
||||
{Style.it(
|
||||
workpad.css,
|
|
@ -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) => <div>Page</div>,
|
||||
}));
|
||||
|
||||
jest.mock('../../../components/link', () => ({
|
||||
Link: (props: any) => <div>Link</div>,
|
||||
jest.mock('../routing', () => ({
|
||||
RoutingLink: (props: any) => <div>Link</div>,
|
||||
}));
|
||||
|
||||
describe('<ExportApp />', () => {
|
|
@ -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) => ({
|
|
@ -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);
|
18
x-pack/plugins/canvas/public/components/fullscreen/index.tsx
Normal file
18
x-pack/plugins/canvas/public/components/fullscreen/index.tsx
Normal file
|
@ -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 <Component isFullscreen={isFullscreen} children={children} />;
|
||||
};
|
|
@ -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;
|
|
@ -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 <Component onLoad={onLoad} />;
|
||||
};
|
|
@ -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<string, any>;
|
||||
}
|
||||
|
||||
export const Link: FC<Props & EuiLinkProps> = ({
|
||||
onClick,
|
||||
target,
|
||||
name,
|
||||
params,
|
||||
children,
|
||||
...linkArgs
|
||||
}) => {
|
||||
const router = useContext(RouterContext);
|
||||
|
||||
if (router) {
|
||||
const navigateTo = (ev: MouseEvent<HTMLButtonElement, globalThis.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 (
|
||||
<EuiLink {...linkArgs} target={target} onClick={navigateTo}>
|
||||
{children}
|
||||
</EuiLink>
|
||||
);
|
||||
} catch (e) {
|
||||
return <div>{strings.getErrorMessage(e.message)}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
return <div>{strings.getErrorMessage('Router Undefined')}</div>;
|
||||
};
|
||||
|
||||
Link.contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
Link.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
params: PropTypes.object,
|
||||
};
|
|
@ -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<Props, State> {
|
|||
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<Props, State> {
|
|||
};
|
||||
|
||||
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<Props, State> {
|
|||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<Link
|
||||
name="loadWorkpad"
|
||||
params={{ id: workpadId, page: pageNumber }}
|
||||
aria-label={strings.getPageNumberAriaLabel(pageNumber)}
|
||||
>
|
||||
{Style.it(
|
||||
workpadCSS,
|
||||
<div>
|
||||
<PagePreview height={100} page={page} onRemove={this.onConfirmRemove} />
|
||||
</div>
|
||||
<WorkpadRoutingContext.Consumer>
|
||||
{({ getUrl }) => (
|
||||
<RoutingLink to={getUrl(pageNumber)}>
|
||||
{Style.it(
|
||||
workpadCSS,
|
||||
<div>
|
||||
<PagePreview height={100} page={page} onRemove={this.onConfirmRemove} />
|
||||
</div>
|
||||
)}
|
||||
</RoutingLink>
|
||||
)}
|
||||
</Link>
|
||||
</WorkpadRoutingContext.Consumer>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
|
|
|
@ -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);
|
|
@ -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 (
|
||||
<Component
|
||||
onPreviousPage={onPreviousPage}
|
||||
onAddPage={onAddPage}
|
||||
onMovePage={onMovePage}
|
||||
onRemovePage={onRemovePage}
|
||||
{...propsFromState}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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);
|
|
@ -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<Omit<Props, 'onDuplicate' | 'isWriteable'>> = (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 (
|
||||
<Component {...props} onDuplicate={onDuplicate} isWriteable={stateFromProps.isWriteable} />
|
||||
);
|
||||
};
|
|
@ -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<string, number | string>,
|
||||
state?: Record<string, string>
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const RouterContext = React.createContext<Router | undefined>(undefined);
|
|
@ -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);
|
|
@ -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 (
|
||||
<RouterContext.Provider value={this.state.router}>
|
||||
<this.state.activeComponent />
|
||||
</RouterContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -5,5 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { routes } from './routes';
|
||||
export { HomeApp } from './home_app';
|
||||
export * from './routing_link';
|
|
@ -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<EuiLinkProps, 'href' | 'onClick'> & RoutingProps;
|
||||
|
||||
export const RoutingLink: FC<RoutingLinkProps> = ({ 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 <EuiLink {...props} />;
|
||||
};
|
||||
|
||||
type RoutingButtonIconProps = Omit<EuiButtonIconProps, 'href' | 'onClick'> & RoutingProps;
|
||||
|
||||
export const RoutingButtonIcon: FC<RoutingButtonIconProps> = ({ 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 <EuiButtonIcon {...props} />;
|
||||
};
|
|
@ -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<Props> = ({
|
|||
}) => {
|
||||
const [activeTray, setActiveTray] = useState<TrayType | null>(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<Props> = ({
|
|||
}
|
||||
}, [isWriteable, activeTray]);
|
||||
|
||||
if (!router) {
|
||||
return <div>{strings.getErrorMessage('Router Undefined')}</div>;
|
||||
}
|
||||
|
||||
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<Props> = ({
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} />
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
<RoutingButtonIcon
|
||||
color="text"
|
||||
onClick={previousPage}
|
||||
to={getUrl(selectedPageNumber - 1)}
|
||||
iconType="arrowLeft"
|
||||
disabled={selectedPageNumber <= 1}
|
||||
isDisabled={selectedPageNumber <= 1}
|
||||
aria-label={strings.getPreviousPageAriaLabel()}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -133,11 +120,11 @@ export const Toolbar: FC<Props> = ({
|
|||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
<RoutingButtonIcon
|
||||
color="text"
|
||||
onClick={nextPage}
|
||||
to={getUrl(selectedPageNumber + 1)}
|
||||
iconType="arrowRight"
|
||||
disabled={selectedPageNumber >= totalPages}
|
||||
isDisabled={selectedPageNumber >= totalPages}
|
||||
aria-label={strings.getNextPageAriaLabel()}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -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 (
|
||||
<WorkpadComponent
|
||||
{...props}
|
||||
setFullscreen={setFullscreenWithEffect}
|
||||
isFullscreen={isFullscreen}
|
||||
undoHistory={undo}
|
||||
redoHistory={redo}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Workpad = compose(
|
||||
|
@ -119,4 +135,4 @@ export const Workpad = compose(
|
|||
},
|
||||
}),
|
||||
withHandlers(zoomHandlerCreators)
|
||||
)(Component);
|
||||
)(AddContexts);
|
||||
|
|
|
@ -123,14 +123,6 @@ export class Workpad extends React.PureComponent {
|
|||
style={fsStyle}
|
||||
data-shared-items-count={totalElementCount}
|
||||
>
|
||||
{isFullscreen && (
|
||||
<Shortcuts
|
||||
name="PRESENTATION"
|
||||
handler={this.keyHandler}
|
||||
targetNodeSelector="body"
|
||||
global
|
||||
/>
|
||||
)}
|
||||
{pages.map((page, i) => (
|
||||
<WorkpadPage
|
||||
key={page.id}
|
||||
|
|
|
@ -7,13 +7,13 @@
|
|||
|
||||
import React, { FC, MouseEventHandler, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Sidebar } from '../../../components/sidebar';
|
||||
import { Toolbar } from '../../../components/toolbar';
|
||||
import { Sidebar } from '../../components/sidebar';
|
||||
import { Toolbar } from '../../components/toolbar';
|
||||
// @ts-expect-error Untyped local
|
||||
import { Workpad } from '../../../components/workpad';
|
||||
import { WorkpadHeader } from '../../../components/workpad_header';
|
||||
import { CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR } from '../../../../common/lib/constants';
|
||||
import { CommitFn } from '../../../../types';
|
||||
import { Workpad } from '../workpad';
|
||||
import { WorkpadHeader } from '../workpad_header';
|
||||
import { CANVAS_LAYOUT_STAGE_CONTENT_SELECTOR } from '../../../common/lib/constants';
|
||||
import { CommitFn } from '../../../types';
|
||||
|
||||
export const WORKPAD_CONTAINER_ID = 'canvasWorkpadContainer';
|
||||
|
|
@ -9,12 +9,12 @@ import { MouseEventHandler } from 'react';
|
|||
import { Dispatch } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
// @ts-expect-error untyped local
|
||||
import { selectToplevelNodes } from '../../../state/actions/transient';
|
||||
import { canUserWrite } from '../../../state/selectors/app';
|
||||
import { getWorkpad, isWriteable } from '../../../state/selectors/workpad';
|
||||
import { selectToplevelNodes } from '../../state/actions/transient';
|
||||
import { canUserWrite } from '../../state/selectors/app';
|
||||
import { getWorkpad, isWriteable } from '../../state/selectors/workpad';
|
||||
import { WorkpadApp as Component } from './workpad_app.component';
|
||||
import { withElementsLoadedTelemetry } from './workpad_telemetry';
|
||||
import { State } from '../../../../types';
|
||||
import { State } from '../../../types';
|
||||
|
||||
export { WORKPAD_CONTAINER_ID } from './workpad_app.component';
|
||||
|
|
@ -12,8 +12,8 @@ import {
|
|||
WorkpadLoadedMetric,
|
||||
WorkpadLoadedWithErrorsMetric,
|
||||
} from './workpad_telemetry';
|
||||
import { METRIC_TYPE } from '../../../lib/ui_metric';
|
||||
import { ResolvedArgType } from '../../../../types';
|
||||
import { METRIC_TYPE } from '../../lib/ui_metric';
|
||||
import { ResolvedArgType } from '../../../types';
|
||||
|
||||
const trackMetric = jest.fn();
|
||||
const Component = withUnconnectedElementsLoadedTelemetry(() => <div />, trackMetric);
|
|
@ -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';
|
|
@ -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<ComponentProps> = (props) => {
|
||||
const { undo, redo } = useContext(WorkpadRoutingContext);
|
||||
|
||||
return <Component {...props} undoHistory={undo} redoHistory={redo} />;
|
||||
};
|
||||
|
||||
export const EditMenu = compose<ComponentProps, OwnProps>(
|
||||
connect(mapStateToProps, mapDispatchToProps, mergeProps),
|
||||
withProps(() => ({ hasPasteData: Boolean(getClipboardData()) })),
|
||||
|
@ -131,4 +137,4 @@ export const EditMenu = compose<ComponentProps, OwnProps>(
|
|||
withHandlers(layerHandlerCreators),
|
||||
withHandlers(groupHandlerCreators),
|
||||
withHandlers(alignmentDistributionHandlerCreators)
|
||||
)(Component);
|
||||
)(EditMenuWithContext);
|
|
@ -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<Props> {
|
|||
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) => {
|
||||
|
|
|
@ -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 (
|
||||
<Component
|
||||
isFullscreen={isFullscreen}
|
||||
nextPage={nextPage}
|
||||
previousPage={previousPage}
|
||||
autoplayEnabled={autoplayEnabled}
|
||||
setFullscreen={setFullscreenWithEffects}
|
||||
toggleAutoplay={toggleAutoplay}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
|
|
|
@ -32,7 +32,7 @@ const { getSecondsText, getMinutesText, getHoursText } = timeStrings;
|
|||
|
||||
interface Props {
|
||||
refreshInterval: number;
|
||||
setRefresh: (interval: number | undefined) => void;
|
||||
setRefresh: (interval: number) => void;
|
||||
disableInterval: () => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) => (
|
||||
<li>
|
||||
<EuiLink onClick={() => onSetInterval(duration)} aria-describedby={descriptionId}>
|
||||
|
@ -71,12 +77,37 @@ export const KioskControls = ({ autoplayInterval, onSetInterval }: Props) => {
|
|||
className="canvasViewMenu__kioskSettings"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiDescriptionListTitle>{strings.getTitle()}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
{timeStrings.getCycleTimeText(interval.length, interval.format)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
<EuiFlexGroup alignItems="center" justifyContent="spaceAround" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
<EuiDescriptionList textStyle="reverse">
|
||||
<EuiDescriptionListTitle>{strings.getTitle()}</EuiDescriptionListTitle>
|
||||
<EuiDescriptionListDescription>
|
||||
{autoplayInterval > 0 ? (
|
||||
<>{timeStrings.getCycleTimeText(interval.length, interval.format)}</>
|
||||
) : (
|
||||
<>{strings.getAutoplayListDurationManualText()}</>
|
||||
)}
|
||||
</EuiDescriptionListDescription>
|
||||
</EuiDescriptionList>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="flexEnd" gutterSize="xs">
|
||||
{autoplayInterval > 0 ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip position="bottom" content={strings.getDisableTooltip()}>
|
||||
<EuiButtonIcon
|
||||
iconType="cross"
|
||||
onClick={disableAutoplay}
|
||||
aria-label={strings.getDisableTooltip()}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<EuiTitle size="xxxs" id={intervalTitleId}>
|
||||
<p>{strings.getCycleFormLabel()}</p>
|
||||
|
|
|
@ -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<Props> = ({
|
||||
|
@ -113,7 +113,7 @@ export const ViewMenu: FunctionComponent<Props> = ({
|
|||
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<Props> = ({
|
|||
closePopover();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: autoplayEnabled
|
||||
? strings.getAutoplayOffMenuItemLabel()
|
||||
: strings.getAutoplayOnMenuItemLabel(),
|
||||
icon: autoplayEnabled ? 'stop' : 'play',
|
||||
onClick: () => {
|
||||
enableAutoplay(!autoplayEnabled);
|
||||
closePopover();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: strings.getAutoplaySettingsMenuItemLabel(),
|
||||
icon: 'empty',
|
||||
|
|
|
@ -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<ComponentProps, {}>(
|
||||
const ViewMenuWithContext: FC<Omit<ComponentProps, PropsFromContext>> = (props) => {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
autoplayInterval,
|
||||
setAutoplayInterval,
|
||||
setFullscreen,
|
||||
setRefreshInterval,
|
||||
refreshInterval,
|
||||
} = useContext(WorkpadRoutingContext);
|
||||
|
||||
const enterFullscreen = useCallback(() => {
|
||||
dispatch(selectToplevelNodes([]));
|
||||
setFullscreen(true);
|
||||
}, [dispatch, setFullscreen]);
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
enterFullscreen={enterFullscreen}
|
||||
setAutoplayInterval={setAutoplayInterval}
|
||||
autoplayEnabled={true}
|
||||
autoplayInterval={autoplayInterval}
|
||||
setRefreshInterval={setRefreshInterval}
|
||||
refreshInterval={refreshInterval}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ViewMenu = compose<Omit<ComponentProps, PropsFromContext>, {}>(
|
||||
connect(mapStateToProps, mapDispatchToProps, mergeProps),
|
||||
withHandlers(zoomHandlerCreators)
|
||||
)(Component);
|
||||
)(ViewMenuWithContext);
|
|
@ -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);
|
173
x-pack/plugins/canvas/public/components/workpad_loader/index.tsx
Normal file
173
x-pack/plugins/canvas/public/components/workpad_loader/index.tsx
Normal file
|
@ -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<ReturnType<typeof useWorkpadService>['find']>;
|
||||
type WorkpadState = WorkpadStatePromise extends PromiseLike<infer U> ? 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<WorkpadState | null>(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 (
|
||||
<Component
|
||||
{...{
|
||||
downloadWorkpad: onDownloadWorkpad,
|
||||
workpads: workpadsState,
|
||||
workpadId,
|
||||
canUserWrite,
|
||||
cloneWorkpad,
|
||||
createWorkpad,
|
||||
findWorkpads,
|
||||
removeWorkpads,
|
||||
formatDate,
|
||||
onClose,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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 (
|
||||
<Link
|
||||
<RoutingLink
|
||||
data-test-subj="canvasWorkpadLoaderWorkpad"
|
||||
name="loadWorkpad"
|
||||
params={{ id: workpad.id }}
|
||||
to={`/workpad/${workpad.id}`}
|
||||
aria-label={strings.getLoadWorkpadArialLabel()}
|
||||
>
|
||||
{workpadName}
|
||||
</Link>
|
||||
</RoutingLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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 <ComposedWorkpadPage {...props} isFullscreen={isFullscreen} />;
|
||||
};
|
||||
|
||||
WorkpadPage.propTypes = {
|
||||
pageId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 }) => (
|
|||
</div>
|
||||
);
|
||||
export const WorkpadTemplates: FunctionComponent<WorkpadTemplatesProps> = ({ onClose }) => {
|
||||
const router = useContext(RouterContext);
|
||||
const history = useHistory();
|
||||
const services = useServices();
|
||||
|
||||
const [templates, setTemplates] = useState<CanvasTemplate[] | undefined>(undefined);
|
||||
const [creatingFromTemplateName, setCreatingFromTemplateName] = useState<string | undefined>(
|
||||
undefined
|
||||
|
@ -53,20 +56,21 @@ export const WorkpadTemplates: FunctionComponent<WorkpadTemplatesProps> = ({ 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 <Creating name={creatingFromTemplateName} />;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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]);
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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 = () => (
|
||||
<Route path="/">
|
||||
<HomeApp />
|
||||
</Route>
|
||||
);
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { Link } from './link';
|
||||
export * from './home_route';
|
22
x-pack/plugins/canvas/public/routes/index.tsx
Normal file
22
x-pack/plugins/canvas/public/routes/index.tsx
Normal file
|
@ -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 }) => (
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
{ExportWorkpadRoute()}
|
||||
{WorkpadRoute()}
|
||||
{HomeRoute()}
|
||||
</Switch>
|
||||
</Router>
|
||||
);
|
|
@ -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,
|
||||
}) => <WorkpadRoutingContext.Provider value={context}>{children}</WorkpadRoutingContext.Provider>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
|
@ -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<number | undefined>(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]);
|
||||
};
|
|
@ -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]);
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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<WorkpadPageRouteParams>();
|
||||
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]);
|
||||
};
|
|
@ -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,
|
||||
}) => <WorkpadRoutingContext.Provider value={context}>{children}</WorkpadRoutingContext.Provider>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
|
@ -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<number | undefined>(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]);
|
||||
};
|
|
@ -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 });
|
||||
});
|
||||
});
|
|
@ -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<string | undefined>(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]);
|
||||
};
|
|
@ -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<boolean>(false);
|
||||
const history = useHistory();
|
||||
const { search } = history.location;
|
||||
const params = useParams<WorkpadPageRouteParams>();
|
||||
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]);
|
||||
};
|
|
@ -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 });
|
||||
});
|
||||
});
|
|
@ -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<string | Error | undefined>(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];
|
||||
};
|
|
@ -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));
|
||||
});
|
||||
});
|
|
@ -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<boolean>(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]);
|
||||
};
|
23
x-pack/plugins/canvas/public/routes/workpad/index.tsx
Normal file
23
x-pack/plugins/canvas/public/routes/workpad/index.tsx
Normal file
|
@ -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<WorkpadRouteParams>;
|
||||
export type WorkpadPageRouteProps = RouteComponentProps<WorkpadPageRouteParams>;
|
27
x-pack/plugins/canvas/public/routes/workpad/route_state.ts
Normal file
27
x-pack/plugins/canvas/public/routes/workpad/route_state.ts
Normal file
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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}</>;
|
||||
};
|
118
x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx
Normal file
118
x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx
Normal file
|
@ -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 = () => (
|
||||
<Route
|
||||
path={'/workpad/:id'}
|
||||
exact={false}
|
||||
children={(route: WorkpadRouteProps) => (
|
||||
<WorkpadLoaderComponent params={route.match.params} key="workpad-loader">
|
||||
{(workpad: CanvasWorkpad) => (
|
||||
<Switch>
|
||||
<Route
|
||||
path="/workpad/:id/page/:pageNumber"
|
||||
children={(pageRoute) => (
|
||||
<WorkpadHistoryManager>
|
||||
<WorkpadRoutingContextComponent>
|
||||
<WorkpadPresentationHelper>
|
||||
<WorkpadApp />
|
||||
</WorkpadPresentationHelper>
|
||||
</WorkpadRoutingContextComponent>
|
||||
</WorkpadHistoryManager>
|
||||
)}
|
||||
/>
|
||||
<Route path="/workpad/:id" strict={false} exact={true}>
|
||||
<Redirect to={`/workpad/${route.match.params.id}/page/${workpad.page + 1}`} />
|
||||
</Route>
|
||||
</Switch>
|
||||
)}
|
||||
</WorkpadLoaderComponent>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
export const ExportWorkpadRoute = () => (
|
||||
<Route
|
||||
path={'/export/workpad/pdf/:id/page/:pageNumber'}
|
||||
children={(route: WorkpadPageRouteProps) => (
|
||||
<WorkpadLoaderComponent loadPages={false} params={route.match.params}>
|
||||
{() => (
|
||||
<ExportRouteManager>
|
||||
<ExportApp />
|
||||
</ExportRouteManager>
|
||||
)}
|
||||
</WorkpadLoaderComponent>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
export const ExportRouteManager: FC = ({ children }) => {
|
||||
const params = useParams<WorkpadPageRouteParams>();
|
||||
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 <Redirect to="/" />;
|
||||
}
|
||||
|
||||
if (!workpad) {
|
||||
return <CanvasLoading />;
|
||||
}
|
||||
|
||||
return children(workpad);
|
||||
};
|
|
@ -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<WorkpadRoutingContextType>(
|
||||
basicWorkpadRoutingContext
|
||||
);
|
||||
|
||||
export const WorkpadRoutingContextComponent: FC = ({ children }) => {
|
||||
const routingContext = useRoutingContext();
|
||||
|
||||
return (
|
||||
<WorkpadRoutingContext.Provider value={routingContext}>
|
||||
{children}
|
||||
</WorkpadRoutingContext.Provider>
|
||||
);
|
||||
};
|
|
@ -26,6 +26,7 @@ const defaultContextValue = {
|
|||
platform: {},
|
||||
navLink: {},
|
||||
search: {},
|
||||
workpad: {},
|
||||
};
|
||||
|
||||
const context = createContext<CanvasServices>(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 = <Props extends WithServicesProps>(type: ComponentType<Props>) => {
|
||||
const EnhancedType: FC<Props> = (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 <context.Provider value={value}>{children}</context.Provider>;
|
||||
};
|
||||
|
|
|
@ -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<typeof services.search>;
|
||||
reporting: ServiceFromProvider<typeof services.reporting>;
|
||||
labs: ServiceFromProvider<typeof services.labs>;
|
||||
workpad: ServiceFromProvider<typeof services.workpad>;
|
||||
}
|
||||
|
||||
export const startServices = async (
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<CanvasServices> = {}) => {
|
||||
|
|
21
x-pack/plugins/canvas/public/services/stubs/workpad.ts
Normal file
21
x-pack/plugins/canvas/public/services/stubs/workpad.ts
Normal file
|
@ -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),
|
||||
};
|
99
x-pack/plugins/canvas/public/services/workpad.ts
Normal file
99
x-pack/plugins/canvas/public/services/workpad.ts
Normal file
|
@ -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<Pick<CanvasWorkpad, 'name' | 'id' | '@timestamp' | '@created'>>;
|
||||
}
|
||||
|
||||
export interface WorkpadService {
|
||||
get: (id: string) => Promise<CanvasWorkpad>;
|
||||
create: (workpad: CanvasWorkpad) => Promise<CanvasWorkpad>;
|
||||
createFromTemplate: (templateId: string) => Promise<CanvasWorkpad>;
|
||||
find: (term: string) => Promise<WorkpadFindResponse>;
|
||||
remove: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const workpadServiceFactory: CanvasServiceFactory<WorkpadService> = (
|
||||
_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}`);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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');
|
||||
|
|
|
@ -52,7 +52,7 @@ export const setWorkpad = createThunk(
|
|||
'setWorkpad',
|
||||
(
|
||||
{ dispatch, type },
|
||||
workpad: CanvasWorkpad,
|
||||
workpad: Omit<CanvasWorkpad, 'assets'>,
|
||||
{ loadPages = true }: { loadPages?: boolean } = {}
|
||||
) => {
|
||||
dispatch(createAction(type)(workpad)); // set the workpad object in state
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
};
|
|
@ -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)]);
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
};
|
|
@ -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
|
||||
),
|
||||
];
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue