-
Notifications
You must be signed in to change notification settings - Fork 8.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add RedirectAppLinks component in kibana_react #67595
add RedirectAppLinks component in kibana_react #67595
Conversation
Pinging @elastic/kibana-platform (Team:Platform) |
@@ -306,6 +306,7 @@ export class ApplicationService { | |||
const relUrl = http.basePath.prepend(getAppUrl(availableMounters, appId, path)); | |||
return absolute ? relativeToAbsolute(relUrl) : relUrl; | |||
}, | |||
parseAppUrl: (url) => parseAppUrl(url, http.basePath, this.apps), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As the new RedirectCrossAppLinks
component should ignore links pointing to the current application, I couldn't just use navigateToApp
directly, and had to expose parseAppUrl
to allow the consumer to check if the app from the link is, or not, the active app.
Exposing this API still makes sense anyway I think
it('intercept click events on children link elements', () => { | ||
let event: MouseEvent; | ||
|
||
const component = mount( | ||
<div | ||
onClick={(e) => { | ||
event = e; | ||
}} | ||
> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like these enzyme based tests are as good as actual FTR test, as the only difference is that they are using jsdoc instead of the actual browser implementation. It also allow more precise testing, as we can collect the event to perform verifications, which would not be possible on FTR tests.
Tell me if you think that adding FTR tests is still necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope. Seems fine to me. Can be covered by FTR when in use by actual applications.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any UI we could drop this into now for testing? Maybe the SavedObjects UI?
|
||
import { getClosestLink } from './utils'; | ||
|
||
const createBranch = (...tags: string[]): HTMLElement[] => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be simpler to just use raw html strings. I prefer this because it eliminates the possibility of there being a bug in this setup code.
const element = document.createElement('div')
element.innerHTML = `<div><a></a></div>`;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This utility method has the advantage to return the created branch elements as an array
const [, container, target] = createBranch('A', 'DIV', 'SPAN');
Using plain innerHTML
would force to manually access each individual node of the branch in the tests. I think the decreased readability is a biggest downside than a bug risk in the createBranch
method?
I could also change to something like
const element = document.createElement('a')
element.innerHTML = `<div><span></span></div>`;
const [, container, target] = getLeftmostBranch(element);
but it's still 3 lines versus one (and getLeftmostBranch
would have a logic very close to createBranch
anyway)
wdyt?
!hasActiveModifierKey(e) // ignore clicks with modifier keys | ||
) { | ||
const appInfo = parseAppUrl(link.href); | ||
if (appInfo && appInfo.app !== currentAppId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remind me again why we can't call this for links within the same app?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be nice if this solution worked for all links
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would work for intra-app links. It's just an opinionated decision, based on the fact that most (all) out apps are currently using react-router
and the Link
component. Handling links to the active link could/would result in a click on the link be handled both by the Link
click handler and ours, resulting on performing the history.push
twice. (Also the name of our component is redirect**Cross**AppLinks
But I'm unsure about that TBH, maybe we should just decide than any app links should be handled and document that using Link
inside RedirectCrossAppLinks
should be avoided? wdyt @joshdover @Dosant ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at https://github.com/ReactTraining/react-router/blob/master/packages/react-router-dom/modules/Link.js#L49, the Link
onClick does prevent default, so clicking on a Link
inside our component would juste trigger the Link
's navigation, and not ours.
We are probably fine to remove the currentApp check then. Which also means we can use the component for the navlinks.
Should the component be renamed to RedirectAppLinks
then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
based on the fact that most (all) out apps are currently using
react-router
and theLink
component
I'm not sure this is true? In my rudimentary search (git grep "import.*Link.*from 'react-router*" -- {src,x-pack}/plugins
) I only found that beats_management and uptime plugins were using this component. I think the main reason devs are not using this component is that you can't really use it with EuiButton or EuiLink.
We are probably fine to remove the currentApp check then. Which also means we can use the component for the navlinks.
Should the component be renamed to
RedirectAppLinks
then?
If it works, than a rename makes sense to me 👍
I added it to the SOM table, however as the management plugin is still in legacy mode (the KP management plugin is still just a bridge to the legacy plugin), all links are still triggering full page refresh (however with actual debugging I saw that it worked, but it's not enough). Not sure where we could wire this for testing. @Dosant maybe you have an idea? |
How about our There are also functional tests for those demos, so we could cover that navigation in function test to run with examples use: |
Another option, if we get rid of this non-same-app check: We could also wire it directly to the nav by removing the current custom onClick handler in |
Adapted and renamed the component to no longer exclude the current app.
Actually we can't, as the component lives in the |
Due to the lack of any obvious place to do it, I ended up adding FTR tests instead of wiring the feature to some existing links. PR should be in its final state now. |
// see https://github.com/DefinitelyTyped/DefinitelyTyped/pull/12239 | ||
const target = e.target as HTMLElement; | ||
|
||
const link = getClosestLink(target, container); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could use built-in Element.closest() method?
const link = getClosestLink(target, container); | |
const link = target.closest('.containerClass a'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure, due to browser support: https://caniuse.com/#search=closest?
Unfortunately I don't have a browserstack account to ensure this API is properly shimmed by corejs
Do we have a usable account somewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are discounting IE11 support in v7.9, so it's not a problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That requires quite a bit of changes in the handler unit tests as it can no longer pass down the container for testing, but I guess using native APIs should be encouraged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hum. it seems that our version of jsdom
doesnt supports closest
, which is strange as it's supposed to be available for quite a long time now.
TypeError: target.closest is not a function
41 | const target = e.target as HTMLElement;
42 |
> 43 | const link = target.closest(`.${containerClass} a`) as HTMLAnchorElement;
console.log(target, target.parentElement, target.closest, target.parentElement.closest)
// HTMLAnchorElement {} HTMLDivElement {...} undefined undefined
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed: the jsdom
version that (our version of) jest is using (node_modules/jest-environment-jsdom/node_modules/jsdom/lib/jsdom/living/nodes/Element-impl.js
) is a 11.5.1
, which does not implements closest
...
Latest jest does have a version that implements it though (https://github.com/facebook/jest/blob/master/packages/jest-environment-jsdom/package.json)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a shame. Let's keep the current implementation then. We can add a comment to switch to native API later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will add a comment for when #58095 lands.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, I wasn't able to bring the most recent version of jsdom
in that PR, because seems a combination of jest@25
with jsdom>=14
got serious performance degradation and we cannot bump to jest@26
, because it requires typescript>=3.8
, so I hope @restrry PR gets merged soon and I will give it a try to jest@26
and jsdom@16
navigateToUrl: ApplicationStart['navigateToUrl']; | ||
} | ||
|
||
export const createCrossAppClickHandler = ({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method doesn't contain any logic to enforce the cross-app navigation. Does it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch, this one wasn't properly renamed when we decided to get rid of the cross-app limitation.
* Utility component that will intercept click events on children anchor (`<a>`) elements to call | ||
* `application.navigateToUrl` with the link's href. This will trigger SPA friendly navigation | ||
* when the link points to a valid kibana app. | ||
* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we recommend using it on the topmost level of the component tree?
decodeURIComponent( | ||
url.format({ | ||
protocol: 'http:', | ||
hostname: process.env.TEST_KIBANA_HOST || 'localhost', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: our tests shouldn't rely on the test environment too much since we do not have control over it. We can match against some parts of browser URL instead:
const currentUrl = await browser.getCurrentUrl();
const parsedCurrentUrl = Url.parse(currentUrl);
expect(parsedCurrentUrl.pathname).to.eql('/app/applink_end/some-path');
expect(parsedCurrentUrl.hash).to.eql('#/some/hash');
return browser.executeAsync<boolean>(async (cb) => { | ||
const reloaded = window.__nonReloadedFlag !== true; | ||
cb(reloaded); | ||
return reloaded; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately It is, as least for TS not to complains, because our executeAsync
signature is broken (and so is webdriver's but in a different way), but I got tired of adding the comment on every browser.executeAsync
tbh.
kibana/test/functional/services/common/browser.ts
Lines 482 to 485 in 13138c0
public async executeAsync<R>( | |
fn: string | ((...args: any[]) => Promise<R>), | |
...args: any[] | |
): Promise<R> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree. But TS doesn't work well with WebDriver implementation when a callback is always the last argument passed in a function.
We could change executeAsync signature to accept 1 additional argument at max:
async executeAsync<T = any>(
fn: <T>(cb: (value: T) => void) => void | Promise<void>,
): Promise<T>;
async executeAsync<T = any, Context extends Record<string, any> = Record<string, any>>(
fn: <T>(context: Context, cb: (value: T) => void) => void | Promise<void>,
context: Context,
): Promise<T>;
There are 13 tests with executeAsync
in the code base, it doesn't look like a significant effort. @elastic/kibana-qa Any objections we do this change in a separate PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll defer to @dmlemeshko on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@elastic/kibana-qa Any objections we do this change in a separate PR?
No problem from my side.
const browser = getService('browser'); | ||
const testSubjects = getService('testSubjects'); | ||
|
||
const setNonReloadedFlag = async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: here and below unnecessary async
|
||
beforeEach(() => { | ||
application = applicationServiceMock.createStartContract(); | ||
application.parseAppUrl.mockReturnValue({ app: 'targetApp' }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't appear this is used anymore? Can we remove this API now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It isn't. I feel like I'll have to reimplement it soon enough by doing so, but you're probably right, I should remove it from this PR.
💚 Build SucceededHistory
To update your PR or re-run it, just comment with: |
@Dosant (or anyone else from @elastic/kibana-app-arch) as |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gave it a try with following scenarios:
- Cross link between different KP example apps without page refresh
- Wrapped uiActions context menu with it and checked that link are working without page refresh
One thing to keep in mind, is that consumer have to add this wrapper for every react root. (Guess this is just like react works)
e.g.
<RedirectCrossAppLinks>
<App>
<a href />
<... native js>
ReactDOM.render(<RedirectCrossAppLinks><a href /></RedirectCrossAppLinks>)
</ ... native js>
</App>
</RedirectCrossAppLinks>
A bit sad that had to explicitly pass applicationStart
to use it and in that nested case above ⬆️ this is a bit of a burden
Yea, unfortunately this is how react works. Roots are strictly isolated in react world. There is no alternative that wouldn't have the same limitation as my previous PR using a global handler.
Other alternative would have been to use a context, but you would have to pass the |
* implements RedirectCrossAppLinks component * update doc * review comments * use `RedirectCrossAppLinks` in SOM SO table page * update snapshots due to merge * do not filter current app * rename component * fix snapshots * add FTR tests * review comments * remove the `parseAppUrl` unused core API * fix snapshots * fix test plugin ts version * add newline
* implements RedirectCrossAppLinks component * update doc * review comments * use `RedirectCrossAppLinks` in SOM SO table page * update snapshots due to merge * do not filter current app * rename component * fix snapshots * add FTR tests * review comments * remove the `parseAppUrl` unused core API * fix snapshots * fix test plugin ts version * add newline
Just noting we should make sure to add these types of updates to existing docs when possible. Here's a ticket to address: #72432 |
Summary
Fix #58751
Add a
<RedirectAppLinks>
component inkibana_react
to help with cross-app linking.The component will intercept any click performed on
<a>
children and callapplication.navigateToUrl
to avoid performing a full page refresh when the link points to a valid kibana application route.Checklist