Add new dashboard apps dynamically to an existing flask app without restarting the app

I have an existing Flask app, which mostly uses dynamic components to load content. By dynamic I mean pages are not defined at the time of deployment of the Flask app, but can be added into a database at any time and these pages are displayed using custom routes using path components.

I want to do the same thing for dashboard pages. I need some mechanism by which I can add new dash apps to an existing flask app without restarting the running app. If I try this conventionally, I get this:

AssertionError: The setup method 'register_blueprint' can no longer be called on the application. It has already handled its first request, any changes will not be applied consistently.
Make sure all imports, decorators, functions, etc. needed to set up the application are done before running it.

How can I achieve this?

I don’t think this approach is supported in Dash. I believe the best approach would be to make a new deployment.

@fmdasher,

This actually may be possible. Is your goal to be able to create new page target, or just change the layout inside on each call?

^^ irrelevant question

You can use the location pathname to determine which page you are going to call from your database, assuming the url is the key. Then you return the page.

@app.callback (
Output("page-content"),
Input("url","pathname")
)
def updatePage(pathname):
     if "myCusomPage" in pathname:
          return pathname.split("myCusomPage/")[1]
     return "this is a fallback catch"

Nice, seems like that might work. :slightly_smiling_face:
I hope this will still work when multiple clients have loaded multiple URLs? And I hope there won’t be any security issues since all of them are running off the same base app…?

Edit: what if I need to register separate callback functions for each ‘page’ loaded from my db?

You might be able to utilize the flask_login to determine the user.

Having a function that creates layouts and callbacks is not easy. Plus you have to check for duplicate callbacks before creating another one.

Dash does not support adding callbacks dynamically, hence my previous answer. But if you only need to add content dynamically, you could simply serve the content from an external source (e.g. an S3 bucket); then you would be able to change the content dynamically by modifying the externally hosted data.

@Emil,

This is where I would disagree. Getting the calls to manipulate data on the serverside would be the interesting part.

You can add dynamic clientside_callbacks when creating the layout. I know, because I have done this. The interesting part is making sure when you reload your page that you dont try to write over the same callback. Which takes extra effort to make sure that you dont have two buttons trying to use the same id with a different function.

My point was aimed mainly at normal (Python) callbacks. These callbacks interact with the Flask backend, which is stateless by design. Adding callbacks to the backend at runtime would violate this principle, similar to registering a blueprint which was fmdasher tried to do in the beginning, and is thus not allowed/supported.

Clientside callback on the other hand are essentially just JS code, so I guess it might be possible to add them dynamically, though I haven’t tried it myself. Do you have a link to some docs on the topic and/or an example? :slight_smile:

All I can give at the moment is the basic structure.

This is on page that is dynamic in the information that it loads, thus this endpoint would cause conflicts normally.

    maps = app.callback_map

    inputData = []
    for info in maps:
        inputData.append(maps[info]['inputs'][0]['id'])
    if adjButtons:
        if not buttonData[0] in inputData:
            app.clientside_callback(
                scripts, Output(buttonData[0], 'value'),
                Input(buttonData[0], 'n_clicks'), prevent_initial_call=True
            )
    for x in defaultButtons:
        if x['id'] not in inputData:
            try:
                app.clientside_callback(
                    functions[x['function']], Output(x['id'], 'value'),
                    Input(x['id'], 'n_clicks'), prevent_initial_call=True
                )
            except:
                pass

I use the inputs to keep the dynamic inputs separate from each other.

adjButtons is a list of buttons that I build and split out the scripts (function), defaultButtons is a list of default buttons obviously. :smiley:

Could you post a complete (preferable small) app that performs the demonstrates the approach? I just made a quick demo app, but neither normal nor clientside callbacks seem to work,

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

app = Dash(prevent_initial_callbacks=True)
app.layout = html.Div([
        html.Button("Add", id="add"),
        html.Div([], id="container"),
])


@app.callback(Output("container", "children"), Input("add", "n_clicks"), State("container", "children"))
def add(n_clicks, children):
    btn_id = f"btn{n_clicks}"
    log_id = f"log{n_clicks}"
    children.append(html.Button(f"Button number {n_clicks}", id=btn_id))
    children.append(html.Div(id=log_id))

    # # Serverside callback, doesn't work.
    # @app.callback(Output(log_id, "children"), Input(btn_id, "n_clicks"))
    # def log(m_clicks):
    #     return m_clicks

    # Clientside callback, doesn't work either.
    app.clientside_callback("function(x){return x}", Output(log_id, "children"), Input(btn_id, "n_clicks"))

    return children


if __name__ == '__main__':
    app.run_server()

@Emil,

It’s not perfect, but does convey the thinking behind being dynamic.

from dash import Dash, html, Output, Input, State, dcc
from flask import Flask

server = Flask(__name__)


app = Dash(server=server,
external_scripts=[
    {'src':"http://127.0.0.1:8050/assets/myButtons.js"}],
    suppress_callback_exceptions=True,
           prevent_initial_callbacks=True)
app.layout = html.Div([
        dcc.Store(id='newButton', storage_type='local'),
        html.Button("Add", id="add"),
        html.Div([], id="container"),
])

@server.route("/assets/myButtons.js", methods=['GET'])
def myButtons():
    maps = app.callback_map

    btns = ['btn'+str(i) for i in range(20)]

    inputData = []
    for info in maps:
        inputData.append(maps[info]['inputs'][0]['id'])
    for btn_id in btns:
        if not btn_id in inputData:
            app.clientside_callback(
                """
                    function(x,y) {
                        alert(y + ' clicked ' + x + ' times')
                        return y
                    }
                """,
                Output(btn_id, 'children'),
                Input(btn_id, 'n_clicks'),
                State(btn_id,'children')
            )
    return "Your buttons have been added"


@app.callback(Output("container", "children"),
              Input("add", "n_clicks"),
              State("container", "children")
              )
def add(n_clicks, children):
    btn_id = f"btn{n_clicks}"
    log_id = f"log{n_clicks}"
    children.append(html.Button(f"Button number {n_clicks}", id=btn_id))
    children.append(html.Div(id=log_id))

    return children


if __name__ == '__main__':
    app.run(debug=True)

Potentially, on the first load it would all the buttons and the events associated with it. Even on the first load, it knows that something is there and is being triggered but it doesnt quite work. On the second load, everything works as expected.

image

The point of dynamic buttons is being able to pass in arguments and add buttons and functionality without having to go into the app to make the changes. Not add the buttons this way of this example.

Also, please note, your example worked first by adding the callback, once you refreshed, it would have worked like my second picture up to what you had added.

1 Like

Hi guys, thanks for the replies. To give you an update, here’s what I’ve done for now:

# run_dashserver.py

dash_prefixed = Blueprint('dash', __name__)

def create_flaskapp():
	flask_app = Flask('dash app')
	flask_app.config["SECRET_KEY"] = SECRET_KEY
	dashboard_apps = models.Dashboard.get(force_list=True)  # Fetch dashboard objects from my DB
	for dashboard_app in dashboard_apps:
		dash_layout, register_callbacks_fn = execute_function(dashboard_app.function)  # Runs the dynamic function that returns both the layout and the register_callbacks function. 
        # register_callbacks is a function that defines the actual callback inside it (see next file) so that the callback decorator is seen at the time of initializing the Flask app.
		register_dashapp(flask_app, dashboard_app.id, dashboard_app.id, dash_layout, register_callbacks_fn)  # Makes sure the Flask app 'sees' the route for each dashboard object

	flask_app.register_blueprint(dash_prefixed, url_prefix='/dash')
	return flask_app


def register_dashapp(flask_app, title, base_pathname, layout, register_callbacks_fn):
	my_dashapp = dash.Dash(
		__name__,
		server=flask_app,
		url_base_pathname=f'/dash/{base_pathname}/',
	)

	with flask_app.app_context():
		my_dashapp.title = title
		my_dashapp.layout = layout
		register_callbacks_fn(my_dashapp)
	protect_dashviews(my_dashapp)  # Some authentication

Now for the dynamic function which is being called for each dashboard object above:

# Dynamic function for a single dashboard object

def my_dashboard():
	html = dash.html
	# etc
	# etc
	# Build the dashboard layout
	dashapp_layout = html.Div(
		children=[
			# etc
		],
		style={
			# etc
		}
	)

	def register_callbacks(dashapp):
		
		@dashapp.callback(
			output={
				# Output mapping
			},
			inputs=[
				# Inputs
			],
			prevent_initial_call=True	# So that it doesn't run on page load
		)
		def update_values(n_clicks):
			# prepare the output_values_dict
			return output_values_dict
		
	return dashapp_layout, register_callbacks

Looks pretty standard, but the secret sauce to making this whole shebang work is that run_dashserver.py runs inside a separate docker container of its own, separate from my main Flask app. If any new dashboards are added to the DB (or there are any modifications in existing ones), the container needs to be restarted so that the changes can be loaded.

This approach is far from ideal:

  • Although my main application need not be interrupted, it still means that there is a loss in service for users who may be browsing other dashboards while the container restart is taking place.
  • The more the number of dashboards, the more time it takes to restart this container and load up everything.
  • I haven’t figured out a way to elegantly define external CSS for each dashboard separately, because AFAIK the css is supposed to be configured at the time of creating the main Flask app and not the individual dashboard views.

This is basically a hack to get my requirements to work just somewhat. I’d love for there to be a better way to do this. Looking forward to chatting with anyone who can build further on this. :slight_smile:

Hello @fmdasher,

What you might be able to do is instead of restarting the same server, get a secondary server running with the new dashboards.

Then upon its complete initialization, spin down the other one.

This would require some use of a load balancer to send requests to the different backend.

Also, how many different dashboards are going to be added at a given time?

By dashboards, do you mean 1 page with different charts or completely different routes too?

If the former, you should check out my dashboard-helper.

This offers the building blocks of how to load dynamically served content to the same page.

Thanks, but it’s going to be completely different routes and complex dashboards each with their own authorization rules (I’ll probably implement something that utilizes flask-login).

You can use it with flask-login to determine different levels or customizations. As I use it for this too.

What are you thoughts about the other stuff I mentioned with the two servers and a load balancer.