dash-startup-loading-plugin 1.0.4__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dash_startup_loading_plugin-1.1.0/MANIFEST.in +2 -0
- dash_startup_loading_plugin-1.1.0/PKG-INFO +17 -0
- {dash_startup_loading_plugin-1.0.4 → dash_startup_loading_plugin-1.1.0}/pyproject.toml +8 -7
- {dash_startup_loading_plugin-1.0.4 → dash_startup_loading_plugin-1.1.0}/src/dash_startup_loading_plugin/__init__.py +3 -9
- dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin/plugin.py +416 -0
- dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin/py.typed +0 -0
- dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin/resources/LOADING-UI-LICENSE +21 -0
- dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin/resources/loading-ui.js +775 -0
- dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin/resources/loading.css +164 -0
- dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin/resources/loading.js +45 -0
- {dash_startup_loading_plugin-1.0.4 → dash_startup_loading_plugin-1.1.0}/src/dash_startup_loading_plugin/resources/theme.js +51 -19
- dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin.egg-info/PKG-INFO +17 -0
- dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin.egg-info/SOURCES.txt +17 -0
- dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin.egg-info/entry_points.txt +2 -0
- dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin.egg-info/requires.txt +2 -0
- dash_startup_loading_plugin-1.0.4/MANIFEST.in +0 -2
- dash_startup_loading_plugin-1.0.4/PKG-INFO +0 -303
- dash_startup_loading_plugin-1.0.4/README.md +0 -289
- dash_startup_loading_plugin-1.0.4/README.zh-CN.md +0 -274
- dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/README.md +0 -289
- dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/README.zh-CN.md +0 -274
- dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/examples/__init__.py +0 -5
- dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/examples/__main__.py +0 -5
- dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/examples/antd.py +0 -109
- dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/examples/basic.py +0 -49
- dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/examples/cli.py +0 -89
- dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/examples/fac.py +0 -110
- dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/examples/mantine.py +0 -79
- dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/examples/shared.py +0 -33
- dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/plugin.py +0 -296
- dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/resources/loading.css +0 -64
- dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin/resources/loading.js +0 -177
- dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin.egg-info/PKG-INFO +0 -303
- dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin.egg-info/SOURCES.txt +0 -29
- dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin.egg-info/entry_points.txt +0 -5
- dash_startup_loading_plugin-1.0.4/src/dash_startup_loading_plugin.egg-info/requires.txt +0 -1
- dash_startup_loading_plugin-1.0.4/tests/test_documentation.py +0 -75
- dash_startup_loading_plugin-1.0.4/tests/test_examples.py +0 -150
- dash_startup_loading_plugin-1.0.4/tests/test_plugin.py +0 -270
- {dash_startup_loading_plugin-1.0.4 → dash_startup_loading_plugin-1.1.0}/LICENSE +0 -0
- {dash_startup_loading_plugin-1.0.4 → dash_startup_loading_plugin-1.1.0}/setup.cfg +0 -0
- {dash_startup_loading_plugin-1.0.4 → dash_startup_loading_plugin-1.1.0}/src/dash_startup_loading_plugin.egg-info/dependency_links.txt +0 -0
- {dash_startup_loading_plugin-1.0.4 → dash_startup_loading_plugin-1.1.0}/src/dash_startup_loading_plugin.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dash-startup-loading-plugin
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: A configurable full-screen startup loading overlay for Dash apps, packaged as a Dash Hooks plugin.
|
|
5
|
+
Author-email: Ethan Zhang <ethan.zhang2016@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/c0debeez/dash-startup-loading-plugin
|
|
8
|
+
Keywords: dash,plotly,loading,plugin
|
|
9
|
+
Classifier: Framework :: Dash
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/plain
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: dash>=3.0.3
|
|
14
|
+
Requires-Dist: typing-extensions>=4.11
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
An installable full-screen startup loading overlay for Dash apps.
|
|
@@ -4,9 +4,9 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dash-startup-loading-plugin"
|
|
7
|
-
version = "1.0
|
|
7
|
+
version = "1.1.0"
|
|
8
8
|
description = "A configurable full-screen startup loading overlay for Dash apps, packaged as a Dash Hooks plugin."
|
|
9
|
-
readme = "
|
|
9
|
+
readme = { text = "An installable full-screen startup loading overlay for Dash apps.", content-type = "text/plain" }
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
11
|
authors = [
|
|
12
12
|
{ name = "Ethan Zhang", email = "ethan.zhang2016@gmail.com" }
|
|
@@ -17,16 +17,17 @@ license = "MIT"
|
|
|
17
17
|
license-files = ["LICENSE"]
|
|
18
18
|
dependencies = [
|
|
19
19
|
"dash>=3.0.3",
|
|
20
|
+
"typing-extensions>=4.11",
|
|
20
21
|
]
|
|
21
22
|
|
|
23
|
+
[project.urls]
|
|
24
|
+
Repository = "https://github.com/c0debeez/dash-startup-loading-plugin"
|
|
25
|
+
|
|
22
26
|
[dependency-groups]
|
|
23
27
|
test = [
|
|
24
28
|
"pytest>=8",
|
|
25
29
|
]
|
|
26
30
|
|
|
27
|
-
[project.scripts]
|
|
28
|
-
dash-startup-loading-plugin = "dash_startup_loading_plugin.examples:main"
|
|
29
|
-
|
|
30
31
|
[project.entry-points."dash_hooks"]
|
|
31
32
|
dash_startup_loading_plugin = "dash_startup_loading_plugin"
|
|
32
33
|
|
|
@@ -39,10 +40,10 @@ where = ["src"]
|
|
|
39
40
|
|
|
40
41
|
[tool.setuptools.package-data]
|
|
41
42
|
dash_startup_loading_plugin = [
|
|
42
|
-
"README.md",
|
|
43
|
-
"README.zh-CN.md",
|
|
44
43
|
"resources/*.css",
|
|
45
44
|
"resources/*.js",
|
|
45
|
+
"resources/LOADING-UI-LICENSE",
|
|
46
|
+
"py.typed",
|
|
46
47
|
]
|
|
47
48
|
|
|
48
49
|
[tool.pytest.ini_options]
|
|
@@ -5,14 +5,11 @@ from importlib.metadata import PackageNotFoundError, version
|
|
|
5
5
|
try:
|
|
6
6
|
__version__ = version("dash-startup-loading-plugin")
|
|
7
7
|
except PackageNotFoundError: # pragma: no cover - source tree fallback
|
|
8
|
-
__version__ = "1.0
|
|
8
|
+
__version__ = "1.1.0"
|
|
9
9
|
|
|
10
10
|
from .plugin import (
|
|
11
11
|
StartupLoadingConfig,
|
|
12
|
-
|
|
13
|
-
configure_dac,
|
|
14
|
-
configure_fac,
|
|
15
|
-
configure_dmc,
|
|
12
|
+
setup,
|
|
16
13
|
get_config,
|
|
17
14
|
reset_config,
|
|
18
15
|
)
|
|
@@ -20,10 +17,7 @@ from .plugin import (
|
|
|
20
17
|
__all__ = [
|
|
21
18
|
"StartupLoadingConfig",
|
|
22
19
|
"__version__",
|
|
23
|
-
"
|
|
24
|
-
"configure_dac",
|
|
25
|
-
"configure_fac",
|
|
26
|
-
"configure_dmc",
|
|
20
|
+
"setup",
|
|
27
21
|
"get_config",
|
|
28
22
|
"reset_config",
|
|
29
23
|
]
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""Dash Hooks registration and startup-overlay configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass, fields, replace
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from html import escape
|
|
10
|
+
from importlib.resources import files
|
|
11
|
+
from threading import RLock
|
|
12
|
+
from typing import Any, Literal, TypedDict, cast, get_args
|
|
13
|
+
from weakref import WeakKeyDictionary, ref
|
|
14
|
+
|
|
15
|
+
from dash import get_app, hooks
|
|
16
|
+
from dash.exceptions import AppNotFoundError
|
|
17
|
+
from flask import current_app, has_app_context, request
|
|
18
|
+
from typing_extensions import Unpack
|
|
19
|
+
|
|
20
|
+
_OVERLAY_MARKER = "data-dash-loading"
|
|
21
|
+
_BODY_PATTERN = re.compile(r"<body(?:\s[^>]*)?>", flags=re.IGNORECASE)
|
|
22
|
+
_HEAD_END_PATTERN = re.compile(r"</head\s*>", flags=re.IGNORECASE)
|
|
23
|
+
_CONFIG_LOCK = RLock()
|
|
24
|
+
_DASH_APPS_BY_SERVER: WeakKeyDictionary[Any, list[Any]] = WeakKeyDictionary()
|
|
25
|
+
_explicit_options: frozenset[str] = frozenset()
|
|
26
|
+
|
|
27
|
+
LoaderName = Literal[
|
|
28
|
+
"default",
|
|
29
|
+
"antd",
|
|
30
|
+
"accordion-loader",
|
|
31
|
+
"analyzing-image",
|
|
32
|
+
"arc",
|
|
33
|
+
"bars",
|
|
34
|
+
"bobbing-dots",
|
|
35
|
+
"bouncing-dots",
|
|
36
|
+
"classic",
|
|
37
|
+
"clock-ring",
|
|
38
|
+
"comet-spinner",
|
|
39
|
+
"concentric-ring",
|
|
40
|
+
"conveyor-loop",
|
|
41
|
+
"dash-ring",
|
|
42
|
+
"diamond",
|
|
43
|
+
"dots",
|
|
44
|
+
"dots-ring",
|
|
45
|
+
"dual-arc",
|
|
46
|
+
"fade-arc",
|
|
47
|
+
"infinity",
|
|
48
|
+
"infinity-square-snake",
|
|
49
|
+
"infinity-track",
|
|
50
|
+
"morphing-infinity",
|
|
51
|
+
"orbit-ring",
|
|
52
|
+
"pulsating-dots",
|
|
53
|
+
"pulse",
|
|
54
|
+
"pulse-dot",
|
|
55
|
+
"quarter-ring",
|
|
56
|
+
"ring",
|
|
57
|
+
"ripple",
|
|
58
|
+
"satellite-ring",
|
|
59
|
+
"skeleton",
|
|
60
|
+
"spiral",
|
|
61
|
+
"spokes",
|
|
62
|
+
"square-accordion",
|
|
63
|
+
"square-grid",
|
|
64
|
+
"square-snake",
|
|
65
|
+
"swirling",
|
|
66
|
+
"symmetric-wave",
|
|
67
|
+
"terminal",
|
|
68
|
+
"text-blink",
|
|
69
|
+
"text-dots",
|
|
70
|
+
"text-shimmer",
|
|
71
|
+
"text-shimmer-wave",
|
|
72
|
+
"triple-dot-spinner",
|
|
73
|
+
"twin-orbit",
|
|
74
|
+
"typing",
|
|
75
|
+
"wandering-eyes",
|
|
76
|
+
"wave",
|
|
77
|
+
]
|
|
78
|
+
ThemeMode = Literal["auto", "light", "dark"]
|
|
79
|
+
_LOADING_UI_LOADERS = frozenset(get_args(LoaderName)) - {"default", "antd"}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@hooks.setup(priority=100)
|
|
83
|
+
def _register_dash_app(app: Any) -> None:
|
|
84
|
+
_DASH_APPS_BY_SERVER.setdefault(app.server, []).append(ref(app))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class SetupOptions(TypedDict, total=False):
|
|
88
|
+
enabled: bool
|
|
89
|
+
aria_label: str
|
|
90
|
+
z_index: int
|
|
91
|
+
background: str
|
|
92
|
+
dark_background: str
|
|
93
|
+
loader_color: str | None
|
|
94
|
+
loader_dark_color: str | None
|
|
95
|
+
theme_mode: ThemeMode
|
|
96
|
+
loader: LoaderName
|
|
97
|
+
loader_text: str
|
|
98
|
+
loader_size: int | None
|
|
99
|
+
loader_stroke_width: int
|
|
100
|
+
custom_loader_html: str | None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True)
|
|
104
|
+
class StartupLoadingConfig:
|
|
105
|
+
"""Configuration serialized into the startup overlay.
|
|
106
|
+
|
|
107
|
+
``custom_loader_html`` is inserted verbatim and must only contain trusted
|
|
108
|
+
HTML supplied by the application author.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
enabled: bool = True
|
|
112
|
+
aria_label: str = "Loading"
|
|
113
|
+
z_index: int = 9999
|
|
114
|
+
background: str = "#ffffff"
|
|
115
|
+
dark_background: str = "#121212"
|
|
116
|
+
loader_color: str | None = None
|
|
117
|
+
loader_dark_color: str | None = None
|
|
118
|
+
theme_mode: ThemeMode = "auto"
|
|
119
|
+
loader: LoaderName = "default"
|
|
120
|
+
loader_text: str = "Loading"
|
|
121
|
+
loader_size: int | None = 12
|
|
122
|
+
loader_stroke_width: int = 2
|
|
123
|
+
custom_loader_html: str | None = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
_DEFAULT_CONFIG = StartupLoadingConfig()
|
|
127
|
+
_config = _DEFAULT_CONFIG
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _validate(config: StartupLoadingConfig) -> StartupLoadingConfig:
|
|
131
|
+
if not isinstance(config.enabled, bool):
|
|
132
|
+
raise TypeError("enabled must be a boolean")
|
|
133
|
+
if config.theme_mode not in {"auto", "light", "dark"}:
|
|
134
|
+
raise ValueError("theme_mode must be 'auto', 'light', or 'dark'")
|
|
135
|
+
if config.loader not in _LOADING_UI_LOADERS | {"default", "antd"}:
|
|
136
|
+
raise ValueError("loader must be a Loading UI loader name, 'default', or 'antd'")
|
|
137
|
+
if not isinstance(config.loader_text, str) or not config.loader_text.strip():
|
|
138
|
+
raise ValueError("loader_text must be a non-empty string")
|
|
139
|
+
for name in ("loader_color", "loader_dark_color"):
|
|
140
|
+
value = getattr(config, name)
|
|
141
|
+
if value is not None and (not isinstance(value, str) or not value.strip()):
|
|
142
|
+
raise ValueError(f"{name} must be None or a non-empty CSS color")
|
|
143
|
+
if config.loader_size is not None and config.loader_size < 0:
|
|
144
|
+
raise ValueError("loader_size must be greater than or equal to zero")
|
|
145
|
+
if config.loader_stroke_width < 0:
|
|
146
|
+
raise ValueError("loader_stroke_width must be greater than or equal to zero")
|
|
147
|
+
return config
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def setup(**changes: Unpack[SetupOptions]) -> StartupLoadingConfig:
|
|
151
|
+
"""Update the process-wide plugin configuration.
|
|
152
|
+
|
|
153
|
+
Call this before creating ``dash.Dash``. The Dash hooks registry is
|
|
154
|
+
process-wide, so one configuration is shared by all apps in the process.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
valid_names = {field.name for field in fields(StartupLoadingConfig)}
|
|
158
|
+
unknown = set(changes).difference(valid_names)
|
|
159
|
+
if unknown:
|
|
160
|
+
names = ", ".join(sorted(unknown))
|
|
161
|
+
raise TypeError(f"Unknown startup loading option(s): {names}")
|
|
162
|
+
global _config, _explicit_options
|
|
163
|
+
with _CONFIG_LOCK:
|
|
164
|
+
_config = _validate(replace(_config, **changes))
|
|
165
|
+
_explicit_options = _explicit_options.union(changes)
|
|
166
|
+
return _config
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_config() -> StartupLoadingConfig:
|
|
170
|
+
"""Return the active immutable configuration."""
|
|
171
|
+
|
|
172
|
+
with _CONFIG_LOCK:
|
|
173
|
+
return _config
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _configuration_state() -> tuple[StartupLoadingConfig, frozenset[str]]:
|
|
177
|
+
with _CONFIG_LOCK:
|
|
178
|
+
return _config, _explicit_options
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def reset_config() -> StartupLoadingConfig:
|
|
182
|
+
"""Restore the default configuration, primarily for tests."""
|
|
183
|
+
|
|
184
|
+
global _config, _explicit_options
|
|
185
|
+
with _CONFIG_LOCK:
|
|
186
|
+
_config = _DEFAULT_CONFIG
|
|
187
|
+
_explicit_options = frozenset()
|
|
188
|
+
return _config
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _mantine_forced_color_scheme(layout: Any) -> str | None:
|
|
192
|
+
"""Return a static MantineProvider color scheme without rendering the layout."""
|
|
193
|
+
|
|
194
|
+
if layout is None or callable(layout):
|
|
195
|
+
return None
|
|
196
|
+
pending = [layout]
|
|
197
|
+
seen: set[int] = set()
|
|
198
|
+
while pending:
|
|
199
|
+
component = pending.pop()
|
|
200
|
+
if component is None or id(component) in seen:
|
|
201
|
+
continue
|
|
202
|
+
seen.add(id(component))
|
|
203
|
+
if (
|
|
204
|
+
getattr(component, "_namespace", None) == "dash_mantine_components"
|
|
205
|
+
and getattr(component, "_type", None) == "MantineProvider"
|
|
206
|
+
):
|
|
207
|
+
scheme = getattr(component, "forceColorScheme", None)
|
|
208
|
+
if scheme in {"light", "dark"}:
|
|
209
|
+
return scheme
|
|
210
|
+
children = getattr(component, "children", None)
|
|
211
|
+
if isinstance(children, (list, tuple)):
|
|
212
|
+
pending.extend(children)
|
|
213
|
+
elif children is not None:
|
|
214
|
+
pending.append(children)
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _theme_config(
|
|
219
|
+
config: StartupLoadingConfig,
|
|
220
|
+
app_index: str,
|
|
221
|
+
layout: Any = None,
|
|
222
|
+
) -> dict[str, Any]:
|
|
223
|
+
values: dict[str, Any] = {
|
|
224
|
+
"themeMode": config.theme_mode,
|
|
225
|
+
"mantineBundle": "dash_mantine_components" in app_index,
|
|
226
|
+
}
|
|
227
|
+
forced_scheme = _mantine_forced_color_scheme(layout)
|
|
228
|
+
if forced_scheme:
|
|
229
|
+
values["mantineForcedColorScheme"] = forced_scheme
|
|
230
|
+
return values
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _resolved_loader_colors(
|
|
234
|
+
config: StartupLoadingConfig,
|
|
235
|
+
loader: LoaderName,
|
|
236
|
+
) -> tuple[str, str]:
|
|
237
|
+
defaults = ("#1677ff", "#4096ff") if loader == "antd" else ("#000", "#fff")
|
|
238
|
+
return (
|
|
239
|
+
config.loader_color or defaults[0],
|
|
240
|
+
config.loader_dark_color or defaults[1],
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _overlay_html(
|
|
245
|
+
config: StartupLoadingConfig,
|
|
246
|
+
loader: LoaderName | None = None,
|
|
247
|
+
) -> str:
|
|
248
|
+
aria_label = escape(config.aria_label, quote=True)
|
|
249
|
+
class_name = "dash-loading"
|
|
250
|
+
selected_loader = loader or config.loader
|
|
251
|
+
light_color, dark_color = _resolved_loader_colors(config, selected_loader)
|
|
252
|
+
if config.loader_size is not None:
|
|
253
|
+
spinner_size = f"{config.loader_size}px"
|
|
254
|
+
spinner_scale = config.loader_size / 20
|
|
255
|
+
else:
|
|
256
|
+
spinner_size = "20px"
|
|
257
|
+
spinner_scale = 1
|
|
258
|
+
loading_ui_stroke = (
|
|
259
|
+
config.loader_stroke_width / spinner_scale
|
|
260
|
+
if spinner_scale > 0
|
|
261
|
+
else config.loader_stroke_width
|
|
262
|
+
)
|
|
263
|
+
styles = {
|
|
264
|
+
"--dash-loading-background": config.background,
|
|
265
|
+
"--dash-loading-dark-background": config.dark_background,
|
|
266
|
+
"--dash-loading-loader-color": light_color,
|
|
267
|
+
"--dash-loading-loader-dark-color": dark_color,
|
|
268
|
+
"--dash-loading-size": spinner_size,
|
|
269
|
+
"--dash-loading-scale": f"{spinner_scale:g}",
|
|
270
|
+
"--dash-loading-ui-display": "none" if spinner_scale == 0 else "inline-flex",
|
|
271
|
+
"--dash-loading-stroke": f"{config.loader_stroke_width}px",
|
|
272
|
+
"--dash-loading-ui-stroke": f"{loading_ui_stroke:g}px",
|
|
273
|
+
"--dash-loading-z-index": str(config.z_index),
|
|
274
|
+
}
|
|
275
|
+
if config.background != _DEFAULT_CONFIG.background:
|
|
276
|
+
styles["--dash-loading-mantine-light-background"] = config.background
|
|
277
|
+
if config.dark_background != _DEFAULT_CONFIG.dark_background:
|
|
278
|
+
styles["--dash-loading-mantine-dark-background"] = config.dark_background
|
|
279
|
+
style = escape(";".join(f"{name}:{value}" for name, value in styles.items()), quote=True)
|
|
280
|
+
ring = (
|
|
281
|
+
'<svg class="dash-loading__ring" viewBox="0 0 24 24" fill="none" '
|
|
282
|
+
'aria-hidden="true" xmlns="http://www.w3.org/2000/svg">'
|
|
283
|
+
'<path d="M21 12.0004C20.9999 13.901 20.3981 15.7528 19.2809 17.2904'
|
|
284
|
+
'C18.1637 18.8279 16.5885 19.9723 14.7809 20.5596'
|
|
285
|
+
'C12.9733 21.1469 11.0262 21.1468 9.21864 20.5594'
|
|
286
|
+
'C7.41109 19.9721 5.83588 18.8276 4.71876 17.29'
|
|
287
|
+
'C3.60165 15.7523 2.99999 13.9005 3 11.9999'
|
|
288
|
+
'C3.00001 10.0993 3.60171 8.24755 4.71884 6.70994'
|
|
289
|
+
'C5.83598 5.17233 7.4112 4.02785 9.21877 3.44052'
|
|
290
|
+
'C11.0263 2.85319 12.9734 2.85316 14.781 3.44044" '
|
|
291
|
+
'stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
292
|
+
'</svg>'
|
|
293
|
+
)
|
|
294
|
+
loader_html = config.custom_loader_html
|
|
295
|
+
if loader_html is None:
|
|
296
|
+
if selected_loader == "default":
|
|
297
|
+
loader_html = '<span class="dash-loading__spinner" aria-hidden="true"></span>'
|
|
298
|
+
elif selected_loader == "antd":
|
|
299
|
+
loader_html = (
|
|
300
|
+
'<span class="dash-loading__antd-spinner" aria-hidden="true">'
|
|
301
|
+
'<span class="dash-loading__antd-dot">'
|
|
302
|
+
'<i></i><i></i><i></i><i></i>'
|
|
303
|
+
'</span></span>'
|
|
304
|
+
)
|
|
305
|
+
elif selected_loader == "ring":
|
|
306
|
+
loader_html = ring
|
|
307
|
+
else:
|
|
308
|
+
name = escape(selected_loader, quote=True)
|
|
309
|
+
loader_text = escape(config.loader_text, quote=True)
|
|
310
|
+
loader_html = (
|
|
311
|
+
f'<span class="dash-loading__loading-ui" data-dash-loading-ui="{name}" '
|
|
312
|
+
f'data-dash-loading-text="{loader_text}" '
|
|
313
|
+
'aria-hidden="true"></span>'
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
content_class = "dash-loading__content"
|
|
317
|
+
if config.custom_loader_html is None and selected_loader in _LOADING_UI_LOADERS:
|
|
318
|
+
content_class += " dash-loading__content--loading-ui"
|
|
319
|
+
loader_html = f'<div class="dash-loading__spinner-region">{loader_html}</div>'
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
f'<div class="{class_name}" {_OVERLAY_MARKER} '
|
|
323
|
+
'role="status" aria-live="polite" '
|
|
324
|
+
f'aria-label="{aria_label}" aria-busy="true" style="{style}">'
|
|
325
|
+
f'<div class="{content_class}">{loader_html}</div>'
|
|
326
|
+
"</div>"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@lru_cache(maxsize=4)
|
|
331
|
+
def _resource_text(name: str) -> str:
|
|
332
|
+
return files("dash_startup_loading_plugin").joinpath("resources").joinpath(name).read_text(encoding="utf-8").strip()
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _inline_head_resources(
|
|
336
|
+
app_index: str,
|
|
337
|
+
config: StartupLoadingConfig,
|
|
338
|
+
layout: Any = None,
|
|
339
|
+
) -> str:
|
|
340
|
+
theme_config = json.dumps(
|
|
341
|
+
_theme_config(config, app_index, layout),
|
|
342
|
+
ensure_ascii=False,
|
|
343
|
+
separators=(",", ":"),
|
|
344
|
+
)
|
|
345
|
+
resources = (
|
|
346
|
+
'<style data-dash-loading-resource="style">'
|
|
347
|
+
f"{_resource_text('loading.css')}"
|
|
348
|
+
"</style>"
|
|
349
|
+
'<script data-dash-loading-resource="theme">'
|
|
350
|
+
f"window.__dashLoadingThemeConfig={theme_config};"
|
|
351
|
+
f"{_resource_text('theme.js')}"
|
|
352
|
+
"</script>"
|
|
353
|
+
)
|
|
354
|
+
head_end = _HEAD_END_PATTERN.search(app_index)
|
|
355
|
+
if head_end is not None:
|
|
356
|
+
return app_index[: head_end.start()] + resources + app_index[head_end.start() :]
|
|
357
|
+
return resources + app_index
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _inject_overlay(app_index: str, layout: Any = None) -> str:
|
|
361
|
+
config, explicit_options = _configuration_state()
|
|
362
|
+
if not config.enabled or _OVERLAY_MARKER in app_index:
|
|
363
|
+
return app_index
|
|
364
|
+
|
|
365
|
+
body_match = _BODY_PATTERN.search(app_index)
|
|
366
|
+
if body_match is None:
|
|
367
|
+
return app_index
|
|
368
|
+
app_index = _inline_head_resources(app_index, config, layout)
|
|
369
|
+
body_match = _BODY_PATTERN.search(app_index)
|
|
370
|
+
assert body_match is not None
|
|
371
|
+
position = body_match.end()
|
|
372
|
+
dash_antd_bundle = "dash_antd_components" in app_index
|
|
373
|
+
loader = (
|
|
374
|
+
"antd"
|
|
375
|
+
if dash_antd_bundle and "loader" not in explicit_options
|
|
376
|
+
else config.loader
|
|
377
|
+
)
|
|
378
|
+
scripts = ""
|
|
379
|
+
if config.custom_loader_html is None and loader in _LOADING_UI_LOADERS - {"ring"}:
|
|
380
|
+
scripts += (
|
|
381
|
+
'<script data-dash-loading-resource="loading-ui">'
|
|
382
|
+
f'{_resource_text("loading-ui.js")}'
|
|
383
|
+
'</script>'
|
|
384
|
+
)
|
|
385
|
+
scripts += f'<script data-dash-loading-resource="script">{_resource_text("loading.js")}</script>'
|
|
386
|
+
return app_index[:position] + _overlay_html(config, loader) + scripts + app_index[position:]
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _current_app_layout() -> Any:
|
|
390
|
+
if has_app_context():
|
|
391
|
+
server = cast(Any, current_app)._get_current_object()
|
|
392
|
+
registered = [app_ref() for app_ref in _DASH_APPS_BY_SERVER.get(server, [])]
|
|
393
|
+
apps = [app for app in registered if app is not None]
|
|
394
|
+
if apps:
|
|
395
|
+
path = request.path
|
|
396
|
+
matches = [
|
|
397
|
+
app
|
|
398
|
+
for app in apps
|
|
399
|
+
if path.startswith(app.config.routes_pathname_prefix)
|
|
400
|
+
]
|
|
401
|
+
selected = max(
|
|
402
|
+
matches or apps,
|
|
403
|
+
key=lambda app: len(app.config.routes_pathname_prefix),
|
|
404
|
+
)
|
|
405
|
+
return selected.layout
|
|
406
|
+
try:
|
|
407
|
+
return get_app().layout
|
|
408
|
+
except AppNotFoundError:
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@hooks.index(priority=100)
|
|
413
|
+
def inject_startup_loading(app_index: str) -> str:
|
|
414
|
+
"""Inject the pre-React overlay into the final HTML document."""
|
|
415
|
+
|
|
416
|
+
return _inject_overlay(app_index, _current_app_layout())
|
|
File without changes
|
dash_startup_loading_plugin-1.1.0/src/dash_startup_loading_plugin/resources/LOADING-UI-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bartosz Zagrodzki
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|