Replace history-extra in place of standard url routing (#121440)

* Replace history-extra in place of standard url routing

* Fix smoke test

* Fix smoke test again

* Handle embeddalbe save + return

* Update StoryShot
This commit is contained in:
Corey Robertson 2022-05-02 11:36:40 -04:00 committed by GitHub
parent 2ef21c2d44
commit 27d1fa1797
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 179 additions and 104 deletions

View file

@ -271,7 +271,6 @@
"handlebars": "4.7.7",
"he": "^1.2.0",
"history": "^4.9.0",
"history-extra": "^5.0.1",
"hjson": "3.2.1",
"http-proxy-agent": "^2.1.0",
"https-proxy-agent": "^5.0.0",

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import React from 'react';
import React, { FC } from 'react';
import useObservable from 'react-use/lib/useObservable';
import ReactDOM from 'react-dom';
import { CoreStart } from '@kbn/core/public';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
@ -32,9 +33,28 @@ const embeddablesRegistry: {
const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => {
const I18nContext = core.i18n.Context;
const EmbeddableRenderer: FC<{ embeddable: IEmbeddable }> = ({ embeddable }) => {
const currentAppId = useObservable(core.application.currentAppId$, undefined);
const embeddableContainerContext: EmbeddableContainerContext = {
getCurrentPath: () => window.location.hash,
if (!currentAppId) {
return null;
}
const embeddableContainerContext: EmbeddableContainerContext = {
getCurrentPath: () => {
const urlToApp = core.application.getUrlForApp(currentAppId);
const inAppPath = window.location.pathname.replace(urlToApp, '');
return inAppPath + window.location.search + window.location.hash;
},
};
return (
<plugins.embeddable.EmbeddablePanel
embeddable={embeddable}
containerContext={embeddableContainerContext}
/>
);
};
return (embeddableObject: IEmbeddable) => {
@ -45,10 +65,7 @@ const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => {
>
<I18nContext>
<KibanaThemeProvider theme$={core.theme.theme$}>
<plugins.embeddable.EmbeddablePanel
embeddable={embeddableObject}
containerContext={embeddableContainerContext}
/>
<EmbeddableRenderer embeddable={embeddableObject} />
</KibanaThemeProvider>
</I18nContext>
</div>

View file

@ -5,14 +5,9 @@
* 2.0.
*/
import React, { FC, useRef, useEffect } from 'react';
import { Observable } from 'rxjs';
import React, { FC, useEffect } from 'react';
import PropTypes from 'prop-types';
import { History } from 'history';
// @ts-expect-error
import createHashStateHistory from 'history-extra/dist/createHashStateHistory';
import { ScopedHistory } from '@kbn/core/public';
import { skipWhile, timeout, take } from 'rxjs/operators';
import { useNavLinkService } from '../../services';
// @ts-expect-error
import { shortcutManager } from '../../lib/shortcut_manager';
@ -33,64 +28,18 @@ class ShortcutManagerContextWrapper extends React.Component {
}
export const App: FC<{ history: ScopedHistory }> = ({ history }) => {
const historyRef = useRef<History>(createHashStateHistory() as History);
const { updatePath } = useNavLinkService();
useEffect(() => {
return historyRef.current.listen(({ pathname }) => {
updatePath(pathname);
return history.listen(({ pathname, search }) => {
updatePath(pathname + search);
});
});
useEffect(() => {
return history.listen(({ pathname, hash }) => {
// The scoped history could have something that triggers a url change, and that change is not seen by
// our hash router. For example, a scopedHistory.replace() as done as part of the saved object resolve
// alias match flow will do the replace on the scopedHistory, and our app doesn't react appropriately
// So, to work around this, whenever we see a url on the scoped history, we're going to wait a beat and see
// if it shows up in our hash router. If it doesn't, then we're going to force it onto our hash router
// I don't like this at all, and to overcome this we should switch away from hash router sooner rather than later
// and just use scopedHistory as our history object
const expectedPath = hash.substr(1);
const action = history.action;
// Observable of all the path
const hashPaths$ = new Observable<string>((subscriber) => {
subscriber.next(historyRef.current.location.pathname);
const unsubscribeHashListener = historyRef.current.listen(({ pathname: newPath }) => {
subscriber.next(newPath);
});
return unsubscribeHashListener;
});
const subscription = hashPaths$
.pipe(
skipWhile((value) => value !== expectedPath),
timeout(100),
take(1)
)
.subscribe({
error: (e) => {
if (action === 'REPLACE') {
historyRef.current.replace(expectedPath);
} else {
historyRef.current.push(expectedPath);
}
},
});
window.setTimeout(() => subscription.unsubscribe(), 150);
});
}, [history, historyRef]);
return (
<ShortcutManagerContextWrapper>
<div className="canvas canvasContainer">
<CanvasRouter history={historyRef.current} />
<CanvasRouter history={history} />
</div>
</ShortcutManagerContextWrapper>
);

View file

@ -7,14 +7,10 @@
import React, { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { getBaseBreadcrumb } from '../../lib/breadcrumbs';
import { resetWorkpad } from '../../state/actions/workpad';
import { Home as Component } from './home.component';
import { usePlatformService } from '../../services';
export const Home = () => {
const { setBreadcrumbs } = usePlatformService();
const [isMounted, setIsMounted] = useState(false);
const dispatch = useDispatch();
@ -25,9 +21,5 @@ export const Home = () => {
}
}, [dispatch, isMounted, setIsMounted]);
useEffect(() => {
setBreadcrumbs([getBaseBreadcrumb()]);
}, [setBreadcrumbs]);
return <Component />;
};

View file

@ -382,6 +382,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = `
className="euiLink euiLink--primary"
data-test-subj="canvasWorkpadTableWorkpad"
href="/workpad/workpad-2"
onClick={[Function]}
rel="noreferrer"
>
<span>
@ -558,6 +559,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = `
className="euiLink euiLink--primary"
data-test-subj="canvasWorkpadTableWorkpad"
href="/workpad/workpad-1"
onClick={[Function]}
rel="noreferrer"
>
<span>
@ -734,6 +736,7 @@ exports[`Storyshots Home/Components/Workpad Table Workpad Table 1`] = `
className="euiLink euiLink--primary"
data-test-subj="canvasWorkpadTableWorkpad"
href="/workpad/workpad-0"
onClick={[Function]}
rel="noreferrer"
>
<span>

View file

@ -6,6 +6,7 @@
*/
import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { getBaseBreadcrumb } from '../../lib/breadcrumbs';
import { resetWorkpad } from '../../state/actions/workpad';
@ -16,10 +17,11 @@ export const HomeApp = () => {
const { setBreadcrumbs } = usePlatformService();
const dispatch = useDispatch();
const onLoad = () => dispatch(resetWorkpad());
const history = useHistory();
useEffect(() => {
setBreadcrumbs([getBaseBreadcrumb()]);
}, [setBreadcrumbs]);
setBreadcrumbs([getBaseBreadcrumb(history)]);
}, [setBreadcrumbs, history]);
return <Component onLoad={onLoad} />;
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import React, { FC } from 'react';
import React, { FC, useCallback, MouseEvent } from 'react';
import { EuiLink, EuiLinkProps, EuiButtonIcon, EuiButtonIconProps } from '@elastic/eui';
import { useHistory } from 'react-router-dom';
@ -15,13 +15,43 @@ interface RoutingProps {
type RoutingLinkProps = Omit<EuiLinkProps, 'href' | 'onClick'> & RoutingProps;
const isModifiedEvent = (event: MouseEvent) =>
!!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
const isLeftClickEvent = (event: MouseEvent) => event.button === 0;
const isTargetBlank = (event: MouseEvent) => {
const target = (event.target as HTMLElement).getAttribute('target');
return target && target !== '_self';
};
export const RoutingLink: FC<RoutingLinkProps> = ({ to, ...rest }) => {
const history = useHistory();
const onClick = useCallback(
(event: MouseEvent) => {
if (event.defaultPrevented) {
return;
}
// Let the browser handle links that open new tabs/windows
if (isModifiedEvent(event) || !isLeftClickEvent(event) || isTargetBlank(event)) {
return;
}
// Prevent regular link behavior, which causes a browser refresh.
event.preventDefault();
// Push the route to the history.
history.push(to);
},
[history, to]
);
// Generate the correct link href (with basename accounted for)
const href = history.createHref({ pathname: to });
const props = { ...rest, href } as EuiLinkProps;
const props = { ...rest, href, onClick } as EuiLinkProps;
return <EuiLink {...props} />;
};
@ -31,10 +61,30 @@ type RoutingButtonIconProps = Omit<EuiButtonIconProps, 'href' | 'onClick'> & Rou
export const RoutingButtonIcon: FC<RoutingButtonIconProps> = ({ to, ...rest }) => {
const history = useHistory();
const onClick = useCallback(
(event: MouseEvent) => {
if (event.defaultPrevented) {
return;
}
// Let the browser handle links that open new tabs/windows
if (isModifiedEvent(event) || !isLeftClickEvent(event) || isTargetBlank(event)) {
return;
}
// Prevent regular link behavior, which causes a browser refresh.
event.preventDefault();
// Push the route to the history.
history.push(to);
},
[history, to]
);
// Generate the correct link href (with basename accounted for)
const href = history.createHref({ pathname: to });
const props = { ...rest, href } as EuiButtonIconProps;
const props = { ...rest, href, onClick } as EuiButtonIconProps;
return <EuiButtonIcon {...props} />;
};

View file

@ -29,7 +29,7 @@ interface Props {
export const EditorMenu: FC<Props> = ({ addElement }) => {
const embeddablesService = useEmbeddablesService();
const { pathname, search } = useLocation();
const { pathname, search, hash } = useLocation();
const platformService = usePlatformService();
const stateTransferService = embeddablesService.getStateTransfer();
const visualizationsService = useVisualizationsService();
@ -61,11 +61,11 @@ export const EditorMenu: FC<Props> = ({ addElement }) => {
path,
state: {
originatingApp: CANVAS_APP,
originatingPath: `#/${pathname}${search}`,
originatingPath: `${pathname}${search}${hash}`,
},
});
},
[stateTransferService, pathname, search]
[stateTransferService, pathname, search, hash]
);
const createNewEmbeddable = useCallback(

View file

@ -5,12 +5,47 @@
* 2.0.
*/
import { MouseEvent } from 'react';
import { History } from 'history';
import { ChromeBreadcrumb } from '@kbn/core/public';
export const getBaseBreadcrumb = (): ChromeBreadcrumb => ({
text: 'Canvas',
href: '#/',
});
const isModifiedEvent = (event: MouseEvent) =>
!!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
const isLeftClickEvent = (event: MouseEvent) => event.button === 0;
const isTargetBlank = (event: MouseEvent) => {
const target = (event.target as HTMLElement).getAttribute('target');
return target && target !== '_self';
};
export const getBaseBreadcrumb = (history: History): ChromeBreadcrumb => {
const path = '/';
const href = history.createHref({ pathname: path });
const onClick = (event: MouseEvent) => {
if (event.defaultPrevented) {
return;
}
// Let the browser handle links that open new tabs/windows
if (isModifiedEvent(event) || !isLeftClickEvent(event) || isTargetBlank(event)) {
return;
}
// Prevent regular link behavior, which causes a browser refresh.
event.preventDefault();
// Push the route to the history.
history.push(path);
};
return {
text: 'Canvas',
href,
onClick,
};
};
export const getWorkpadBreadcrumb = ({
name = 'Workpad',

View file

@ -6,17 +6,48 @@
*/
import React, { FC } from 'react';
import { Router, Switch } from 'react-router-dom';
import { Router, Switch, Route, RouteComponentProps, Redirect } from 'react-router-dom';
import { History } from 'history';
import { parse, stringify } from 'query-string';
import { HomeRoute } from './home';
import { WorkpadRoute, ExportWorkpadRoute } from './workpad';
const isHashPath = (hash: string) => {
return hash.indexOf('#/') === 0;
};
const mergeQueryStrings = (query: string, queryFromHash: string) => {
const queryObject = parse(query);
const hashObject = parse(queryFromHash);
return stringify({ ...queryObject, ...hashObject });
};
export const CanvasRouter: FC<{ history: History }> = ({ history }) => (
<Router history={history}>
<Switch>
{ExportWorkpadRoute()}
{WorkpadRoute()}
{HomeRoute()}
</Switch>
<Route
path="/"
children={(route: RouteComponentProps) => {
// If it looks like the hash is a route then we will do a redirect
if (isHashPath(route.location.hash)) {
const [hashPath, hashQuery] = route.location.hash.split('?');
let search = route.location.search || '?';
if (hashQuery !== undefined) {
search = mergeQueryStrings(search, `?${hashQuery}`);
}
return <Redirect to={`${hashPath.substr(1)}${search.length > 1 ? `?${search}` : ''}`} />;
}
return (
<Switch>
{ExportWorkpadRoute()}
{WorkpadRoute()}
{HomeRoute()}
</Switch>
);
}}
/>
</Router>
);

View file

@ -7,6 +7,7 @@
import { i18n } from '@kbn/i18n';
import React, { FC, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { getBaseBreadcrumb, getWorkpadBreadcrumb } from '../../lib/breadcrumbs';
// @ts-expect-error
import { setDocTitle } from '../../lib/doc_title';
@ -27,13 +28,14 @@ export const WorkpadPresentationHelper: FC = ({ children }) => {
useFullscreenPresentationHelper();
useAutoplayHelper();
useRefreshHelper();
const history = useHistory();
useEffect(() => {
platformService.setBreadcrumbs([
getBaseBreadcrumb(),
getBaseBreadcrumb(history),
getWorkpadBreadcrumb({ name: workpad.name }),
]);
}, [workpad.name, workpad.id, platformService]);
}, [workpad.name, workpad.id, platformService, history]);
useEffect(() => {
setDocTitle(workpad.name);
@ -44,7 +46,7 @@ export const WorkpadPresentationHelper: FC = ({ children }) => {
objectNoun: getWorkpadLabel(),
currentObjectId: workpad.id,
otherObjectId: workpad.aliasId,
otherObjectPath: `#/workpad/${workpad.aliasId}`,
otherObjectPath: `/workpad/${workpad.aliasId}`,
})
: null;

View file

@ -20,7 +20,7 @@ export type CanvasNavLinkServiceFactory = KibanaPluginServiceFactory<
export const navLinkServiceFactory: CanvasNavLinkServiceFactory = ({ coreStart, appUpdater }) => ({
updatePath: (path: string) => {
appUpdater?.next(() => ({
defaultPath: `#${path}`,
defaultPath: `${path}`,
}));
getSessionStorage().set(`${SESSIONSTORAGE_LASTPATH}:${coreStart.http.basePath.get()}`, path);

View file

@ -94,7 +94,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it(`allows a workpad to be edited`, async () => {
await PageObjects.common.navigateToActualUrl(
'canvas',
'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31',
'/workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31',
{
ensureCurrentUrl: true,
shouldLoginIfPrompted: false,
@ -171,7 +171,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it(`does not allow a workpad to be edited`, async () => {
await PageObjects.common.navigateToActualUrl(
'canvas',
'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31',
'/workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31',
{
ensureCurrentUrl: true,
shouldLoginIfPrompted: false,

View file

@ -80,7 +80,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
it(`allows a workpad to be edited`, async () => {
await PageObjects.common.navigateToActualUrl(
'canvas',
'workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31',
'/workpad/workpad-1705f884-6224-47de-ba49-ca224fe6ec31',
{
ensureCurrentUrl: true,
shouldLoginIfPrompted: false,

View file

@ -48,9 +48,9 @@ export default function canvasSmokeTest({ getService, getPageObjects }) {
await retry.try(async () => {
const url = await browser.getCurrentUrl();
// remove all the search params, just compare the route
const hashRoute = new URL(url).hash.split('?')[0];
expect(hashRoute).to.equal(`#/workpad/${testWorkpadId}/page/1`);
const path = new URL(url).pathname;
expect(path).to.equal(`/app/canvas/workpad/${testWorkpadId}/page/1`);
});
});

View file

@ -16133,11 +16133,6 @@ highlight.js@^10.1.1, highlight.js@^10.4.1, highlight.js@~10.4.0:
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.1.tgz#d48fbcf4a9971c4361b3f95f302747afe19dbad0"
integrity sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg==
history-extra@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/history-extra/-/history-extra-5.0.1.tgz#95a2e59dda526c4241d0ae1b124a77a5e4675ce8"
integrity sha512-6XV1L1lHgporVWgppa/Kq+Fnz4lhBew7iMxYCTfzVmoEywsAKJnTjdw1zOd+EGLHGYp0/V8jSVMEgqx4QbHLTw==
history@^4.9.0:
version "4.9.0"
resolved "https://registry.yarnpkg.com/history/-/history-4.9.0.tgz#84587c2068039ead8af769e9d6a6860a14fa1bca"