šŸ“£ Introducing Dash `/pages` - A Dash 2.x Feature Preview

Iā€™ll take a look tomorrow @johnkangw ! I suspect itā€™ll be a quick fix somewhere in the routing code.

1 Like

@chriddyp Thanks so much!

1 Like

Hey @AnnMarieW

To double back on this, are there plans to integrate a solution to accessing the cache in callbacks outside the app.py file?

For example, I have multipage app that uses a structure similar to the ā€œCaching and Signalingā€ example in the docs. Once the expensive query is complete, callbacks on every page will be utilizing this same data source and need to access the global cache.

Since the cache is tied to the app object, I havenā€™t been able to make this work on a multipage setup without just putting all my callbacks inside the app.py file. This quickly becomes hard to navigate with a larger multipage app.

Iā€™m happy to put together a simple example if that wasnā€™t clear, thanks!

Hi @bigmike

Iā€™m not sure, but I added it to this Github issue since itā€™s related. Thanks for raising this issue. You can track the progress in Github.

1 Like

Awesome feature, thanks!

One pain Iā€™ve run into while using this feature is from tests. If I use pytest to do something like

from pages import my_page

def test_my_page():
    assert(4 == my_page.my_function())

I get AttributeError: module 'dash' has no attribute 'register_page' when running pytest. My workaround has been to wrap dash.register_page in a try-catch.

Hi guys,

Just started converting my dash app over to a multipage app and have a couple of questions.

Is it possible to go to another page from within a callback, preferable with a query string?
Can you access the query parameter from within a callback?
How do you use dcc.Loading so that we get loading icon while each page loads?

Thanks for any help, RIchard

Hi @Precog

The easiest way is to return a different layout based on the query string. Another option is to use path variables. You can find examples in the docs here: https://dashlabs.pythonanywhere.com/

There are two sample apps with query strings in dash-labs demos folder

  • multi_page_basics/ is a minimal example including both query strings and path variable.
  • multi_page_query_strings/ is an example of sharing query data between pages

One way to access query parameters from a callback is to store them in a dcc.Store component.

You can find more information on loading states here: Loading States | Dash for Python Documentation | Plotly

Thanks AnnMarieW.

Just in case anyone else came across the same issues as me, this is what I did:

1) Is it possible to go to another page from within a callback, , preferably with a query string?

I used dcc.Location in a callback to redirect to another page:
 return dcc.Location(pathname=f"/status-page", id="someid_doesnt_matter")

I tried adding a query string such as f"/status-page?unitname=unit" but i get a 404 error because the url ends up being /status-page%3funitname=unit . Any ideas how to get round this one?

2)Can you access the query parameter from within a callback?
I didnt try this but i presume you can add location as a state and get the query string in the callback e.g. State(ā€˜urlā€™, ā€˜pathnameā€™)

3) How do you use dcc.Loading so that we get loading icon while each page loads?
I ended up wrapping the page container as follows: dcc.Loading(dl.plugins.page_container)

Hope this helps anyone else.

1 Like

Hi, is there any update on the issue raised by johnkangw re UnsupportedRelativePath? I have the same problem although not using Dash Enterprise but proxying Flask through IIS - hence wsgi. Interestingly Iā€™m getting 200 returned on various routes before it fails on dash-update-component, e.g.

200 http://localhost/myapp/_dash-component-suites/dash/dash_table/bundle.v5_0_0m1644814682.js
200 http://localhost/myapp/_dash-dependencies
200 http://localhost/myapp/_dash-layout
500 http://localhost/myapp/_dash-update-component

This is awesome, but I am running into an issue:

I execute the below code and on my first attempt it works! I am able to have a dropdown and navigate to these pages successfully.

Printing out my dash.page_registry(values) successfully:

{ā€˜moduleā€™: ā€˜pages.heatmapsā€™, ā€˜supplied_pathā€™: ā€˜/ā€™, ā€˜path_templateā€™: None, ā€˜pathā€™: ā€˜/ā€™, ā€˜supplied_nameā€™: None, ā€˜nameā€™: ā€˜Heatmapsā€™, ā€˜supplied_titleā€™: None, ā€˜titleā€™: ā€˜Heatmapsā€™, ā€˜supplied_descriptionā€™: None, ā€˜descriptionā€™: ā€˜Heatmapsā€™, ā€˜orderā€™: 0, ā€˜supplied_orderā€™: None, ā€˜supplied_layoutā€™: None, ā€˜imageā€™: None, ā€˜supplied_imageā€™: None, ā€˜redirect_fromā€™: None, ā€˜layoutā€™: Div([P(ā€˜Medals included:ā€™), Checklist(id=ā€˜heatmaps-medalsā€™, options=[{ā€˜labelā€™: ā€˜goldā€™, ā€˜valueā€™: ā€˜goldā€™}, {ā€˜labelā€™: ā€˜silverā€™, ā€˜valueā€™: ā€˜silverā€™}, {ā€˜labelā€™: ā€˜bronzeā€™, ā€˜valueā€™: ā€˜bronzeā€™}], value=[ā€˜goldā€™, ā€˜silverā€™, ā€˜bronzeā€™]), Graph(id=ā€˜heatmaps-graphā€™)])}

{ā€˜moduleā€™: ā€˜pages.bar_chartsā€™, ā€˜supplied_pathā€™: None, ā€˜path_templateā€™: None, ā€˜pathā€™: ā€˜/bar-chartsā€™, ā€˜supplied_nameā€™: None, ā€˜nameā€™: ā€˜Bar chartsā€™, ā€˜supplied_titleā€™: None, ā€˜titleā€™: ā€˜Bar chartsā€™, ā€˜supplied_descriptionā€™: None, ā€˜descriptionā€™: ā€˜Bar chartsā€™, ā€˜orderā€™: None, ā€˜supplied_orderā€™: None, ā€˜supplied_layoutā€™: None, ā€˜imageā€™: None, ā€˜supplied_imageā€™: None, ā€˜redirect_fromā€™: None, ā€˜layoutā€™: Div([Dropdown(id=ā€˜dropdownā€™, clearable=False, options=[{ā€˜labelā€™: ā€˜Sunā€™, ā€˜valueā€™: ā€˜Sunā€™}, {ā€˜labelā€™: ā€˜Satā€™, ā€˜valueā€™: ā€˜Satā€™}, {ā€˜labelā€™: ā€˜Thurā€™, ā€˜valueā€™: ā€˜Thurā€™}, {ā€˜labelā€™: ā€˜Friā€™, ā€˜valueā€™: ā€˜Friā€™}], style={ā€˜widthā€™: ā€˜50%ā€™}, value=ā€˜Sunā€™), Graph(id=ā€˜bar-chartā€™)])}

{ā€˜moduleā€™: ā€˜pages.histogramsā€™, ā€˜supplied_pathā€™: None, ā€˜path_templateā€™: None, ā€˜pathā€™: ā€˜/histogramsā€™, ā€˜supplied_nameā€™: None, ā€˜nameā€™: ā€˜Histogramsā€™, ā€˜supplied_titleā€™: None, ā€˜titleā€™: ā€˜Histogramsā€™, ā€˜supplied_descriptionā€™: None, ā€˜descriptionā€™: ā€˜Histogramsā€™, ā€˜orderā€™: None, ā€˜supplied_orderā€™: None, ā€˜supplied_layoutā€™: None, ā€˜imageā€™: None, ā€˜supplied_imageā€™: None, ā€˜redirect_fromā€™: None, ā€˜layoutā€™: Div([Graph(id=ā€˜histograms-graphā€™), P(ā€˜Mean:ā€™), Slider(id=ā€˜histograms-meanā€™, marks={-3: ā€˜-3ā€™, 3: ā€˜3ā€™}, max=3, min=-3, value=0), P(ā€˜Standard Deviation:ā€™), Slider(id=ā€˜histograms-stdā€™, marks={1: ā€˜1ā€™, 3: ā€˜3ā€™}, max=3, min=1, value=1)])}

However, If I stop executing the code and retry the code, the dropdown values no longer appear and I cannot navigate to my pages. If I restart my IDE (Spyder for python on Windows), I am able to successfully re-run it.

No registry values are available to be printed

Screenshots attached:

Code:

import os
path =rā€™C:\Users\hellothere\Dropbox\test_multi_page - Copyā€™
os.chdir(path)

import dash # pip install dash
import dash_labs as dl # pip install dash-labs
import dash_bootstrap_components as dbc # pip install dash-bootstrap-components

Code from: dash-labs/docs/demos/multi_page_example1 at main Ā· plotly/dash-labs Ā· GitHub

app = dash.Dash(
name, plugins=[dl.plugins.pages], external_stylesheets=[dbc.themes.BOOTSTRAP]
)

for x in dash.page_registry.values():
print(x)

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=ā€œprimaryā€,
dark=True,
className=ā€œmb-2ā€,
)

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

if name == ā€œmainā€:
app.run_server(debug=False)


Hi @newuser357 and welcome back :slight_smile:

Thanks for trying out pages and Iā€™m glad you like it! Sorry you ran into this bug. The issue is that it canā€™t find the pages/ folder unless the app.py is run from the CWD.

This has already been reported, and as soon as the nice folks at Plotly approve my pull request to fix this, we can do another release.

Hey Dash community,

:mega: Iā€™m pleased to announce the latest release of dash-labs V1.0.3

pip install -U dash-labs

:confetti_ball: Bug Fix: No longer need to run app.py from cwd.
This version fixes a bug where the main app.py needed to be run from the current working directory else it couldnā€™t find the files in the pages/ folder. Thanks for reporting @newuser357 and @eiriklid in dash-labs issue #84

:confetti_ball: New feature Title and Description Updated Dynamically!

As requested by @raptorbrad and @bradley-erickson in dash-labs issue#74

Itā€™s now possible to update the title in the browser tab and the title and description in the meta tags dynamically with a function.

You can see an example of this live in the on-line dash-labs docs: Asset Analysis: inventory branch-1001

The easiest way to try all the \pages features locally is to copy the multi_page_basics folder from the dash labs demos directory and run app.py

Here is the pages/path_variables.py in the demo which updates the title and description dynamically based on the path variables.


pages/path_variables.py

import dash

def title(asset_id=None, dept_id=None):
    return f"Asset Analysis: {asset_id} {dept_id}"


def description(asset_id=None, dept_id=None):
    return f"This is the AVN Industries Asset Analysis: {asset_id} in {dept_id}"


dash.register_page(
    __name__,
    path_template="/asset/<asset_id>/department/<dept_id>",
    title=title,
    description=description,
    path="/asset/inventory/department/branch-1001",
)


def layout(asset_id=None, dept_id=None, **other_unknown_query_strings):
    return dash.html.Div(
        f"variables from pathname:  asset_id: {asset_id} dept_id: {dept_id}"
    )

Here is what the link looks like if you share a link to this page here or on social media:

The title and description will be updated if you change the variables in the path.

4 Likes

Thanks for putting so much effort into dash-labs @AnnMarieW! It is a great addition to dash and works beautifully!

I have trouble wrapping my head around something that might have been solved before, but I could not find any pointers:

I am trying to set up a dash-lab \pages app that enables users to upload a (potentially large, 500MB) data file to be stored temporarily and analyzed in different ways. I want to avoid actual authentication, but still I need some kind of user management to store the file temporarily in a user/session-specific folder.

Currently, I create a user/session id during the first call to the app.layout, similar to @chriddypā€™s example here: Github: dash-cache-signal-session, and store it in a hidden Div. However, due to the restrictions that the ā€˜nativeā€™ dash uploader component has, I want to use the dash-uploader component that supports writing uploaded files with an arbitrary size directly to a user-specific folder if a unique user/session id is provided. I am not sure, how to forward the unique session id created during the initial call of the main layout function to the pages. Does anyone know how to do this or has done this before?

MWE:

app.py:

import app_configuration
import dash
import dash_bootstrap_components as dbc
import dash_labs as dl
import dash_uploader as du
import uuid

from dash import dcc, html, Input, Output, State

app = dash.Dash(__name__,
                plugins=[dl.plugins.pages],
                suppress_callback_exceptions=True)

app.title = "app title"

du.configure_upload(app, "uploads", use_upload_id=True, upload_api=None, http_request_handler=None)

split_position = 2
navbar = dbc.Navbar(
    dbc.Container([
        dbc.Row(
            dbc.Col(dbc.NavbarBrand(html.B(app_configuration.APP_NAME), className="ms-2")),
            align="center",
            className="g-0",
        ),

        dbc.Row([
            dbc.NavbarToggler(id="navbar-toggler"),
            dbc.Collapse(
                dbc.Nav([
                    dbc.NavItem(dbc.NavLink(page['name'], href=page['path'], active="exact" if page["path"] == "/" else "partial")) if page["order"] != split_position else dbc.NavItem(dbc.NavLink(page['name'], href=page['path'], active="partial"),  className="me-auto") for page in dash.page_registry.values() if page["module"] != "pages.not_found_404" and "sub" not in page["module"]
                ], className="w-100"),
                id="navbar-collapse",
                is_open=False,
                navbar=True,),
            ], className="flex-grow-1"),
    ], fluid=True),
    dark=True,
    color="primary",
    fixed="top",
    sticky="top",
)


def layout():
    return dbc.Container([
        dcc.Location(id='url', refresh=False),

        html.Div(id="userid", hidden=False),

        html.Div([navbar], className="mb-2"),
        dl.plugins.page_container
    ], fluid=True, style={"position": "relative", "display": "block"})

app.layout = layout

@app.callback(
    Output('userid', 'children'),
    Input('url', 'pathname'),
    State('userid', 'children')
)
def create_user(url, user_id):
    """Create a unique user/session id and store it in a hidden div."""
    print("create_user:", url, user_id)
    if not user_id:
        user_id = str(uuid.uuid4())
    return user_id

if __name__ == "__main__":
    app.run_server(debug=True, port=5000)

page_1.py:

import dash
import dash_bootstrap_components as dbc
import dash_uploader as du
from dash import html

dash.register_page(__name__, top_nav=True, path="/uploader", order=1)

content = html.Div([
    du.Upload(id="uploader",
              text="Drag & Drop or click to select",
              filetypes=["txt", "csv", "xlsx"],
              max_file_size=512,
              upload_id=UNIQUE_USER_ID_GOES_HERE,
              max_files=1)
])

layout = dbc.Container([
    dbc.Row([
        dbc.Col([content])
    ])
], fluid=True, className="mb-3")

Hi @mawe and thanks for your kind words :blush:

This is a great question. You could try updating the layout of page_1.py in a callback with the Input of the ā€œuseridā€ component (which is typically a dcc.Store rather than a hidden div.

So page_1 might look something like:


import dash
import dash_bootstrap_components as dbc
import dash_uploader as du
from dash import html, callback, Input, Output

dash.register_page(__name__, top_nav=True, path="/uploader", order=1)


layout = dbc.Container([
    dbc.Row([
        dbc.Col(html.Div(id="content"))
    ])
], fluid=True, className="mb-3")


@callback(
    Output("content", "children"),
    Input("userid", "data"),
)
def update_layout(userid):
    return html.Div([
        du.Upload(id="uploader",
                  text="Drag & Drop or click to select",
                  filetypes=["txt", "csv", "xlsx"],
                  max_file_size=512,
                  upload_id=userid,
                  max_files=1)
    ])

I didnā€™t try this - could you let me know if it works?

1 Like

Thanks @AnnMarieW, it works! Actually, I was thinking something along what you proposed, but I never tried because I assumed that the somewhat dynamically generated dash-uploader component would not be available for callbacksā€¦ Next time, Iā€™ll better try these ideas :slight_smile: I also changed the hidden div to a dcc.Store, as you suggested. The hidden div was a leftover of my test prototype. Thanks again!

1 Like

So I have updated the package. and am trying the new example ( py the multi_page_basics folder

If I download that example as is - I donā€™t get any of the pages. If I then declare my path (in this case the dropbox app on my desktop), I see the expected result. But if I stop running the code and then run it again - I am missing all of the added pages.

I am assuming this is due to some path variable issue that I am not figuring out how to fix. Most likely its user error.

Updated package via pip update:

without declaring the path:


declaring the path in app.py:


after ending the code and restarting:
image

Amazing feature! I am using app.index_string to modify the default HTML Index Template to add Google Analytics to my app. When using the pages plugin it seems to completely ignore my index_string customization. Does anyone know what causes this and how I can still customize the HTML Index template while using this feature?

Hi @ryanf

Thanks for reporting! You are correct ā€“ I could verify that the pages plugin does not handle a customized index_string.

Iā€™m working on the PR to move pages to Dash and the good news is that it works in that version. The PR is very close to ready, and Iā€™m hoping it will be included in the next Dash release :crossed_fingers:

Iā€™ll open this as an issue in dash-labs so we can track it, but it will probably be on the back burner unless there is some big delay in getting it included in Dash.

1 Like

Hi @AnnMarieW! I appreciate you researching this! Great to hear that it should be resolved with the (hopefully soon) move to Dash. Thanks for all your hard work on pages! It really is an incredibly valuable feature!!

Thanks for your kind words @ryanf. :blush:
Iā€™m looking forward to seeing pages/ included in Dash