Skip to content

Commit

Permalink
[Feat] Enable external serving of Vizro assets (#775)
Browse files Browse the repository at this point in the history
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: huong-li-nguyen <huong_li_nguyen@mckinsey.com>
  • Loading branch information
3 people authored Oct 10, 2024
1 parent 358742d commit 2d41b0b
Show file tree
Hide file tree
Showing 26 changed files with 345 additions and 160 deletions.
1 change: 1 addition & 0 deletions .vale/styles/Microsoft/ignore.txt
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,4 @@ Langchain's
codespace
codespaces
uv
jsDeliver
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!--
A new scriv changelog fragment.
Uncomment the section that is right (remove the HTML comment wrapper).
-->

<!--
### Highlights ✨
- A bullet item for the Highlights ✨ category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Removed
- A bullet item for the Removed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->

### Added

- All Vizro resources are served through a CDN when `serve_locally=False`. ([#775](https://github.com/mckinsey/vizro/pull/775))

<!--
### Changed
- A bullet item for the Changed category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
<!--
### Deprecated
- A bullet item for the Deprecated category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->

### Fixed

- Remove extraneous `<link>` to font file. ([#775](https://github.com/mckinsey/vizro/pull/775))
- Fix user override of Vizro's JavaScript resources. ([#775](https://github.com/mckinsey/vizro/pull/775))

<!--
### Security
- A bullet item for the Security category with a link to the relevant PR at the end of your entry, e.g. Enable feature XXX ([#1](https://github.com/mckinsey/vizro/pull/1))
-->
2 changes: 1 addition & 1 deletion vizro-core/docs/pages/user-guides/data.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ data_manager.cache = Cache(config={"CACHE_TYPE": "SimpleCache", "CACHE_DEFAULT_T

!!! warning

Simple cache exists purely for single-process development purposes and is not intended to be used in production. If you deploy with multiple workers, [for example with gunicorn](run.md/#gunicorn), then you should use a production-ready cache backend. All of Flask-Caching's [built-in backends](https://flask-caching.readthedocs.io/en/latest/#built-in-cache-backends) other than `SimpleCache` are suitable for production. In particular, you might like to use [`FileSystemCache`](https://cachelib.readthedocs.io/en/stable/file/) or [`RedisCache`](https://cachelib.readthedocs.io/en/stable/redis/):
Simple cache exists purely for single-process development purposes and is not intended to be used in production. If you deploy with multiple workers, [for example with Gunicorn](run.md/#gunicorn), then you should use a production-ready cache backend. All of Flask-Caching's [built-in backends](https://flask-caching.readthedocs.io/en/latest/#built-in-cache-backends) other than `SimpleCache` are suitable for production. In particular, you might like to use [`FileSystemCache`](https://cachelib.readthedocs.io/en/stable/file/) or [`RedisCache`](https://cachelib.readthedocs.io/en/stable/redis/):

```py title="Production-ready caches"
# Store cached data in CACHE_DIR
Expand Down
8 changes: 6 additions & 2 deletions vizro-core/docs/pages/user-guides/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ INFO:werkzeug:WARNING: This is a development server. Do not use it in a producti

!!! warning "In production"

As per the above warning message, which is [further explained in the Flask documentation](https://flask.palletsprojects.com/en/3.0.x/deploying/), the Flask development server is intended for use only during local development and **should not** be used when deploying to production. Instead, you should instead use a production-ready solution such as [gunicorn](#gunicorn).
As per the above warning message, which is [further explained in the Flask documentation](https://flask.palletsprojects.com/en/3.0.x/deploying/), the Flask development server is intended for use only during local development and **should not** be used when deploying to production. Instead, you should instead use a production-ready solution such as [Gunicorn](#gunicorn).

### Automatic reloading and debugging

Expand Down Expand Up @@ -147,4 +147,8 @@ A Vizro app wraps a Dash app, which itself wraps a Flask app. Hence to deploy a

Internally, `app = Vizro()` contains a Flask app in `app.dash.server`. However, as a convenience, the Vizro `app` itself implements the [WSGI application interface](https://werkzeug.palletsprojects.com/en/3.0.x/terms/#wsgi) as a shortcut to the underlying Flask app. This means that, as in the [above example with Gunicorn](#gunicorn), the Vizro `app` object can be directly supplied to the WSGI server.

[`Vizro`][vizro.Vizro] accepts `**kwargs` that are passed through to `Dash`. This enables you to configure the underlying Dash app using the same [arguments that are available](https://dash.plotly.com/reference#dash.dash) in `Dash`. For example, in a deployment context, you might like to specify a custom `url_base_pathname` to serve your Vizro app at a specific URL rather than at your domain root.
[`Vizro`][vizro.Vizro] accepts `**kwargs` that are passed through to `Dash`. This enables you to configure the underlying Dash app using the same [arguments that are available](https://dash.plotly.com/reference#dash.dash) in `Dash`. For example, in a deployment context, some of the below arguments may be useful:

- `url_base_pathname`: serve your Vizro app at a specific path rather than at the domain root. For example, if you host your dashboard at http://www.example.com/my_dashboard/ then you would set `url_base_pathname="/my_dashboard/"` .
- `serve_locally`: set to `False` to [serve Dash component libraries from a Content Delivery Network (CDN)](https://dash.plotly.com/external-resources#serving-dash's-component-libraries-locally-or-from-a-cdn), which reduces load on the server and can improve performance. Vizro uses [jsDeliver](https://www.jsdelivr.com/) as a CDN for CSS and JavaScript sources.
- `assets_external_path`: when `serve_locally=False`, you can also [serve your own assets from a CDN](https://dash.plotly.com/external-resources#load-assets-from-a-folder-hosted-on-a-cdn).
4 changes: 2 additions & 2 deletions vizro-core/examples/scratch_dev/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
],
)

dashboard = vm.Dashboard(pages=[page])
dashboard = vm.Dashboard(pages=[page], navigation=vm.Navigation(nav_selector=vm.NavBar()))

if __name__ == "__main__":
Vizro().build(dashboard).run()
Vizro(serve_locally=False).build(dashboard).run()
63 changes: 25 additions & 38 deletions vizro-core/src/vizro/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
import os

import plotly.io as pio
from dash.development.base_component import Component
from dash.development.base_component import Component, ComponentRegistry

from ._constants import VIZRO_ASSETS_PATH
from ._themes import dark, light
from ._vizro import Vizro
from ._vizro import Vizro, _make_resource_spec

logging.basicConfig(level=os.getenv("VIZRO_LOG_LEVEL", "WARNING"))
pio.templates["vizro_dark"] = dark
Expand All @@ -16,42 +17,28 @@
__version__ = "0.1.25.dev0"


# For the below _css_dist to be used by Dash, it must be retrieved by dash.resources.Css.get_all_css(). This means it
# must be added to dash.development.base_component.ComponentRegistry. The simplest way to do this is to run
# For the below _css_dist and _js_dist to be used by Dash, they must be retrieved by dash.resources.Css.get_all_css().
# This means adding them to dash.development.base_component.ComponentRegistry. The simplest way to do this is to run
# ComponentRegistry.registry.add("vizro") and this appears to be sufficient for our needs, but it is not documented
# anywhere. The same function is run (together with some others which we probably don't need) when subclassing
# Component, thanks to the metaclass ComponentMeta. So we define a dummy component to go through this safer route,
# even though we don't need the component for anything. _css_dist is automatically served on import of vizro, regardless
# of whether the Vizro class or any other bits are used.
class _Dummy(Component):
pass


# For dev versions, a branch or tag called e.g. 0.1.20.dev0 does not exist and so won't work with the CDN. We point
# to main instead, but this can be manually overridden to the current feature branch name if required.
# _git_branch = __version__ if "dev" not in __version__ else "main"
_git_branch = __version__ if "dev" not in __version__ else "main"
_library_css = ["static/css/figures"]
_base_external_url = f"https://cdn.jsdelivr.net/gh/mckinsey/vizro@{_git_branch}/vizro-core/src/vizro/"

# CSS is packaged and accessed using relative_package_path when serve_locally=False (the default) in
# the Dash instantiation. When serve_locally=True then, where defined, external_url will be used instead.
_css_dist = [
{
"namespace": "vizro",
"relative_package_path": f"{css_file}.css",
"external_url": f"{_base_external_url}{css_file}.min.css",
}
for css_file in _library_css
# anywhere. The same function is run (together with some others which we don't need and make things a bit dirty) when
# subclassing Component, thanks to the metaclass ComponentMeta. We used to define a dummy component and do this route,
# even though we don't need the component for anything, but it's cleaner to do ComponentRegistry.registry.add("vizro").
# Since we need to *remove* the library resources when Vizro is used as a framework it's also clearer when this gets
# reversed by ComponentRegistry.registry.discard("vizro") in Vizro(). _css_dist and _js_dist is automatically served on
# import of vizro, regardless of whether the Vizro class or any other bits are used.
ComponentRegistry.registry.add("vizro")

# Files needed to use Vizro as a library (not a framework), e.g. in a pure Dash app.
# This list should be kept to the bare minimum so we don't insert any more than the minimum required CSS on pure Dash
# apps. At the moment the only library components we support just are KPI cards. Note that anything that's not CSS
# is handled as a script, even if it's a font file or image.
_library_css_files = [
VIZRO_ASSETS_PATH / "css/figures.css",
]
_library_js_files = [
VIZRO_ASSETS_PATH / "css/fonts/material-symbols-outlined.woff2",
]


# Include font file so that figures with icons can be used outside Vizro as pure Dash components.
# The file can be served through the CDN in the same way as the CSS files but external_url is irrelevant here. The way
# the file is requested is through a relative url("./fonts/...") in the requesting CSS file. When the CSS file is
# served from the CDN then this will refer to the font file also on the CDN.
_css_dist.append(
{
"namespace": "vizro",
"relative_package_path": "static/css/fonts/material-symbols-outlined.woff2",
}
)
_css_dist = [_make_resource_spec(css_file) for css_file in sorted(_library_css_files)]
_js_dist = [_make_resource_spec(js_file) for js_file in sorted(_library_js_files)]
4 changes: 3 additions & 1 deletion vizro-core/src/vizro/_constants.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""File to store constants."""

from pathlib import Path

ALL_OPTION = "ALL"
NONE_OPTION = "NONE"
STATIC_URL_PREFIX = "vizro"
MODULE_PAGE_404 = "not_found_404"
EMPTY_SPACE_CONST = -1
ON_PAGE_LOAD_ACTION_PREFIX = "on_page_load_action"
FILTER_ACTION_PREFIX = "filter_action"
PARAMETER_ACTION_PREFIX = "parameter_action"
ACCORDION_DEFAULT_TITLE = "SELECT PAGE"
VIZRO_ASSETS_PATH = Path(__file__).with_name("static")
Loading

0 comments on commit 2d41b0b

Please sign in to comment.