Skip to content
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

Merged
merged 23 commits into from
Jan 13, 2020

Conversation

majagrubic
Copy link
Contributor

@majagrubic majagrubic commented Dec 16, 2019

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 strikethroughs to remove checklist items you don't feel are applicable to this PR.

For maintainers

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');
Copy link
Contributor Author

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?

@majagrubic majagrubic changed the title First version of adding Lens to dashboard [Lens][Dashboard]First version of adding Lens to dashboard Dec 16, 2019
@majagrubic majagrubic changed the title [Lens][Dashboard]First version of adding Lens to dashboard [Lens][Dashboard] Adding Lens to Dashboard Dec 16, 2019
if (this.props.editorParams && this.props.editorParams.includes('addToDashboard')) {
params = `${params}?addToDashboard`;
Copy link
Contributor Author

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]) {
Copy link
Contributor

@timroes timroes Dec 16, 2019

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?

Copy link
Contributor Author

@majagrubic majagrubic Dec 16, 2019

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.)

Copy link
Contributor Author

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.

@majagrubic
Copy link
Contributor Author

Jenkins, test this

@majagrubic majagrubic marked this pull request as ready for review December 19, 2019 07:10
@majagrubic majagrubic requested a review from a team December 19, 2019 07:10
@majagrubic majagrubic added release_note:roadmap Feature:Dashboard Dashboard related features Feature:Lens Team:Visualizations Visualization editors, elastic-charts and infrastructure and removed Feature:Lens labels Dec 19, 2019
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-app (Team:KibanaApp)

Copy link
Contributor

@wylieconlon wylieconlon left a 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
Copy link
Contributor

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}`);
Copy link
Contributor

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;
}
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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.

@majagrubic
Copy link
Contributor Author

@elasticmachine merge upstream

Copy link
Contributor

@chrisdavies chrisdavies left a 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 {
Copy link
Contributor

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;
Copy link
Contributor

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?

Copy link
Contributor Author

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 += '?';
Copy link
Contributor

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}`;
Copy link
Contributor

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) {
Copy link
Contributor

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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;
Copy link
Contributor

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);
  }

Copy link
Contributor Author

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\/.*\?)(.*)/;

Copy link
Contributor

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;

Copy link
Contributor Author

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.

@majagrubic
Copy link
Contributor Author

Thanks @wylieconlon and @chrisdavies for your comments. I've applied some of the comments, especially around url_helper living inside the dashboard code and better error handling.
Regarding the URL handling - my idea was to create a simple and platform-agnostic way to handle URLs, without any dependencies, so parsing urls with regex seemed like a way to go. I've added a dependency to legacy platform to handle URLs without any regexes, so that it's more standard now. I'm not super happy with that, but interested to see what you think.

@majagrubic majagrubic force-pushed the add-lens-to-dash branch 2 times, most recently from e6995d7 to 25010ff Compare January 10, 2020 22:26
@majagrubic
Copy link
Contributor Author

@elasticmachine merge upstream

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@flash1293
Copy link
Contributor

Found a separate bug when looking into this (but maybe it makes sense to fix it together with this PR):

lens background change

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

Copy link
Contributor

@flash1293 flash1293 left a 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;
Copy link
Contributor

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debugging leftover?

Copy link
Contributor Author

@majagrubic majagrubic Jan 13, 2020

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) {
Copy link
Contributor

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-ignores.

Copy link
Contributor Author

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:

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}`;
}

/**
Copy link
Contributor

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!

@majagrubic
Copy link
Contributor Author

majagrubic commented Jan 13, 2020

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.

@majagrubic majagrubic merged commit 7543b0c into elastic:master Jan 13, 2020
majagrubic pushed a commit to majagrubic/kibana that referenced this pull request Jan 13, 2020
* 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 <elasticmachine@users.noreply.github.com>
@majagrubic majagrubic deleted the add-lens-to-dash branch January 13, 2020 17:53
majagrubic pushed a commit that referenced this pull request Jan 13, 2020
* 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 <elasticmachine@users.noreply.github.com>

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
jkelastic pushed a commit to jkelastic/kibana that referenced this pull request Jan 17, 2020
* 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 <elasticmachine@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature:Dashboard Dashboard related features Team:Visualizations Visualization editors, elastic-charts and infrastructure v7.6.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants