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

Add functionality to update browser url with parameters in order for users bookmark their views and settings #811

Closed
MarcSkovMadsen opened this issue Nov 24, 2019 · 9 comments
Labels
type: enhancement Minor feature or improvement to an existing feature
Milestone

Comments

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Nov 24, 2019

Introduction

I'm trying to build a multipage app using Panel.

My applications are typically some where the user can navigate and configure settings to get "views" most relevant to their specific use case like navigating to a "flow model" and filtering on the country "Norway".

When they have their view of interest they would like to be able to quickly navigate back to that specific view or share their view.

I belive the best way to enable this is to be able to bookmark a "view" and share at direct link. This is currently only partically supported in Panel (see workarounds below).

Solution

Add an Url widget to keep the "view" in sync with the url shown in the browser.

The api to set the browser url could be something like

url = pn.widgets.Url(
    relative_url = "flow-model",
    parameters = {"country": "Norway"}
)

There would be access to get and set the parameters including the full absolute_url

relative_url = url.relative_url # flow-model
absolute_url = url.absolute_url # https://example.com/flow-model`
parameters = url.parameters # {"country": "Norway"}

and the url can be watched for changes and handled in both Python and Javascript using all the normal functionality like link, jslink and watch.

Workarounds

  1. You can serve multiple apps in Panel to get the example.com/flow-model effect. But then you no longer have an integrated, single page application.

  2. You can use code to generate a link with parameters and provide it to the user in a textbox or maybe via a "share" button. And then access the parameters the next time the user opens the page via

image

  1. You can use Javascript to set the url
window.location.href = "https://example.com/flow-model/?country=Norway";

This should be useable allready now in panel. I just don't know how to watch this without creating a custom widget. And I have not learned how to do that yet. Maybe I should?

I can also get the url in Javascipt

myVar = window.location.href;

Then I need to figure out how to get it in Python?

Additional Context

If for some reason changing the relative url is not a good idea just being able to set the url parameters in the browser would be very usefull on it's own. Then the page name could just be a parameter like example.com/?page=flow-model&country=Norway.

@MarcSkovMadsen MarcSkovMadsen added the TRIAGE Default label for untriaged issues label Nov 24, 2019
@MarcSkovMadsen MarcSkovMadsen changed the title Add functionality to update browser url with parameters Add functionality to update browser url with parameters in order for users to save bookmarks Nov 24, 2019
@MarcSkovMadsen MarcSkovMadsen changed the title Add functionality to update browser url with parameters in order for users to save bookmarks Add functionality to update browser url with parameters in order for users bookmark their views and settings Nov 24, 2019
@MarcSkovMadsen
Copy link
Collaborator Author

MarcSkovMadsen commented Nov 26, 2019

I've created a prototype to keep the keep a Parametrized Class in sync with the browser url parameters. This might be usefull for someone therefore I share it here.

image

pnx.py

"""This module contains functionality to keep a param.Parametrized class
in sync with the browser url


"""
import json
import urllib

import param

import panel as pn


class UrlMixin:
    """This Mixin enables parameters from a param.Parametrized Class to be shown in the url or the
    browser. http://example

    Mix this class with the param.Parametrized class and add the set_browser_url_parameters HTML pane to
    your app in order to get url updates

    Example use case

    class _Country(pnx.param.UrlMixin, param.Parameterized):
        country = param.String()

        @param.depends("country")
        def set_browser_url_parameters(self):
            return super().set_browser_url_parameters()
    """

    def __init__(self):
        """Initializes from the browser url parameters"""
        for key, value in pn.state.session_args.items():
            if key in self._parameter_dict():
                value_str = value[0].decode("utf8")
                self.set_param(key, value_str)

    def set_browser_url_parameters(self) -> pn.pane.HTML:
        """A HTML Pane. Should be included in the app in order
        to update the browser url when a parameter changes.

        Returns:
            pn.pane.HTML -- A pane containing the javascript script to update the browser url
        """
        return pn.pane.HTML(self._browser_url_parameters_script())

    def _browser_url_parameters_script(self) -> str:
        if len(self.get_param_values()) > 1:
            state = f'{{param: "{self._urlencode()}"}}'
            title = ""
            url = f"?{self._urlencode()}"

            return f"""<script>window.history.replaceState({state},"{title}","{url}");</script>"""
        return ""

    def _parameter_dict(self):
        return {item[0]: item[1] for item in self.get_param_values() if item[0] != "name"}

    def _urlencode(self):
        return urllib.parse.urlencode(self._parameter_dict())

and pytests and a manual app to test

test_url.py

"""Tests of the awesome_panel functionality"""
import importlib

import param

import pnx
import panel as pn


class _Country(pnx.param.UrlMixin, param.Parameterized):
    country = param.String()

    @param.depends("country")
    def set_browser_url_parameters(self):
        return super().set_browser_url_parameters()


def test_url():
    country_url = _Country()
    country_url.set_param(country="Denmark")

    assert country_url._parameter_dict() == {"country": "Denmark"}
    assert country_url._urlencode() == "country=Denmark"
    assert (
        country_url._browser_url_parameters_script()
        == '<script>window.history.replaceState({param: "country=Denmark"},"","?country=Denmark");</script>'
    )


def test_pn_url():
    """Manual Test"""
    # Given
    country_url = _Country()
    text = """
Manual Tests

- opening [http://localhost:5006/test_url](http://localhost:5006/test_url) works without error
- opening [http://localhost:5006/test_url?country=](http://localhost:5006/test_url?country=) works without error
- opening [http://localhost:5006/test_url?country=Denmark](http://localhost:5006/test_url?country=Denmark) then the country widget parameter is set to Denmark
- Changing the country widget parameter to Norway changes the browser url to
[http://localhost:5006/test_url?country=Norway](http://localhost:5006/test_url?country=Norway)
    """
    panel = pn.Column(country_url.param, text, country_url.set_browser_url_parameters)
    panel.servable("test")


if __name__.startswith("bk_script"):
    test_pn_url()

@philippjfr philippjfr added type: enhancement Minor feature or improvement to an existing feature and removed TRIAGE Default label for untriaged issues labels Nov 29, 2019
@MarcSkovMadsen
Copy link
Collaborator Author

I've just simplified and restated my feature request below.

I would like to be able to keep the server app state in sync with the client app state via the browser url. I.e.

  • If the user/ client navigates to a url my app state should be updated using the full url including parameters.
  • If I change my app state the parameters of my state should be available in the browser url for bookmarking and sharing.

@jbednar
Copy link
Member

jbednar commented Dec 4, 2019

Making it simple to set parameters using URL arguments seems like a great idea and provides a lot of power.

@philippjfr
Copy link
Member

I think we should add a bokeh model which syncs the URL parameters and then add it automatically. An API I could imagine is something like:

pn.state.url_params.sync(value=some_parameterized_object.param.value)

@philippjfr philippjfr added this to the v0.8.0 milestone Dec 5, 2019
@jbednar
Copy link
Member

jbednar commented Dec 5, 2019

Sounds good to me!

@philippjfr philippjfr modified the milestones: v0.8.0, v0.9.0 Jan 16, 2020
@MarcSkovMadsen
Copy link
Collaborator Author

MarcSkovMadsen commented Feb 16, 2020

Some inspiration might be found from the JS window.location component https://css-tricks.com/snippets/javascript/get-url-and-url-parts-in-javascript/

and the Dash location component

https://dash.plot.ly/dash-core-components/location

I would say we should provide a Location component with the same properties as the JS window.location component and then some easy functionality to

  1. Functionality to get and set the search value from a dictionary
  2. Functionality to get and set the search value from a parameterized class and a subset of its parameters.
    • This functionality could both be manual (i.e. manually use function to do this) or automatic (via param.depends or similar).
    • Maybe the functionality should be able to take a list of parameterized classes and subsets?

@MarcSkovMadsen
Copy link
Collaborator Author

I've added a Work in Progress pull request for this at #1101

@MarcSkovMadsen
Copy link
Collaborator Author

I think we should add a bokeh model which syncs the URL parameters and then add it automatically. An API I could imagine is something like:

pn.state.url_params.sync(value=some_parameterized_object.param.value)

I don't understand this api so thats why this is not what I've been providing code for.

@philippjfr
Copy link
Member

The location component has been merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement Minor feature or improvement to an existing feature
Projects
None yet
Development

No branches or pull requests

3 participants