Skip to content
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

Add ability to add global loading spinner to application(s) #4659

Merged
merged 2 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions panel/command/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,10 @@ class Serve(_BkServe):
action = 'store_true',
help = "Whether to reuse sessions when serving the initial request.",
)),
('--global-loading-spinner', dict(
action = 'store_true',
help = "Whether to add a global loading spinner to the application(s).",
)),
)

# Supported file extensions
Expand Down Expand Up @@ -281,6 +285,7 @@ def customize_kwargs(self, args, server_kwargs):
raise ValueError("rest-provider %r not recognized." % args.rest_provider)

config.autoreload = args.autoreload
config.global_loading_spinner = args.global_loading_spinner
config.reuse_sessions = args.reuse_sessions

if config.autoreload:
Expand Down
3 changes: 3 additions & 0 deletions panel/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ class _config(_base_config):
exception_handler = param.Callable(default=None, doc="""
General exception handler for events.""")

global_loading_spinner = param.Boolean(default=False, doc="""
Whether to add a global loading spinner for the whole application.""")

load_entry_points = param.Boolean(default=True, doc="""
Load entry points from external packages.""")

Expand Down
10 changes: 10 additions & 0 deletions panel/io/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
from bokeh.application.application import SessionContext
from bokeh.document.document import Document
from bokeh.document.events import DocumentChangedEvent, ModelChangedEvent
from bokeh.models import CustomJS

from ..config import config
from .loading import LOADING_INDICATOR_CSS_CLASS
from .model import monkeypatch_events
from .state import curdoc_locked, state

Expand Down Expand Up @@ -115,6 +118,13 @@ def init_doc(doc: Optional[Document]) -> Document:
if thread:
state._thread_id_[curdoc] = thread.ident

if config.global_loading_spinner:
doc.js_on_event(
'document_ready', CustomJS(code=f"""
const body = document.getElementsByTagName('body')[0]
body.classList.remove({LOADING_INDICATOR_CSS_CLASS!r}, {config.loading_spinner!r})
""")
)
session_id = curdoc.session_context.id
sessions = state.session_info['sessions']
if session_id not in sessions:
Expand Down
3 changes: 2 additions & 1 deletion panel/io/pyodide.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from ..util import edit_readonly, is_holoviews, isurl
from . import resources
from .document import MockSessionContext
from .loading import LOADING_INDICATOR_CSS_CLASS
from .mime_render import WriteCallbackStream, exec_with_return, format_mime
from .state import state

Expand Down Expand Up @@ -413,7 +414,7 @@ def hide_loader() -> None:
from js import document

body = document.getElementsByTagName('body')[0]
body.classList.remove("pn-loading", config.loading_spinner)
body.classList.remove(LOADING_INDICATOR_CSS_CLASS, config.loading_spinner)

def sync_location():
"""
Expand Down
3 changes: 3 additions & 0 deletions panel/io/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,9 @@ def css_raw(self):
css_txt = f.read()
if css_txt not in raw:
raw.append(css_txt)
if config.global_loading_spinner:
loading_base = (DIST_DIR / "css" / "loading.css").read_text()
raw.extend([loading_base, loading_css()])
return raw + process_raw_css(config.raw_css)

@property
Expand Down
34 changes: 27 additions & 7 deletions panel/io/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
)
from bokeh.embed.util import RenderItem
from bokeh.io import curdoc
from bokeh.models import CustomJS
from bokeh.server.server import Server as BokehServer
from bokeh.server.urls import per_app_patterns
from bokeh.server.views.autoload_js_handler import (
Expand All @@ -69,6 +70,7 @@
from ..util import edit_readonly, fullpath
from ..util.warnings import warn
from .document import init_doc, unlocked, with_lock # noqa
from .loading import LOADING_INDICATOR_CSS_CLASS
from .logging import (
LOG_SESSION_CREATED, LOG_SESSION_DESTROYED, LOG_SESSION_LAUNCHING,
)
Expand Down Expand Up @@ -120,6 +122,14 @@ def _eval_panel(
from ..pane import panel as as_panel
from ..template import BaseTemplate

if config.global_loading_spinner:
doc.js_on_event(
'document_ready', CustomJS(code=f"""
const body = document.getElementsByTagName('body')[0]
body.classList.remove({LOADING_INDICATOR_CSS_CLASS!r}, {config.loading_spinner!r})
""")
)

# Set up instrumentation for logging sessions
logger.info(LOG_SESSION_LAUNCHING, id(doc))
def _log_session_destroyed(session_context):
Expand Down Expand Up @@ -214,23 +224,33 @@ def server_html_page_for_session(
if template is FILE:
template = BASE_TEMPLATE

session.document._template_variables['theme_name'] = config.theme
session.document._template_variables['dist_url'] = dist_url
for root in session.document.roots:
doc = session.document
doc._template_variables['theme_name'] = config.theme
doc._template_variables['dist_url'] = dist_url
for root in doc.roots:
patch_model_css(root, dist_url=dist_url)

render_item = RenderItem(
token = token or session.token,
roots = session.document.roots,
roots = doc.roots,
use_for_title = False,
)

if template_variables is None:
template_variables = {}

bundle = bundle_resources(session.document.roots, resources)
return html_page_for_render_items(bundle, {}, [render_item], title,
template=template, template_variables=template_variables)
with set_curdoc(doc):
bundle = bundle_resources(doc.roots, resources)
html = html_page_for_render_items(
bundle, {}, [render_item], title, template=template,
template_variables=template_variables
)
if config.global_loading_spinner:
html = html.replace(
'<body>', f'<body class="{LOADING_INDICATOR_CSS_CLASS} {config.loading_spinner}">'
)
return html


def autoload_js_script(doc, resources, token, element_id, app_path, absolute_url, absolute=False):
resources = Resources.from_bokeh(resources, absolute=absolute)
Expand Down
27 changes: 27 additions & 0 deletions panel/tests/ui/io/test_loading.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import time

import pytest

try:
from playwright.sync_api import expect
except ImportError:
pytestmark = pytest.mark.skip('playwright not available')

from panel.config import config
from panel.io.server import serve
from panel.pane import Markdown

pytestmark = pytest.mark.ui

def test_global_loading_indicator(page, port):
def app():
config.global_loading_spinner = True
return Markdown('Blah')

serve(app, port=port, threaded=True, show=False)

time.sleep(0.5)

page.goto(f"http://localhost:{port}")

expect(page.locator("body")).not_to_have_class('pn-loading')