Dash alerting user for inactivity

Is it possible to alert the user if no activity is performed in the last X mins? By inactivity I mean no user inputs (button clicks, etc) and no browsing through different pages of the Dash application.
If inactivity is detected just show a pop-up message that “You have been inactive, and your session will soon log out.”

I don’t think this is something readily available in Dash but if there are any workarounds that would be of great help, thank you!

Hello @dataturnsmeon,

You’d have to do this clientside as this is all browser based.

You’ll need an event listener that listens to any document user input basically. Reset the timer on those.

Thanks for the response @jinnyzor :slight_smile:

Tbh I don’t have much experience in JS, could you point me to some references or if a minimal solution that would be really helpful.

I found these results

Do you think any of these could work with Dash clientside callbacks? And what would be the input to the callback? Could I use the id of a component as input?

Hi @dataturnsmeon

You could use the EventListener component from dash-extensions

Here is an example:

import dash
from dash import Dash, dcc, html, Input, Output, State, ctx
from dash_extensions import EventListener
import time

# JavaScript event(s) that we want to listen to and what properties to collect.
click_event = {"event": "click"}
keydown_event= {"event": "keydown"}

app = Dash(__name__)

app.layout = html.Div([
    EventListener(
        [
            dcc.Input(),
            html.Button("Click here!"),
        ],
        events=[click_event, keydown_event],        
        id="el",
    ),
    dcc.Store(id="last-activity"),
    dcc.Interval(id="inactivity-check", interval=5000),
    html.Div(id="log")

])

@app.callback(
    Output("last-activity", "data"),
    Output("log", "children"),
    Input("el", "n_events"),
    Input("inactivity-check", "n_intervals"),
    State("last-activity", "data")
)
def click_event(event, inactivity_timer, last_activity):
    if ctx.triggered_id== "inactivity-check":
        if last_activity:
            return None, dash.no_update
        else:
            return None, f"no activity in 5 seconds as of {time.time()}"
    return time.time(), ""


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

2 Likes

Yes, you could use these and add them as event listeners one time via a clientside callback.

The cool thing about doing it in JS, you can tell the browser to navigate, and you dont have to worry about that extra step. If you want to display the alert via a callback, then you will have to figure out a way to trigger a callback without also resetting the timer.

Here is a js script you can put into your assets folder:

let timer, currSeconds = 0;
function resetTimer() {
    /* Hide the timer text */
    document.querySelector(".logout_queue")
            .style.display = 'none';

    /* Clear the previous interval */
    clearInterval(timer);

    /* Reset the seconds of the timer */
    currSeconds = 0;

    /* Set a new interval */
    timer =
        setInterval(startIdleTimer, 1000);
}

// Define the events that
// would reset the timer
window.onload = resetTimer;
window.onmousedown = resetTimer;
window.ontouchstart = resetTimer;
window.onclick = resetTimer;
window.onkeypress = resetTimer;

function startIdleTimer() {

    /* Increment the
        timer seconds */
    currSeconds++;

    if (currSeconds > 30) {
        window.location.href = '../logout'
    }

    /* Set the timer text
        to the new value */
    document.querySelector(".timeout")
        .textContent = 31 - currSeconds;

    /* Display the timer text */
    document.querySelector(".logout_queue")
        .style.display = 'flex';
}

And the app layout and callback:

app.layout = html.Div(
    [
        html.A('logout', href='../logout'),
        html.Br(),
        dash.page_container,
        html.Div(
                [html.Div(['You will be logged out in: ', html.Span(className='timeout')],
                            style={'backgroundColor':'white'}, id='testing')],
                className='logout_queue',
                id='logout_queue',
                style={'display':'none', 'height':'100vh', 'width':'100vw',
                'backgroundColor':'rgba(0,0,0,0.4)', 'position':'absolute',
                'justifyContent':'center', 'alignItems':'center',
                'top':'0px', 'left':'0px'},
                )
    ]
)

app.clientside_callback(
    """
        function () {
            resetTimer();
            window.dash_clientside.no_update
        }
    """,
Output('logout_queue','id'), Input('logout_queue','id'))

This will display like this:

And then redirect to the logout page after 30 seconds.

In the above example, the login / logout flow is outside of the dash app, so you dont have to account for it looping and redirecting to the logout from the login page. XD

You can ignore the locations from the window.location.href when you dont want to do the logout flow.


You can bring all of this js code inside of the clientside callback if you want, instead of having it in your assets folder.

2 Likes

Thanks a ton @jinnyzor and @AnnMarieW :100:

This is really helpful, I’ll try out both of these approaches.

@AnnMarieW

I was trying out your solution and I liked the subtle implementation but for some reason after wrapping my app layout in the Eventlistener I am seeing a blinking cursor on the page.

I added a text to your example and I am seeing the same behavior, the cursor even appears on the button name.
image

Is this some kind of bug related to the dash_extensions package?

@dataturnsmeon When I run it, I don’t see a blinking cursor. What version of Dash Extensions are you using?

@AnnMarieW I think it’s a browser related bug. I was using Brave browser where this behaviour was occuring, on all other browsers it’s working fine.

Although now there is another problem I am stuck with, the dcc.Interval is acting a bit strange for longer duration of interval. When I tested the with interval set to 10 seconds the callback was working perfectly in sync with the timing.
With longer durations 30 secs or 1 minute it is taking longer than the set interval.

For 30 secs i.e interval=30000 the log message was displayed approximately after 55 seconds (a delay of 15 seconds). For longer duration the gap is much longer for interval of 1 minute it took 1 min 50 secs for log message.

Ideally I want to set the interval to be either 5 or 10 minutes but couldn’t figure the delay in timing.

Try breaking the callback into two like this:


@app.callback(
    Output("last-activity", "data"),
    Input("el", "n_events"),
)
def click_event(_):
    return time.time()


@app.callback(
    Output("log", "children"),
    Input("inactivity-check", "n_intervals"),
    State("last-activity", "data")
)
def click_event(n, last_activity):
    if last_activity is None:
        return ""
    now=time.time()
    if now-int(last_activity) > 30:
        return  f"no activity in 30 seconds as of {now}"
    return ""


This should be accurate within about 1 second. If not then @jinnyzor clientside solution would be better.

1 Like

Thanks @AnnMarieW :crown:

This works !

1 Like

oooh, I like the :crown:
Thanks! :rofl:

1 Like