Skip to content

Add busy indicators (aka spinners) #918

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 70 commits into from
May 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
b5dfe7e
Add function for including loading spinners in apps.
nstrayer Dec 15, 2023
fb0e218
Fix documentation formatting
nstrayer Dec 19, 2023
80c38b4
Add option for delay on the spinner animation. Also simplify animatio…
nstrayer Dec 20, 2023
fc69915
Refactor server function to control sleep behavior
nstrayer Dec 20, 2023
77deff3
Add TODO for toggling spinner growing
nstrayer Dec 20, 2023
2e076b9
Disable grow animation by default.
nstrayer Dec 20, 2023
11a4726
Add two new loading spinners and add always-on spinner to demo app
nstrayer Dec 20, 2023
dd54ced
Use literals for typing of type and format docstrings
nstrayer Dec 20, 2023
54a7f0d
Make disc spinner more balanced
nstrayer Dec 20, 2023
6733696
Remove set width and heights from svg as they are never actually used
nstrayer Dec 20, 2023
1fbb479
Add option for page-level spinner and modularize css a bit
nstrayer Dec 22, 2023
95a8df2
make the page-level spinner have a smaller default size
nstrayer Dec 22, 2023
14cbdc0
Add spinners as on by default and add response to classes to turn off…
nstrayer Dec 22, 2023
4a8a91e
Add ability to target a specific output with a spinner.
nstrayer Dec 22, 2023
b0f8576
Remove the no-longer-used spinners-common.css file.
nstrayer Dec 22, 2023
30ce38c
Merge branch 'main' into spinners
cpsievert Apr 2, 2024
bef3ae8
Fix small doc issues
cpsievert Apr 3, 2024
7977393
Bundle HTMLDependency() with shiny's core dependencies
cpsievert Apr 3, 2024
10cbc90
Update example to demonstrate some current problems better
cpsievert Apr 3, 2024
8bd3513
Return tag instead of HTMLDependency; simplify API
cpsievert Apr 3, 2024
b4f5cef
Fix/improve element level spinner behavior. Provide a disable()/enabl…
cpsievert Apr 3, 2024
f98e30e
Merge branch 'main' into spinners
cpsievert Apr 3, 2024
ff93b4a
Fix plot tasks effects other plots
cpsievert Apr 4, 2024
2826e34
Get rid of blur effect in favor of opacity dim
cpsievert Apr 4, 2024
f21f5ae
Introduce concept of indicator modes; leverage build-time Sass and tr…
cpsievert Apr 4, 2024
b5a268e
Execute the extended tasks concurrently
cpsievert Apr 5, 2024
7862ce1
Add page-level spinner when shiny is busy, and nothing in the UI is r…
cpsievert Apr 5, 2024
ce4760a
Add spinner mode
cpsievert Apr 5, 2024
5f38b79
Merge branch 'main' into spinners
cpsievert Apr 5, 2024
924ed38
rm yarn.lock
cpsievert Apr 8, 2024
93ef45b
Rename loading-indicators -> busy-indicators
cpsievert Apr 8, 2024
dfa0ab7
Activate page-level spinner up until the 1st idle.
cpsievert Apr 8, 2024
1cbe237
Add submodule to express
cpsievert Apr 8, 2024
618278c
Remove example commited by mistake
cpsievert Apr 8, 2024
391be77
Embed svgs in the css; simplify further
cpsievert Apr 8, 2024
dadf49a
Revert changes to package json since plugin is no longer needed
cpsievert Apr 8, 2024
eae88cb
Add to API reference, with an example
cpsievert Apr 8, 2024
ae9994a
Merge branch 'main' into spinners
cpsievert Apr 8, 2024
df4e90b
Increase delay to 0.5s
cpsievert Apr 9, 2024
afdb8d7
Introduce the concept of a pulsing page (instead of a page-wide spinner)
cpsievert Apr 10, 2024
cf3c5f6
Move example to more sensible location
cpsievert Apr 10, 2024
9279dc3
Remove disable spinner classes and just document size=0px as a way to…
cpsievert Apr 10, 2024
3fb4975
Merge branch 'main' into spinners
cpsievert Apr 10, 2024
684b25e
Remove example commited by mistake
cpsievert Apr 11, 2024
a40e0cd
Code review
cpsievert Apr 11, 2024
bbe4e1d
Improve pulse effect; add pulse_options()
cpsievert Apr 12, 2024
0f64d90
Small tweaks to pulse from Greg
cpsievert Apr 23, 2024
707f521
Bring in multi-color gradient
cpsievert Apr 23, 2024
f21d5c3
Merge branch 'main' into spinners
cpsievert Apr 23, 2024
ffad902
Merge branch 'main' into spinners
cpsievert Apr 23, 2024
520045b
Export pulse_options()
cpsievert Apr 23, 2024
e883193
Avoid inline script
cpsievert Apr 23, 2024
edb2245
Show output busy state on 1st draw; disable pulse when spinners are v…
cpsievert Apr 26, 2024
f646b9e
Fix lint
cpsievert Apr 26, 2024
2497552
Merge branch 'main' into spinners
cpsievert Apr 26, 2024
816a1ac
Update express example
cpsievert Apr 26, 2024
4a46170
Decrease default size; do more to avoid clipping; fix example
cpsievert Apr 29, 2024
698cae2
Clean up event handling logic
cpsievert Apr 29, 2024
4efa58b
Add more commentary
cpsievert Apr 30, 2024
7d833f1
Merge branch 'main' into spinners
cpsievert May 8, 2024
b822ee1
Bring in JS/CSS source from RShiny repo
cpsievert May 8, 2024
1285798
Add back spinner selector option
cpsievert May 8, 2024
77755ea
Merge branch 'main' into spinners
cpsievert May 10, 2024
335f059
Port over latest from RShiny
cpsievert May 10, 2024
2949437
Update
cpsievert May 10, 2024
b242cb0
Update
cpsievert May 10, 2024
1125f43
Fix base64 encoding
cpsievert May 10, 2024
23d526e
Fix/improve example
cpsievert May 10, 2024
63191a6
Update changelog
cpsievert May 10, 2024
86ca109
Better naming
cpsievert May 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### New features

* Added busy indicators to provide users with a visual cue when the server is busy calculating outputs or otherwise serving requests to the client. More specifically, a spinner is shown on each calculating/recalculating output, and a pulsing banner is shown at the top of the page when the app is otherwise busy. Use the new `ui.busy_indicator.options()` function to customize the appearance of the busy indicators and `ui.busy_indicator.use()` to disable/enable them. (#918)

* Added support for creating modules using Shiny Express syntax, and using modules in Shiny Express apps. (#1220)

* `ui.page_*()` functions gain a `theme` argument that allows you to replace the Bootstrap CSS file with a new CSS file. `theme` can be a local CSS file, a URL, or a [shinyswatch](https://posit-dev.github.io/py-shinyswatch) theme. In Shiny Express apps, `theme` can be set via `express.ui.page_opts()`. (#1334)
Expand Down
2 changes: 2 additions & 0 deletions docs/_quartodoc-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ quartodoc:
- ui.include_js
- ui.insert_ui
- ui.remove_ui
- ui.busy_indicators.use
- ui.busy_indicators.options
- ui.fill.as_fillable_container
- ui.fill.as_fill_item
- ui.fill.remove_all_fill
Expand Down
2 changes: 2 additions & 0 deletions docs/_quartodoc-express.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ quartodoc:
- name: express.ui.tags # uses tags.rst template
children: embedded
- express.ui.TagList # uses class.rst template
- express.ui.busy_indicators.use
- express.ui.busy_indicators.options
# TODO: should these be included?
# - express.ui.fill.as_fillable_container
# - express.ui.fill.as_fill_item
Expand Down
54 changes: 54 additions & 0 deletions examples/busy_indicators/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# pyright:basic
import time

import numpy as np
import seaborn as sns

from shiny import App, module, reactive, render, ui


# -- Reusable card module --
@module.ui
def card_ui(spinner_type):
return ui.card(
ui.busy_indicators.options(spinner_type=spinner_type),
ui.card_header("Spinner: " + spinner_type),
ui.output_plot("plot"),
)


@module.server
def card_server(input, output, session, rerender):
@render.plot
def plot():
rerender()
time.sleep(1)
sns.lineplot(x=np.arange(100), y=np.random.randn(100))


# -- Main app --
app_ui = ui.page_fillable(
ui.input_task_button("rerender", "Re-render"),
ui.layout_columns(
card_ui("ring", "ring"),
card_ui("bars", "bars"),
card_ui("dots", "dots"),
card_ui("pulse", "pulse"),
col_widths=6,
),
)


def server(input, output, session):

@reactive.calc
def rerender():
return input.rerender()

card_server("ring", rerender=rerender)
card_server("bars", rerender=rerender)
card_server("dots", rerender=rerender)
card_server("pulse", rerender=rerender)


app = App(app_ui, server)
26 changes: 26 additions & 0 deletions scripts/htmlDependencies.R
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,29 @@ ignore <- system(
paste0("cd ", js_path, " && npm install && npm run build"),
intern = TRUE
)


# ------------------------------------------------------------------------------
message("Save spinner types")
spinner_types <- Sys.glob(file.path(www_shared, "busy-indicators", "spinners", "*.svg"))
spinner_types <- tools::file_path_sans_ext(basename(spinner_types))

template <- r"(# Generated by scripts/htmlDependencies.R: do not edit by hand

from __future__ import annotations

from typing import Literal

BusySpinnerType = Literal[
%s
])"

py_lines <- function(x) {
x <- paste(sprintf('"%s"', x), collapse = ",\n ")
paste0(" ", x, ",")
}

writeLines(
sprintf(template, py_lines(spinner_types)),
file.path(shiny_path, "ui", "_busy_spinner_types.py")
)
2 changes: 1 addition & 1 deletion shiny/_versions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
shiny_html_deps = "1.8.1.9000"
shiny_html_deps = "1.8.1.9001"
bslib = "0.7.0.9000"
htmltools = "0.5.8.9000"
bootstrap = "5.3.1"
Expand Down
56 changes: 56 additions & 0 deletions shiny/api-examples/busy_indicators/app-core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import os
import time

import numpy as np
import seaborn as sns

from shiny import App, render, ui

app_ui = ui.page_sidebar(
ui.sidebar(
ui.input_selectize(
"indicator_types",
"Busy indicator types",
["spinners", "pulse"],
multiple=True,
selected=["spinners", "pulse"],
),
ui.download_button("download", "Download source"),
),
ui.card(
ui.card_header(
"Plot that takes a few seconds to render",
ui.input_task_button("simulate", "Simulate"),
class_="d-flex justify-content-between align-items-center",
),
ui.output_plot("plot"),
),
ui.busy_indicators.options(spinner_type="bars3"),
ui.output_ui("indicator_types_ui"),
title="Busy indicators demo",
)


def server(input):

@render.plot
def plot():
input.simulate()
time.sleep(3)
sns.lineplot(x=np.arange(100), y=np.random.randn(100))

@render.ui
def indicator_types_ui():
return ui.busy_indicators.use(
spinners="spinners" in input.indicator_types(),
pulse="pulse" in input.indicator_types(),
)

@render.download
def download():
time.sleep(3)
path = os.path.join(os.path.dirname(__file__), "app-core.py")
return path


app = App(app_ui, server)
49 changes: 49 additions & 0 deletions shiny/api-examples/busy_indicators/app-express.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import os
import time

import numpy as np
import seaborn as sns

from shiny.express import input, render, ui

ui.page_opts(title="Busy spinner demo")

with ui.sidebar():
ui.input_selectize(
"indicator_types",
"Busy indicator types",
["spinners", "pulse"],
multiple=True,
selected=["spinners", "pulse"],
)

@render.download
def download():
time.sleep(3)
path = os.path.join(os.path.dirname(__file__), "app-express.py")
return path


with ui.card():
ui.card_header(
"Plot that takes a few seconds to render",
ui.input_task_button("simulate", "Simulate"),
class_="d-flex justify-content-between align-items-center",
)

@render.plot
def plot():
input.simulate()
time.sleep(3)
sns.lineplot(x=np.arange(100), y=np.random.randn(100))


ui.busy_indicators.options(spinner_type="bars3")


@render.ui
def indicator_types_ui():
return ui.busy_indicators.use(
spinners="spinners" in input.indicator_types(),
pulse="pulse" in input.indicator_types(),
)
5 changes: 5 additions & 0 deletions shiny/express/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
fill,
)

from ...ui import (
busy_indicators,
)

from ...ui import (
AccordionPanel,
AnimationOptions,
Expand Down Expand Up @@ -176,6 +180,7 @@
"strong",
"tags",
# Submodules
"busy_indicators",
"fill",
# Imports from ...ui
"AccordionPanel",
Expand Down
10 changes: 7 additions & 3 deletions shiny/html_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,26 @@

from htmltools import HTMLDependency

from . import __version__
from .ui._html_deps_py_shiny import busy_indicators_dep


def shiny_deps() -> list[HTMLDependency]:
deps = [
HTMLDependency(
name="shiny",
version="0.0.1",
version=__version__,
source={"package": "shiny", "subdir": "www/shared/"},
script={"src": "shiny.js"},
stylesheet={"href": "shiny.min.css"},
)
),
busy_indicators_dep(),
]
if os.getenv("SHINY_DEV_MODE") == "1":
deps.append(
HTMLDependency(
"shiny-devmode",
version="0.0.1",
version=__version__,
head="<script>window.__SHINY_DEV_MODE__ = true;</script>",
)
)
Expand Down
9 changes: 9 additions & 0 deletions shiny/session/_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,15 @@ async def uploadEnd(job_id: str, input_id: str) -> None:
# ==========================================================================
async def _handle_request(
self, request: Request, action: str, subpath: Optional[str]
) -> ASGIApp:
self._send_message_sync({"busy": "busy"})
try:
return await self._handle_request_impl(request, action, subpath)
finally:
self._send_message_sync({"busy": "idle"})

async def _handle_request_impl(
self, request: Request, action: str, subpath: Optional[str]
) -> ASGIApp:
if action == "upload" and request.method == "POST":
if subpath is None or subpath == "":
Expand Down
4 changes: 4 additions & 0 deletions shiny/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
# Expose the fill module for extended usage: ex: ui.fill.as_fill_item(x).
from . import fill

# Export busy_indicators module
from . import busy_indicators

from ._accordion import (
AccordionPanel,
accordion,
Expand Down Expand Up @@ -355,6 +358,7 @@
"em",
"hr",
# Submodules
"busy_indicators",
"fill",
# utils
"js_eval",
Expand Down
20 changes: 20 additions & 0 deletions shiny/ui/_busy_spinner_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by scripts/htmlDependencies.R: do not edit by hand

from __future__ import annotations

from typing import Literal

BusySpinnerType = Literal[
"bars",
"bars2",
"bars3",
"dots",
"dots2",
"dots3",
"pulse",
"pulse2",
"pulse3",
"ring",
"ring2",
"ring3",
]
13 changes: 13 additions & 0 deletions shiny/ui/_html_deps_py_shiny.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from htmltools import HTMLDependency

from .. import __version__
from . import busy_indicators

"""
HTML dependencies for internal dependencies such as dataframe or text area's autoresize.
Expand Down Expand Up @@ -52,3 +53,15 @@ def spin_dependency() -> HTMLDependency:
source={"package": "shiny", "subdir": "www/shared/py-shiny/spin"},
stylesheet={"href": "spin.css"},
)


def busy_indicators_dep() -> HTMLDependency:
return HTMLDependency(
"shiny-busy-indicators",
__version__,
source={"package": "shiny", "subdir": "www/shared/busy-indicators"},
stylesheet={"href": "busy-indicators.css"},
script={"src": "busy-indicators.js"},
head=busy_indicators.use(), # Enable busy indicators by default.
all_files=True,
)
Loading
Loading