Skip to content

Commit bbc9ffe

Browse files
guidooswaldDBclaude
andcommitted
Add Abort button during Load All pagination
Shows a red Abort button in the status bar while pages are loading. Clicking it stops the background thread and renders partial results collected so far. The abort/completion status message auto-dismisses after 5 seconds. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 856df04 commit bbc9ffe

3 files changed

Lines changed: 69 additions & 11 deletions

File tree

app.py

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,14 @@ def _custom_section():
975975
title="Fetch all remaining pages",
976976
style={"display": "none"},
977977
),
978+
html.Button(
979+
[html.I(className="bi bi-x-lg me-1"), "Abort"],
980+
id="load-all-abort-btn",
981+
n_clicks=0,
982+
className="load-all-abort-btn",
983+
title="Cancel loading",
984+
style={"display": "none"},
985+
),
978986
], className="response-panel"),
979987
], className="main-content"),
980988
], className="app-body"),
@@ -1821,13 +1829,14 @@ def _load_all_worker(last_req, host, token, list_key, initial_data):
18211829
Output("load-all-ticker", "disabled"),
18221830
Output("fetch-status-bar", "children", allow_duplicate=True),
18231831
Output("sp-load-all-btn", "style", allow_duplicate=True),
1832+
Output("load-all-abort-btn", "style"),
18241833
Input("sp-load-all-btn", "n_clicks"),
18251834
State("last-request", "data"),
18261835
State("conn-config", "data"),
18271836
prevent_initial_call=True,
18281837
)
18291838
def start_load_all(n_clicks, last_req, conn_config):
1830-
NO = (True, no_update, no_update)
1839+
NO = (True, no_update, no_update, no_update)
18311840
if not n_clicks or not last_req:
18321841
return NO
18331842
initial_data = last_req.get("initial_data", {})
@@ -1858,7 +1867,7 @@ def start_load_all(n_clicks, last_req, conn_config):
18581867
"Loading page 1…",
18591868
], className="fetch-status-inner loading")
18601869

1861-
return False, status, {"display": "none"}
1870+
return False, status, {"display": "none"}, {"display": "inline-flex"}
18621871

18631872

18641873
# 11h-tick: Poll progress from background thread
@@ -1868,31 +1877,33 @@ def start_load_all(n_clicks, last_req, conn_config):
18681877
Output("chips-store", "data", allow_duplicate=True),
18691878
Output("fetch-status-bar", "children", allow_duplicate=True),
18701879
Output("load-all-ticker", "disabled", allow_duplicate=True),
1880+
Output("load-all-abort-btn", "style", allow_duplicate=True),
18711881
Input("load-all-ticker", "n_intervals"),
18721882
State("response-cache", "data"),
18731883
prevent_initial_call=True,
18741884
)
18751885
def poll_load_all(n_intervals, cache):
1876-
NO = (no_update, no_update, no_update, no_update, no_update)
1886+
NO = (no_update, no_update, no_update, no_update, no_update, no_update)
18771887
state = _load_all_state
18781888
pages = state.get("pages", 0)
18791889
total = state.get("total_items", 0)
18801890
elapsed = state.get("elapsed_ms", 0)
1891+
HIDE = {"display": "none"}
18811892

18821893
if state.get("running"):
18831894
# Still loading — update status bar only
18841895
status = html.Div([
18851896
html.I(className="bi bi-arrow-repeat me-2 spin-icon"),
18861897
f"Loading page {pages + 1}… ({total:,} items so far · {elapsed:,}ms)",
18871898
], className="fetch-status-inner loading")
1888-
return no_update, no_update, no_update, status, False
1899+
return no_update, no_update, no_update, status, False, no_update
18891900

18901901
# Auto-dismiss: if finished_at was set, wait 5s then clear status bar
18911902
finished_at = state.get("finished_at")
18921903
if finished_at and not state.get("done"):
18931904
if time.time() - finished_at >= 5:
18941905
state.pop("finished_at", None)
1895-
return no_update, no_update, no_update, "", True # clear status, stop ticker
1906+
return no_update, no_update, no_update, "", True, HIDE # clear status, stop ticker
18961907
return NO # keep ticking, waiting to dismiss
18971908

18981909
if not state.get("done"):
@@ -1902,8 +1913,8 @@ def poll_load_all(n_intervals, cache):
19021913
if state.get("error"):
19031914
status = html.Div([
19041915
html.I(className="bi bi-exclamation-triangle-fill me-2"),
1905-
f"Error after {pages} pages ({total:,} items): {state['error']}",
1906-
], className="fetch-status-inner error")
1916+
f"Aborted after {pages} pages ({total:,} items · {elapsed:,}ms)",
1917+
], className="fetch-status-inner cancelled")
19071918
else:
19081919
status = html.Div([
19091920
html.I(className="bi bi-check-circle-fill me-2"),
@@ -1934,7 +1945,25 @@ def poll_load_all(n_intervals, cache):
19341945
# Mark done, set dismiss timer — keep ticker running for auto-dismiss
19351946
_load_all_state.update({"done": False, "items": [], "finished_at": time.time()})
19361947

1937-
return build_response_panel(merged_result, chips), new_cache, chips or None, status, False
1948+
return build_response_panel(merged_result, chips), new_cache, chips or None, status, False, HIDE
1949+
1950+
1951+
# 11h-abort: Stop background Load All thread
1952+
@app.callback(
1953+
Output("fetch-status-bar", "children", allow_duplicate=True),
1954+
Output("load-all-abort-btn", "style", allow_duplicate=True),
1955+
Input("load-all-abort-btn", "n_clicks"),
1956+
prevent_initial_call=True,
1957+
)
1958+
def abort_load_all(n_clicks):
1959+
if not n_clicks:
1960+
return no_update, no_update
1961+
_load_all_state["running"] = False
1962+
_load_all_state["error"] = "Cancelled"
1963+
return html.Div([
1964+
html.I(className="bi bi-x-circle-fill me-2"),
1965+
"Aborting…",
1966+
], className="fetch-status-inner cancelled"), {"display": "none"}
19381967

19391968

19401969
# 13. Search filter
@@ -2190,7 +2219,8 @@ def update_curl_display(last_req, conn_config):
21902219
)
21912220

21922221

2193-
# Reparent the Load All button into the response-meta bar after each render
2222+
# Reparent the Load All button into the response-meta bar,
2223+
# and the Abort button into the fetch-status-bar, after each render
21942224
app.clientside_callback(
21952225
"""
21962226
function(children, style) {
@@ -2199,6 +2229,11 @@ def update_curl_display(last_req, conn_config):
21992229
if (anchor && btn) {
22002230
anchor.appendChild(btn);
22012231
}
2232+
var statusBar = document.getElementById('fetch-status-bar');
2233+
var abortBtn = document.getElementById('load-all-abort-btn');
2234+
if (statusBar && abortBtn) {
2235+
statusBar.appendChild(abortBtn);
2236+
}
22022237
return window.dash_clientside.no_update;
22032238
}
22042239
""",

assets/style.css

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,7 @@ body {
716716

717717

718718
/* ── Pagination status bar ────────────────────────────────────────── */
719-
.fetch-status-bar { flex-shrink: 0; overflow: hidden; }
719+
.fetch-status-bar { flex-shrink: 0; overflow: hidden; position: relative; }
720720
.fetch-status-bar:empty { display: none; }
721721

722722
.fetch-status-inner {
@@ -752,6 +752,29 @@ body {
752752
border-color: rgba(239,68,68,0.6);
753753
box-shadow: 0 0 8px rgba(239,68,68,0.2);
754754
}
755+
.load-all-abort-btn {
756+
position: absolute;
757+
right: 18px;
758+
top: 50%;
759+
transform: translateY(-50%);
760+
z-index: 5;
761+
background: rgba(239,68,68,0.1);
762+
border: 1px solid rgba(239,68,68,0.3);
763+
color: var(--red);
764+
border-radius: var(--r-sm);
765+
padding: 2px 10px;
766+
font-size: 11px;
767+
font-family: var(--font-mono);
768+
cursor: pointer;
769+
display: inline-flex;
770+
align-items: center;
771+
white-space: nowrap;
772+
transition: all 0.15s;
773+
}
774+
.load-all-abort-btn:hover {
775+
background: rgba(239,68,68,0.22);
776+
border-color: rgba(239,68,68,0.6);
777+
}
755778

756779
.status-spinner {
757780
display: inline-block;

version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
372
1+
378

0 commit comments

Comments
 (0)