ShinyProxy + Dash pages_plugin not redirecting as expected

I know this isn’t necessarily a purely dash question as it involves ShinyProxy but I was hoping someone would have some insight:

x-post : ShinyProxy and Dash pages_plugin not redirecting as expected · Issue #369 · openanalytics/shinyproxy · GitHub

I am trying to create a multi-page app using a new feature introduced in Dash 2 called “Pages” and host it through ShinyProxy More info here on Dash’s new Pages functionality

While most of it works as expected using the demo dash application on ShinyProxy plus the minimal examples provided by Dash, the only issue I’m having is when it comes to first page you see once the container is loaded (i.e. path ‘/’ for the dash app or home.py below)

app.py

import dash
import dash_bootstrap_components as dbc
import dash_labs as dl
import flask
import os

server = flask.Flask(__name__)

app = dash.Dash(__name__,
                plugins=[dl.plugins.pages],
                server=server,
                suppress_callback_exceptions=True,
                routes_pathname_prefix= os.environ['SHINYPROXY_PUBLIC_PATH'],
                requests_pathname_prefix= os.environ['SHINYPROXY_PUBLIC_PATH'],
                external_stylesheets=[dbc.themes.FLATLY, dbc.icons.FONT_AWESOME],
                )

navbar = dbc.NavbarSimple(
    dbc.DropdownMenu(
        [
            dbc.DropdownMenuItem(page["name"], href=page["path"])
            for page in dash.page_registry.values()
            if page["module"] != "pages.not_found_404"
        ],
        nav=True,
        label="More Pages",
    ),
    brand="Multi Page App Plugin Demo",
    color="light",
    dark=False,
)

app.layout = dbc.Container(
    [navbar, dl.plugins.page_container],
    className="dbc",
    fluid=True,
)

if __name__ == '__main__':


    app.run_server(debug=True,
                   use_reloader=True,
                   host='0.0.0.0',
                   port=8050
    )

Meanwhile, the pages directory contains two pages:

home.py

import dash
from dash import html
import os

dash.register_page(__name__, path=os.environ['SHINYPROXY_PUBLIC_PATH'])

def layout():
    # ...
    return html.Div(html.H1(['Home Baby Home']))

vendor.py

import dash
from dash import html, dcc
import os
import pandas as pd
import plotly.graph_objects as go

dash.register_page(__name__, path='vendor')


df = pd.read_csv(
    'https://gist.githubusercontent.com/chriddyp/' +
    '5d1ea79569ed194d432e56108a04d188/raw/' +
    'a9f9e8076b837d541398e999dcbac2b2826a81f8/'+
    'gdp-life-exp-2007.csv')


layout = html.Div([
    dcc.Graph(
        id='life-exp-vs-gdp',
        figure={
            'data': [
                go.Scatter(
                    x=df[df['continent'] == i]['gdp per capita'],
                    y=df[df['continent'] == i]['life expectancy'],
                    text=df[df['continent'] == i]['country'],
                    mode='markers',
                    opacity=0.7,
                    marker={
                        'size': 15,
                        'line': {'width': 0.5, 'color': 'white'}
                    },
                    name=i
                ) for i in df.continent.unique()
            ],
            'layout': go.Layout(
                xaxis={'type': 'log', 'title': 'GDP Per Capita'},
                yaxis={'title': 'Life Expectancy'},
                margin={'l': 40, 'b': 40, 't': 10, 'r': 10},
                legend={'x': 0, 'y': 1},
                hovermode='closest'
            )
        }
    )
])

The expectation above would be that when I open the app, the first thing I see would be in home.py as its path is positioned at “/ (or shiny proxy’s public path)”. However this is not the case, and instead I get a 404. However the ‘vendor’ path defined in vendor.py works as expected.

Screenshot of Home (problematic)
image

Screenshot of Vendor (works as expected)
image

Any ideas on why the opening page is not pointing towards home.py?

Hey @uns123

That’s cool that the pages/ plug-in (almost) works with ShinyProxy :tada:

What happens if you change:

dash.register_page(name, path=os.environ[‘SHINYPROXY_PUBLIC_PATH’])

to

dash.register_page(name, path="/")

It would be helpful to see how the path and the relative_path are defined in the dash.page_registry

You could try out the new utility in dash labs to display the content of the dash.page_registry : print_registry()

Try adding print_registry() to home.py like this:

import dash
from dash import html
import os
from dash_labs import print_registry

dash.register_page(__name__, path=os.environ['SHINYPROXY_PUBLIC_PATH'])

print_registry(__name__, exclude="layout")

def layout():
    # ...
    return html.Div(html.H1(['Home Baby Home']))

Hi @AnnMarieW - big fan of your work :slight_smile:

What happens if you change:

dash.register_page(name, path=os.environ[‘SHINYPROXY_PUBLIC_PATH’])

to

dash.register_page(name, path=“/”)

Changing the “path” to “/” shows the intended output at first load i.e. when I initially navigate to the page I see this:

However, any subsequent visits to the home page using the navbar led to this error

dash.exceptions.UnsupportedRelativePath: Paths that aren't prefixed with requests_pathname_prefix are not supported.
You supplied: / and requests_pathname_prefix was /app_direct_i/savings_new/_/

The print_registry function when the path is “/” prints out:

{'pages.home': {'module': 'pages.home',
                'supplied_path': '/',
                'path_template': None,
                'path': '/',
                'supplied_name': None,
                'name': 'Home',
                'supplied_title': None,
                'title': 'Home',
                'description': '',
                'order': 0,
                'supplied_order': None,
                'supplied_layout': None,
                'image': None,
                'supplied_image': None,
                'image_url': None,
                'redirect_from': None}}

The print_registry function when the path is “os.environ[‘SHINYPROXY_PUBLIC_PATH’]” doesn’t print out anything since it never navigates to that page (shows 404 instead) as pointed out in the original post.

print_registry for the Vendor page yields:

{'pages.vendor': {'module': 'pages.vendor',
                  'supplied_path': 'vendor',
                  'path_template': None,
                  'path': 'vendor',
                  'supplied_name': None,
                  'name': 'Vendor',
                  'supplied_title': None,
                  'title': 'Vendor',
                  'description': '',
                  'order': None,
                  'supplied_order': None,
                  'supplied_layout': None,
                  'image': None,
                  'supplied_image': None,
                  'image_url': None,
                  'redirect_from': None}}

I tried changing the href in the navbar from:

navbar = dbc.NavbarSimple(
    dbc.DropdownMenu(
        [
            dbc.DropdownMenuItem(page["name"], href=page["path"])
            for page in dash.page_registry.values()
            if page["module"] != "pages.not_found_404"
        ],
        nav=True,
        label="More Pages",
    ),
    brand="Multi Page App Plugin Demo",
    color="light",
    dark=False,
)

to

navbar = dbc.NavbarSimple(
    dbc.DropdownMenu(
        [
            dbc.DropdownMenuItem(page["name"], href=os.environ['SHINYPROXY_PUBLIC_PATH'] + page["path"])
            for page in dash.page_registry.values()
            if page["module"] != "pages.not_found_404"
        ],
        nav=True,
        label="More Pages",
    ),
    brand="Multi Page App Plugin Demo",
    color="light",
    dark=False,
)

and in home.py I set to path="/" and everything works as expected - both at initial load as well as through the navbar!

I am going to play around with parameters in the URL to see if that works as expected and will report back.

Thanks again @AnnMarieW!

Hi @uns123

Thanks for your kind words :blush:

Your examples were helpful! Sorry I missed this earlier, but I see now that os.environ['SHINYPROXY_PUBLIC_PATH'] is the pathname prefix.

Rather than getting the correct path like this:

href=os.environ['SHINYPROXY_PUBLIC_PATH'] + page["path"])

You can use app.get_relative_path() (or dash.get_relative_path() if you need to access this from within the pages folder)

href= app.get_relative_path(page["path"]))

And this new feature is coming soon in dash 2.5:

#in dash 2.5:
href= page["relative_path"]

When you register the page, specify the local path without the prefix. For example, here’s the home page:

dash.register_page(__name__ , path="/")

For other pages, if the path will be the same as the module name, you can leave it bank, since it will be inferred. For example, in vendor.py you can just use:

dash.register_page(__name__)

Then the path will be /vendor . If you supply your own pathname, be sure to include the “/” before the name.


Here is more information on the get_relative_path() and strip_relative_path() functions:

get_relative_path()

    Return a path with `requests_pathname_prefix` prefixed before it.
    Use this function when specifying local URL paths that will work
    in environments regardless of what `requests_pathname_prefix` is.
    In some deployment environments, like Dash Enterprise,
    `requests_pathname_prefix` is set to the application name,
    e.g. `my-dash-app`.
    When working locally, `requests_pathname_prefix` might be unset and
    so a relative URL like `/page-2` can just be `/page-2`.
    However, when the app is deployed to a URL like `/my-dash-app`, then
    `app.get_relative_path('/page-2')` will return `/my-dash-app/page-2`.
    This can be used as an alternative to `get_asset_url` as well with
    `app.get_relative_path('/assets/logo.png')`
    Use this function with `app.strip_relative_path` in callbacks that
    deal with `dcc.Location` `pathname` routing.
    That is, your usage may look like:
    ```
    app.layout = html.Div([
        dcc.Location(id='url'),
        html.Div(id='content')
    ])
    @app.callback(Output('content', 'children'), [Input('url', 'pathname')])
    def display_content(path):
        page_name = app.strip_relative_path(path)
        if not page_name:  # None or ''
            return html.Div([
                dcc.Link(href=app.get_relative_path('/page-1')),
                dcc.Link(href=app.get_relative_path('/page-2')),
            ])
        elif page_name == 'page-1':
            return chapters.page_1
        if page_name == "page-2":
            return chapters.page_2
    ```
    """ 


def strip_relative_path(path):
    
    Return a path with `requests_pathname_prefix` and leading and trailing
    slashes stripped from it. Also, if None is passed in, None is returned.
    Use this function with `get_relative_path` in callbacks that deal
    with `dcc.Location` `pathname` routing.
    That is, your usage may look like:
    ```
    app.layout = html.Div([
        dcc.Location(id='url'),
        html.Div(id='content')
    ])
    @app.callback(Output('content', 'children'), [Input('url', 'pathname')])
    def display_content(path):
        page_name = app.strip_relative_path(path)
        if not page_name:  # None or ''
            return html.Div([
                dcc.Link(href=app.get_relative_path('/page-1')),
                dcc.Link(href=app.get_relative_path('/page-2')),
            ])
        elif page_name == 'page-1':
            return chapters.page_1
        if page_name == "page-2":
            return chapters.page_2
    ```
    Note that `chapters.page_1` will be served if the user visits `/page-1`
    _or_ `/page-1/` since `strip_relative_path` removes the trailing slash.
    Also note that `strip_relative_path` is compatible with
    `get_relative_path` in environments where `requests_pathname_prefix` set.
    In some deployment environments, like Dash Enterprise,
    `requests_pathname_prefix` is set to the application name, e.g. `my-dash-app`.
    When working locally, `requests_pathname_prefix` might be unset and
    so a relative URL like `/page-2` can just be `/page-2`.
    However, when the app is deployed to a URL like `/my-dash-app`, then
    `app.get_relative_path('/page-2')` will return `/my-dash-app/page-2`
    The `pathname` property of `dcc.Location` will return '`/my-dash-app/page-2`'
    to the callback.
    In this case, `app.strip_relative_path('/my-dash-app/page-2')`
    will return `'page-2'`
    For nested URLs, slashes are still included:
    `app.strip_relative_path('/page-1/sub-page-1/')` will return
    `page-1/sub-page-1`
    ```
    """
    


2 Likes