-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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
Improve cross-app linking in Platform #58751
Comments
Pinging @elastic/kibana-platform (Team:Platform) |
I'm going to look into it. Just to be sure we are on the same page. We would add a global listener, but only change the behavior of links having a relative url matching the path of a registered app, correct? I.E '/app/dashboard/some-path' // -> match
'/login' // -> match
'http://foo.bar' // -> no match
'http://current-kibana-instance/basepath/app/dashboard' -> no match |
@pgayvallet IMO it also makes sense to check absolute links and makes sure they have same behaviour as relative? Why not? |
Basically just because it adds some complexity to the parsing process. But I guess with the same kibana/src/core/public/application/application_service.tsx Lines 380 to 385 in aa8cb62
|
WIP implementation works with relative urls (either with or without base-path) and absolute urls: interface ParsedApp {
app: string;
path?: string;
}
/**
* Parse given url and return the associated app id and path if an app matches.
* Input url can either be:
* - an path relative to the basePath, ie `/app/my-app/some-path`
* - an path containing the basePath, ie `/base-path/app/my-app/some-path`
* - an absolute url matching the `origin` of kibana instance (as seen by the browser),
* i.e `https://kibana:8080/base-path/app/my-app/some-path`
*/
export const parseAppUrl = (
url: string,
basePath: IBasePath,
apps: Map<string, App<any> | LegacyApp>
): ParsedApp | undefined => { [...] } Does that seems to be answering all use cases we want to cover? |
What happens if the basePath is set to |
I was sure the Will remove the |
So, I created #65164 to test the global listener approach. Manual tests are working fine, however this is breaking almost all the CI FTR tests. I need to look a little closer to the failed tests, but I'm starting to wonder if this really should be enabled 'by default' on all links. |
Have we considered the other option Josh proposed? Instead of a component, I'd suggest a service called getAppLink('discover', { path: '/some-path' });
/*
Returns:
{
href: '/discover/some-path',
onClick: () => {}, // a callback
}
*/
// Apply to a link to support SPA-navigation or opening the link in a new tab.
<EuiLink {...getAppLink('discover', { path: '/some-path' })}>Click me</EuiLink> I like how the consumer is explicitly choosing to use this service. Default behavior of events isn't being prevented by some distant code, so it seems like it would be easier for consumers to debug. |
We discussed it with the team and we had a preference for the other option mostly because it avoids developer errors, by ensuring every cross-app link is automatically wired to SPA navigation. It also seemed that would be a better developer experience. Also as <EuiLink {...application.getAppLinkProps('discover', { path: '/some-path' })}>Click me</EuiLink> and <EuiLink onClick={e => e.preventDefault(); application.navigateToApp('discover', { path: '/some-path' })}>Click me</EuiLink> seems minimal (even if I agree that the first is more graceful). The #65164 is now passing CI, so we might be good with this global handler.
I agree that auto-magical things can sometime be dangerous, which is why I added a way to explicitly disable the 'auto navigation' handler on some links by the addition of a specific css class. I still think that the most common use case (like more than 98%) of cross-app links will be 'plain' links with only an href and no additional |
I closed #65164 due to too many complications related to handler execution order. Options still on the table:
|
Update from a conversation outside Github: We're going to explore next creating a wrapper React component that will essentially behave the same as the global event handler approach we explored before, but be contained to a React UI tree. Example usage: <RedirectCrossAppLinks navigateToUrl={application.navigateToUrl}>
<MyCustomComponentWithLinkToAnotherApp />
</RedirectCrossAppLinks> This const RedirectCrossAppLinks = ({ navigateToUrl, children }) => {
const onClick = (e) => {
let anchor = e.target;
while (anchor.tagName !== "A") {
if (anchor.parentElement === null || anchor === e.currentTarget) {
console.log(`is not inside an <a> tag`);
return;
}
anchor = anchor.parentElement;
}
e.stopPropagation();
navigateToUrl(anchor.attributes.href);
};
return (
<div onClick={onClick}>{children}</div>
);
}; Reason we prefer this over the CJ's proposal is mostly that it allows us to avoid having to couple leaf components deep in a UI tree to a Core API. This should make these more flexible. This pattern does deviate from "React best-practices" since it relies on event delegation. However we think this may be acceptable since this behavior can be viewed as an "enhancement" to the default behavior and it does not introduce a new data flow that needs to be tested by consumers. |
Just throwing my $0.02 in here: I'd much prefer an explicit component to use that says "I want my link to behave like a Router-based link" rather than some kind of magic operating on my links in secret behind the scenes. I understand how it avoids accidental developer error, and that makes sense. But this kind of magic scares me :angular: 😨 I guess I'm exactly in line with CJ on this part:
|
@pgayvallet In a point you made, you proposed that this was a viable alternative: <EuiLink onClick={e => e.preventDefault(); application.navigateToApp('discover', { path: '/some-path' })}>Click me</EuiLink> I just want to point out that without an
I think you raise a good point here. You'd have to wrap the returned const { href, onClick } = getAppLink('discover', { path: '/some-path' });
const wrappedOnClick = (event) => {
trackUiMetric('click', 'clickMeLink');
onClick(event); // TS can alert consumers who forget the event argument
};
<EuiLink href={href} onClick={wrappedOnClick}>Click me</EuiLink> Anyway, this is moot if we're using |
I was just dealing with this and with throw in my 2¢ - I think we need something that would be composable with Eui components. WIP code - https://github.com/elastic/kibana/pull/66781/files#diff-76785e16a195e42ac817f1a982269d11R36 I think a function that returns OnClick and href values is a good solution. Better integration of additional OnClick functionality is possible but we'd need to define what specifically that would be. Someone could always write a function that calls Rant warning I see two problems with I see the same problem with the path and it needs a similar solution. Heck, apps should have this internally - and they often do through string composition. Which is okay, but I see something like -
Perhaps these methods would be backed by URLs would become an expression of app state and api calls rather than a primary interface as it is now. URLs should be seen as the domain of external entry into Kibana. If these ideas are worthwhile, then we don't want It should be noted that there's an interesting similarity between internal and external url expressions and routing. Worth exploring. What if it could all be defined in a single place? If we need a test case, I can recommend index pattern management. Anyway, I'm not sure how possible any of this is but I know it would avoid a lot of the problems I've seen. |
I think I've strayed from the topic at hand. I think this problem set requires a good and agreed upon problem definition. The initial problem statement in the issues description is good but we might want to revisit it if simply to say "we're not going to solve these other specific problems" and create new tickets. |
Slightly tangential (sorry!): @mattkime @cjcenizal Completely agree. Links as pure strings are inherently unsafe. |
I like this line of thought a lot. |
Are there any actual problems you anticipate from this behavior? And just to clarify, this is still opt-in behavior. You can add this wrapper component in your UI tree (or just parts of it) if you want, but it's not required. It's a convenience pattern for enhancing the default behavior. I know we prefer "one way to do things", but I don't necessarily see a problem with having both options available. I believe they would even be interoperable, meaning that a UI component that uses the
I agree with this, but also agree that it's orthogonal to this issue. I believe this is what the URL generator pattern is supposed to solve? I don't think apps should expose a |
If it's opt-in, that's good. I was imagining this was going to wrap the entire Kibana app outside of our individual apps. Personally I'd still discourage folks from using this, because reading the code becomes harder (how did this EuiLink work right?? oh right there's a magical event listener at the root of our app that's handling it for us...) but as long as plugins don't have to opt-in, I can live with it. :D |
const { href, onClick } = core.application.getAppLink('discover', { path: '/some-path' });
const wrappedOnClick = (event) => {
trackUiMetric('click', 'clickMeLink');
onClick(event); // TS can alert consumers who forget the event argument
};
<EuiLink href={href} onClick={wrappedOnClick}>Click me</EuiLink> The main issue with that approach is that the expected
Issue with this option is that many apps got cyclic links (appA links to appB which also links to appA). Exposing @ all
Overall:
I have no issue implementing a As the component will need to call a core API ( Which mean the actual usage would be either using a direct reference, forcing to pass down the core service alongs all the react component tree: <CrossAppLink navigateToUrl={core.application.navigateToUrl} url="/base-path/app/dashboard/path"/> <CrossAppLink navigateToApp{core.application.navigateToApp} app="dashboard" path="/some=path"/> Or using a context provider (which is probably the best option, even if it also has drawbacks, such as harder testability) <CrossAppLinkContext navigateToUrl={core.application.navigateToUrl}>
...
<CrossAppLink url="/base-path/app/dashboard/path"/>
...
<CrossAppLinkContext /> <CrossAppLinkContext navigateToApp={core.application.navigateToApp}>
...
<CrossAppLink app="dashboard" path="/some=path" />
...
<CrossAppLinkContext /> |
My main concern about the 'global' solution was that it would be truly be global. if its opt in then I see no problem. |
I think we're still not quite aligned on this API, this is how I imagined it being used: <RedirectCrossAppLinks
navigateToUrl={coreStart.application.navigateToUrl}>
{/** These would automatically get handled by RedirectCrossAppLinks */}
<a href={`/app/dashboards/${dashboardId}`}>Go to Dashboard</a>
<EuiLink href={`/app/dashboards/${dashboardId}`}>Go to Dashboard</EuiLink>
</RedirectCrossAppLinks> It would be up to the app to wire
Are there any issues with this approach I'm missing? If I'm still not clear, I can do a quick POC as well. |
Sorry if I was unclear. The If everyone is fine with the As I already said on slack, the only thing I don't like about this option is that doesn't quite follow the React way of doing things (global handlers and side effects are not really in React's philosophy). However this is not a technical limitation, and I have to admit this option is the easier to use for consumers, so If no one is opposed, I'm fine trying it. |
Got it, after re-reading, it was clear and I just missed it 😄 |
The |
#67595 has been merged, however I'll keep this issue open for discussion on the eventual additional tools we could provide as alternatives to |
Closing. Feel free to reopen if you think the discussion can continue. |
Follow up issue from discussion in #58217
Currently, the Platform only supports SPA navigation between applications via the
navigateToApp
API. One problem with this API is that in order for applications to support both regular clicks and "open in new tab" (either via ctrl-click or right click -> open in new tab), each app has to implement a component similar toreact-router-dom
's<Link />
component.We could improve this quite a bit, and we have at least two options:
<CrossAppLink />
component inkibana_react
that can call thenavigateToApp
andgetUrlForApp
APIs to create a valid href that handles both in-app navigations and open in new tab navigations.The text was updated successfully, but these errors were encountered: