Skip to content

Commit

Permalink
Replace history-extra in place of standard url routing (#121440)
Browse files Browse the repository at this point in the history
* Replace history-extra in place of standard url routing

* Fix smoke test

* Fix smoke test again

* Handle embeddalbe save + return

* Update StoryShot
  • Loading branch information
Corey Robertson authored May 2, 2022
1 parent 2ef21c2 commit 27d1fa1
Show file tree
Hide file tree
Showing 16 changed files with 179 additions and 104 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) => {
Expand All @@ -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>
Expand Down
59 changes: 4 additions & 55 deletions x-pack/plugins/canvas/public/components/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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>
);
Expand Down
8 changes: 0 additions & 8 deletions x-pack/plugins/canvas/public/components/home/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -25,9 +21,5 @@ export const Home = () => {
}
}, [dispatch, isMounted, setIsMounted]);

useEffect(() => {
setBreadcrumbs([getBaseBreadcrumb()]);
}, [setBreadcrumbs]);

return <Component />;
};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions x-pack/plugins/canvas/public/components/home_app/home_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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} />;
};
56 changes: 53 additions & 3 deletions x-pack/plugins/canvas/public/components/routing/routing_link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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} />;
};
Expand All @@ -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} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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(
Expand Down
43 changes: 39 additions & 4 deletions x-pack/plugins/canvas/public/lib/breadcrumbs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading

0 comments on commit 27d1fa1

Please sign in to comment.