-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
[Lens][Dashboard] Adding Lens to Dashboard #53110
Conversation
0b089a3
to
dcaa202
Compare
routeProps.history.push(`/lens/edit/${id}`); | ||
} else if (addToDashMode && id) { | ||
routeProps.history.push(`/lens/edit/${id}`); | ||
const url = context.core.chrome.navLinks.get('kibana:dashboard'); |
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 is adding more dependencies on context
, which is being deprecated. Is there a better way to do this?
if (this.props.editorParams && this.props.editorParams.includes('addToDashboard')) { | ||
params = `${params}?addToDashboard`; |
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 means maps will be called with addToDashboard
param. If that's an issue, this can be an explicit check for Lens.
@@ -319,6 +319,14 @@ export class DashboardAppController { | |||
kbnUrl.removeParam(DashboardConstants.ADD_VISUALIZATION_TO_DASHBOARD_MODE_PARAM); | |||
kbnUrl.removeParam(DashboardConstants.NEW_VISUALIZATION_ID_PARAM); | |||
} | |||
if ($routeParams[DashboardConstants.NEW_LENS_VISUALIZATION_ID_PARAM]) { |
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 wonder if it would make sense to generalize this URL directly. So that you can pass the embeddable type and the embeddable id via a URL instead of creating a specific case for each embeddable here.
Another advantage is, that we currently have references to x-pack specific code in OSS, that way. Even if it's not code links, but just referencing specific embeddable ids and Lens by name, we usually try not to have those references. With a generic URL we wouldn't need any reference to x-pack specific code here.
Also -- especially since we don't have a good "URL service" to navigate between apps right now -- I like an idea that the observability plugin is taking, that they create short URLs for those kind of links, and then just internally forward to the long URL, so that those short URLs can stay forever the same even if something internally needs to change. So I wonder if we should have a URL in the format /dashboard/add/<dashboard-id>/<embeddable-type>/id/<embeddable-id>
, and make this forward to the appropriate methods internally. (I btw added the id
here explicitly, since I think we later want to be able to add also embeddables by their full embeddable config instead of saved embeddables via that way.)
@majagrubic what are your thoughts on that?
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 like this idea. But to be sure I understand what you are saying - writing the URL in such a way would allow for a more generic way of adding embeddables, and solves the problem of having Lens reference inside Dashboard. The problem of "not having a good URL service to navigate between apps" still remains, as well as the idea of updating window.location
directly.
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.
Additionally, we'd need to handle the case when there is no dashboard-id
, as the dashboard hasn't yet been saved.
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.
Yes I totally agree. This does not solve the generic problem of us needing to build proper URL services in the long run. So this is just an easier way to keep URLs stable before we have those kind of URL services. (And even they might want to use those short URLs in the long run.)
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 made a code change, so that embeddableType
and embeddableId
are more generic, but still passed as query params. Let me know if this is more along the lines what you were thinking.
4898615
to
346d1fb
Compare
Jenkins, test this |
x-pack/test/functional/apps/dashboard_mode/dashboard_empty_screen.js
Outdated
Show resolved
Hide resolved
Pinging @elastic/kibana-app (Team:KibanaApp) |
377fd2b
to
8022cea
Compare
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.
Functionality LGTM, left a nitpicky comment about the use of regexes. I don't quite understand the URL context question as I haven't worked with it personally.
const EMPTY_DASHBOARD_PATTERN = /(.*#\/dashboard\?)(.*)/; | ||
const DASHBOARD_WITH_ID_PATTERN = /(.*#\/dashboard\/.*\?)(.*)/; | ||
const TIME_PATTERN_1 = /(.*)(,time:[^)]+\))(.*)/; | ||
const TIME_PATTERN_2 = /(.*)(time:[^)]+\),)(.*)/; // same as TIME_PATTERN_1, but comma follows, not preceeds |
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.
Can we use rison
to parse these instead of regexes? I think the code will be more readable.
await dashboardVisualizations.ensureNewVisualizationDialogIsShowing(); | ||
await createAndAddLens(title); | ||
await PageObjects.dashboard.waitForRenderComplete(); | ||
await testSubjects.exists(`embeddablePanelHeading-${title}`); |
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.
Nice!
vars[key] = value; | ||
}); | ||
return vars; | ||
} |
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.
When I was looking at use of rison in our code, I also found use of the url
library and some other Kibana-specific URL parsing. Both this function and the previous function seem to be common functions in our code, can we simplify them?
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.
There is the old kibanaUrl
thing, but that doesn't work in new platform afaik. Standard URL api would be the best, but that doesn't work in IE11 :(
Anything specific you had in mind?
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 was hoping to find a way to get rid of the regexes and manual construction of URLs in favor of a standardized way. I'm not very familiar with the setup here, but it is still my assumption that there is a cleaner way of doing. I don't want to block this work over this concern though.
@elasticmachine merge upstream |
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.
Nice work, Maja. I think this probably still needs a bit of work, but I probably won't be around to approve that work in time, so I'm only commenting (not rejecting).
* output: http://localhost:5601/lib/app/kibana# | ||
* @param url | ||
*/ | ||
export function getKibanaBasePathFromDashboardUrl(url: string | undefined): string | null { |
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.
Rather than parsing this from the URL, shouldn't we just use Kibana's base path utilities? (e.g. core.http.basePath
or something like that)
): string { | ||
let dashboardUrl = getUrlWithoutQueryParams(absoluteUrl); | ||
if (!dashboardUrl) { | ||
return absoluteUrl; |
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 still add the query params to the absolute URL?
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.
No, the only thing where the previous function will return null is if the absoluteUrl
itself is null.
if (!dashboardUrl) { | ||
return absoluteUrl; | ||
} | ||
dashboardUrl += '?'; |
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 suspect, you'll want to use encodeURIComponent
on your keys and values here. Something like this:
const queryString = Object.entries(urlParams)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
return `${dashboardUrl}?${queryString}`;
} | ||
const base = regex[1]; | ||
const dashboardState = regex[2]; | ||
return `${base}${DashboardConstants.ADD_EMBEDDABLE_TYPE}=${embeddableType}&${DashboardConstants.ADD_EMBEDDABLE_ID}=${embeddableId}&${dashboardState}`; |
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.
You might want to escape the embeddableId here. Also, you may want to de-duplicate it, as it may already be embedded, and this'll duplicate it in the URL, if I'm not mistaken.
urlVars | ||
); | ||
const dashboardParsedUrl = addEmbeddableToDashboardUrl(dashboardUrl, id, 'lens'); | ||
if (dashboardParsedUrl) { |
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.
If this returns null, we silently fail. Is that the right thing? It seems like a null here is a legit error that should be surfaced in some way.
if (lastDashboardAbsoluteUrl && lensUrl) { | ||
const urlVars = getUrlVars(lastDashboardAbsoluteUrl); | ||
updateUrlTime(urlVars); | ||
window.history.pushState({}, '', lensUrl); |
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 think we generally avoid using pushState directly, and instead use a core service to do this, but I'm not sure. Might be worth looking into, anyway.
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 don't think it exists in the new platform. I think the pushState
is our only option for now.
if (!url) { | ||
return; | ||
} | ||
const lastDashboardAbsoluteUrl = url.url; |
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 also seems like a bunch of detail that belongs to the dashboard module, not to Lens. Maybe something like this:
const url = dashboardUrl.addEmbeddable(url.url, id, 'lens');
if (url) {
window.history.pushState({}, '', url);
}
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 was the original intention, but then we figured the time range is not properly set. So for the hack with setting timerange to work, this won't be so clean.
|
||
const EMPTY_DASHBOARD_PATTERN = /(.*#\/dashboard\?)(.*)/; | ||
const DASHBOARD_WITH_ID_PATTERN = /(.*#\/dashboard\/.*\?)(.*)/; | ||
|
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 url_helper module seems like it should live in src/legacy/core_plugins/kibana/public/dashboard
rather than in Lens. Lens really shouldn't know much about the internal details of dashboard URLs. The dashboard app should provide an API to adjust URLs. (This file could serve as that API.)
import { addEmbeddable } from 'dashboard_url';
// Usage example...
const url = addEmbeddable(someDashboardUrl, 'lens', id);
/// Etc..
const url = addEmbeddable(someDashboardUrl, 'visualization', id);
// We may want to allow callers to modify other things?
const url = setDateRange(someDashboardUrl, dateRange;
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 point. I wasn't sure where to put it, since Lens is the only thing that was using it.
Thanks @wylieconlon and @chrisdavies for your comments. I've applied some of the comments, especially around |
e6995d7
to
25010ff
Compare
@elasticmachine merge upstream |
💚 Build SucceededHistory
To update your PR or re-run it, just comment with: |
Found a separate bug when looking into this (but maybe it makes sense to fix it together with this PR): When the "Go to lens" link is used to open lens, it is 1. opening Lens without closing the modal (that also happens on master) and 2. Lens is not returning to the Dashboard once the visualization was saved. IMHO the "Go to Lens" link should ideally behave like the "Lens" visualization tile. I can also create a separate issue for this if it doesn't fit in this 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.
This looks good to me, tested and works as expected. Left some nits and a question that should not block this PR.
@@ -93,9 +102,60 @@ export class AppPlugin { | |||
http: core.http, | |||
}) | |||
); | |||
const updateUrlTime = (urlVars: Record<string, string>): void => { | |||
const decoded: RisonObject = rison.decode(urlVars._g) as RisonObject; |
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 piece of code can be simplified from a typing perspective:
const decoded = rison.decode(urlVars._g);
if (!isRisonObject(decoded)) {
return;
}
decoded.time = data.query.timefilter.timefilter.getTime();
urlVars._g = rison.encode(decoded);
.catch(() => { | ||
.catch(e => { | ||
// eslint-disable-next-line no-console | ||
console.dir(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.
Debugging leftover?
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 really: right now every exception is caught and a generic message "Unable to save document" is displayed, even if the error originates from trying to figure the dashboard URL to return to. I don't want to change this behavior now without figuring out proper error messages to be displayed to the user (if any), but thought it would be useful to have the error at least logged in case it occurs.
export function getUrlVars(url: string): Record<string, string> { | ||
const vars: Record<string, string> = {}; | ||
// @ts-ignore | ||
url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(_, key, value) { |
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 an interesting usage of replace
. However I think we can use matchAll
in this scenario:
for ([_, key, value] of url.matchAll(/[?&]+([^=&]+)=([^&]*)/gi)) {
vars[key] = value;
}
Then you don;t need any ts-ignore
s.
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 was taken from here:
kibana/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/utils.js
Line 374 in 8e9a8a8
function getUrlVars(url) { |
So I assumed it was well-tested. But I like any suggestion that gets rid of ts-ignore directives 👍
return `${protocol}//${host}${basePath}/app/kibana#/lens/edit/${id}`; | ||
} | ||
|
||
/** |
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.
Thanks for the documentation, that's very helpful!
Thanks @flash1293, I'll address your comments in a follow-up PR, with a fix for the dialog and a bit of a cleanup. I also pinged Alona regarding error messages. |
* First version of adding Lens to dashboard * Fix failing unit test * Replacing explicit Lens query param with a more generic one * Fixing failing unit test * Adding a unit test for redirect * Do not show Save New if adding from Dashboard * Adding functional test * Adding functional test * Fixing type issues * Renaming query params * Fixing failing unit test * Removing unused constants * Fixing erroneous imports * Fixing erroneous import * Fixing import * Fix failing typecheck * Removing timefilter from Dashboard URL * Fixing type error * Replacing time parsing with rison * Replacing URL regex parsing with legacy URLs * Fixing failing test Co-authored-by: Elastic Machine <[email protected]>
* First version of adding Lens to dashboard * Fix failing unit test * Replacing explicit Lens query param with a more generic one * Fixing failing unit test * Adding a unit test for redirect * Do not show Save New if adding from Dashboard * Adding functional test * Adding functional test * Fixing type issues * Renaming query params * Fixing failing unit test * Removing unused constants * Fixing erroneous imports * Fixing erroneous import * Fixing import * Fix failing typecheck * Removing timefilter from Dashboard URL * Fixing type error * Replacing time parsing with rison * Replacing URL regex parsing with legacy URLs * Fixing failing test Co-authored-by: Elastic Machine <[email protected]> Co-authored-by: Elastic Machine <[email protected]>
* First version of adding Lens to dashboard * Fix failing unit test * Replacing explicit Lens query param with a more generic one * Fixing failing unit test * Adding a unit test for redirect * Do not show Save New if adding from Dashboard * Adding functional test * Adding functional test * Fixing type issues * Renaming query params * Fixing failing unit test * Removing unused constants * Fixing erroneous imports * Fixing erroneous import * Fixing import * Fix failing typecheck * Removing timefilter from Dashboard URL * Fixing type error * Replacing time parsing with rison * Replacing URL regex parsing with legacy URLs * Fixing failing test Co-authored-by: Elastic Machine <[email protected]>
Summary
Adding Lens visualization to Dashboard, without navigating away from the dashboard. The idea is the same as for regular visualizations - use the url parameter to know we're in
addToDashboard
mode and then redirect properly. Redirect is a bit ugly, would love to get thoughts and feedback on this.Checklist
Use
strikethroughsto remove checklist items you don't feel are applicable to this PR.- [x] Any text added follows EUI's writing guidelines, uses sentence case text and includes i18n support-[x] Documentation was added for features that require explanation or tutorialsFor maintainers