Asynchronous notifications / output console for long callbacks - what do you think about my solution?

Hi,

In a dashboard I’m building I have one heavy callback that runs a series of operations and I’d like to give the user some indication of the stage that is currently running.

I know we don’t have asynchronous notifications yet, so I came up with a temporary solution that works but feels a bit patchy. I’d love to get some feedback, especially if you see problems with such a solution or have a better one.

The idea is essentially to create a callback that calls itself as long as the process lasts, running different functions and producing matching outputs. I’m using different levels in the html to bypass the restriction on dependency cycles.

Here’s the code:

import time
import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output
from dash.exceptions import PreventUpdate

app = dash.Dash(__name__)

server = app.server

app.layout = html.Div(children=[
	html.Button('Run', id='run_button'),
	html.Div(
		id='output_container',
		children=[
			html.Div(children='', id='output_text'),
			html.Div(id='process_stage', children=-1, style={'display': 'none'})
		]
	)]
)

def dummy():
	return
	
def step1():
	time.sleep(1)

def step2():
	time.sleep(1.5)

def step3():
	time.sleep(2)
	
process_plan = [
	[dummy, ''],
	[step1, 'Running step 1...'],
	[step2, 'Running step 2...'],
	[step3, 'Running step 3...'],
	[dummy, 'Process finished.']
]
	
@app.callback([Output('process_stage', 'children'), 
			   Output('run_button', 'disabled')], 
			  [Input('run_button', 'n_clicks')])
# start the process by setting the stage to 0
def button_clicked(click):
	if not click:
		return -1, False
	else:
		return 0, True

# this callback triggers itself by returning the div container as output and getting triggered by one of its children as input
@app.callback(Output('output_container', 'children'),
			 [Input('process_stage', 'children')])
# the process runs as long as the stage (stored in a hidden div) is between 0 and the number of stages in the plan
def run_process(stage):
	if stage == -1 or stage == len(process_plan)-1:
		raise PreventUpdate
	
	process_plan[stage][0]()
	
    # the callback returns the next stage's output and runs the current stage's function, so output matches what's currently running
	return [html.Div(children=process_plan[stage+1][1], id='output_text'),
			html.Div(id='process_stage', children=stage+1, style={'display': 'none'})]

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