Skip to content

Commit

Permalink
Static previews for input components (#140)
Browse files Browse the repository at this point in the history
* First steps towards static previews

* Rename component static build script

* More explicit path manipulation

* Use statically rendered versions of components

* Opt into static previews

* Work around missing fonts.css

* Build static component previews during `make all`

* Add TODO

* Add comment
  • Loading branch information
jcheng5 authored Apr 30, 2024
1 parent 21eca04 commit 335b01a
Show file tree
Hide file tree
Showing 23 changed files with 191 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ __pycache__/

.venv/
__pycache__/

components/static/
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
requirements \
quarto-exts \
site serve \
static-components \
clean clean-extensions clean-venv distclean

.DEFAULT_GOAL := help
Expand All @@ -12,7 +13,7 @@ PYBIN = $(VENV)/bin


## Build everything
all: deps quartodoc site
all: deps quartodoc static-components site

# Any targets that depend on $(VENV) or $(PYBIN) will cause the venv to be
# created. To use the ven, python scripts should run with the prefix $(PYBIN),
Expand Down Expand Up @@ -90,6 +91,7 @@ serve:
## Remove Quarto website build files
clean:
rm -rf _build
rm -rf components/static
cd py-shiny/docs && make clean

## Remove Quarto extensions
Expand All @@ -102,3 +104,7 @@ clean-venv:

## Remove all build files (Quarto website, quarto extensions, venv)
distclean: clean clean-extensions clean-venv

static-components:
rm -rf components/static
. $(PYBIN)/activate && python components/make-static-previews.py
1 change: 1 addition & 0 deletions _quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ project:
- "*.gif"
- "*.jpg"
- "*.svg"
- /components/static/**
post-render:
- scripts/post-render.py

Expand Down
14 changes: 14 additions & 0 deletions components/_partials/components-list.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,25 @@
animation-fill-mode: forwards;
visibility: hidden;
border-radius: 10px;">
<% if (item.appPreview.static) { // Handle the case where the app preview is prerendered by make-static-previews.py %>
```{=html}
<div class="sourceCode" data-engine="python">
<div class="shinylive-wrapper" style="">
<div class="shinylive-container viewer">
<div class="shinylive-viewer">
<iframe class="app-frame" src="<%= item.appPreview.file.replace(/^components\//, "static/").replace(/\.py$/, ".html") %>"></iframe>
</div>
</div>
</div>
</div>
```
<% } else { %>
```{shinylive-python}
#| standalone: true
#| components: [viewer]
<%= Deno.readTextFileSync(item.appPreview.file) %>
```
<% } %>
</div>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions components/inputs/action-button/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Action Button
sidebar: components
appPreview:
file: components/inputs/action-button/app-preview.py
static: true
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
Expand Down
1 change: 1 addition & 0 deletions components/inputs/action-link/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Action Link
sidebar: components
appPreview:
file: components/inputs/action-link/app-preview.py
static: true
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
Expand Down
1 change: 1 addition & 0 deletions components/inputs/checkbox-group/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Checkbox Group
sidebar: components
appPreview:
file: components/inputs/checkbox-group/app-preview.py
static: true
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
Expand Down
1 change: 1 addition & 0 deletions components/inputs/checkbox/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Checkbox
sidebar: components
appPreview:
file: components/inputs/checkbox/app-preview.py
static: true
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
Expand Down
1 change: 1 addition & 0 deletions components/inputs/date-range-selector/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Date Range Selector
sidebar: components
appPreview:
file: components/inputs/date-range-selector/app-preview.py
static: true
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
Expand Down
1 change: 1 addition & 0 deletions components/inputs/date-selector/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Date Selector
sidebar: components
appPreview:
file: components/inputs/date-selector/app-preview.py
static: true
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
Expand Down
1 change: 1 addition & 0 deletions components/inputs/numeric-input/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Numeric Input
sidebar: components
appPreview:
file: components/inputs/numeric-input/app-preview.py
static: true
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
Expand Down
1 change: 1 addition & 0 deletions components/inputs/password-field/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Password Field
sidebar: components
appPreview:
file: components/inputs/password-field/app-preview.py
static: true
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
Expand Down
1 change: 1 addition & 0 deletions components/inputs/radio-buttons/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Radio Buttons
sidebar: components
appPreview:
file: components/inputs/radio-buttons/app-preview.py
static: true
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
Expand Down
1 change: 1 addition & 0 deletions components/inputs/select-multiple/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Select (Multiple)
sidebar: components
appPreview:
file: components/inputs/select-multiple/app-preview.py
static: true
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
Expand Down
1 change: 1 addition & 0 deletions components/inputs/select-single/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Select (Single)
sidebar: components
appPreview:
file: components/inputs/select-single/app-preview.py
static: true
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
Expand Down
1 change: 1 addition & 0 deletions components/inputs/selectize-multiple/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Selectize (Multiple)
sidebar: components
appPreview:
file: components/inputs/selectize-multiple/app-preview.py
static: true
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
Expand Down
1 change: 1 addition & 0 deletions components/inputs/selectize-single/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Selectize (Single)
sidebar: components
appPreview:
file: components/inputs/selectize-single/app-preview.py
static: true
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
Expand Down
1 change: 1 addition & 0 deletions components/inputs/slider-range/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Slider Range
sidebar: components
appPreview:
file: components/inputs/slider-range/app-preview.py
static: true
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
Expand Down
1 change: 1 addition & 0 deletions components/inputs/slider/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Slider
sidebar: components
appPreview:
file: components/inputs/slider/app-preview.py
static: true
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
Expand Down
1 change: 1 addition & 0 deletions components/inputs/switch/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Switch
sidebar: components
appPreview:
file: components/inputs/switch/app-preview.py
static: true
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
Expand Down
1 change: 1 addition & 0 deletions components/inputs/text-area/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Text Area
sidebar: components
appPreview:
file: components/inputs/text-area/app-preview.py
static: true
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
Expand Down
1 change: 1 addition & 0 deletions components/inputs/text-box/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Text Box
sidebar: components
appPreview:
file: components/inputs/text-box/app-preview.py
static: true
listing:
- id: example
template: ../../_partials/components-detail-example.ejs
Expand Down
149 changes: 149 additions & 0 deletions components/make-static-previews.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import importlib
import os
import shutil
import sys
import time
from pathlib import Path
from typing import Generator

import shiny.html_dependencies
from _qmd import get_qmd_split
from htmltools import Tag, head_content, tags

compdir = Path(__file__).parent # ./components
rootdir = compdir.parent # repository root
cwd = Path(os.getcwd()) # current working directory
# This directory will be where we put all the dependencies from all the
# static component examples, rather than forcing the browser to fetch
# multiple redundant copies from various subfolders.
libdir = compdir / "static" / "lib"

sys.path.insert(0, str(rootdir))


def components(path: Path) -> Generator[Path, None, None]:
for dirpath, _dirnames, filenames in os.walk(path):
if "index.qmd" in filenames:
yield Path(dirpath)


def static_preview_app_files(basedir: Path) -> Generator[Path, None, None]:
for compdir in components(basedir):
meta, _ = get_qmd_split(compdir / "index.qmd")
if meta.get("appPreview", {}).get("static", False):
app_path = Path(meta["appPreview"]["file"])
if not (rootdir / app_path).is_file():
raise FileNotFoundError(
f"{rootdir / app_path} not found (referenced from {compdir / 'index.qmd'})"
)
yield app_path


# components/foo/bar.py => components/static/foo/bar.html
def infile_to_outfile(infile: Path) -> Path:
if infile.parts[0] != "components":
raise ValueError(f"Expected components/... path, got {infile}")
return Path(infile.parts[0], "static", *infile.parts[1:]).with_suffix(".html")


def outfile_to_infile(outfile: Path) -> Path:
if (
len(outfile.parts) < 3
or outfile.parts[0] != "components"
or outfile.parts[1] != "static"
):
raise ValueError(f"Expected components/static/... path, got {outfile}")
return outfile


def render_static_preview(appfile: Path, destfile: Path, libdir: Path):
appfile = rootdir / appfile
destfile = rootdir / destfile

app_mod = importlib.import_module(
str(appfile.relative_to(cwd).with_suffix("")).replace("/", ".")
)
if "app_ui" not in dir(app_mod):
raise RuntimeError(f"app_ui not found in {appfile}")
app_ui = app_mod.app_ui

enrich_app_ui(app_ui)

destfile.parent.mkdir(parents=True, exist_ok=True)

app_ui.save_html(destfile, libdir=relative_to(libdir, destfile.parent))


def enrich_app_ui(app_ui: Tag):
"""
Need to add Shiny dependency because some inputs/outputs won't look right without
being intialized as part of the Shiny binding process.
"""
app_ui.append(shiny.html_dependencies.shiny_deps())
app_ui.append(
head_content(
tags.script(
"""
window.Shiny = window.Shiny || {};
window.Shiny.createSocket = function(url) {
// Prevent Shiny from trying to connect to a live server
// by returning a dummy WebSocket object that just no-ops
return {
addEventListener: (event, callback) => {},
send: () => {},
close: () => {},
readyState: 0,
url
};
};
"""
)
)
)


# TODO: Remove this function once we upgrade to Python 3.12
def relative_to(path1: Path, path2: Path) -> Path:
"""
Equivalent to `path1.relative_to(path2, walk_up=True)`, except that walk_up=True is
only supported from Python 3.12 on and this function works on older versions. If and
when we can assume Python 3.12, this function can be removed in favor of the
built-in method.
Example:
>>> relative_to(Path("a/b/c"), Path("a/b/c/d/e"))
Path('d/e')
>>> relative_to(Path("a/b/c/d/e"), Path("a/b/c/d/f/g"))
Path('../../f/g')
"""
return Path(os.path.relpath(path1, path2))


if __name__ == "__main__":
for app_path in static_preview_app_files(rootdir / "components"):
html_path = infile_to_outfile(app_path)

# # Skip if up-to-date, unless -f flag is passed
# if html_path.stat().st_mtime > (rootdir / app_path).stat().st_mtime:
# continue

print(f"{app_path}\n Writing to {html_path}")
start_time = time.perf_counter()
render_static_preview(app_path, html_path, libdir)
end_time = time.perf_counter()
print(f" Succeeded in {((end_time - start_time) * 1000):.2f}ms")

# As of this writing, there's a bug in the bootstrap dependency that ships with
# shiny in that it doesn't include all_files=True, so some font stuff is missing.
# This code works around the bug by copying the missing files from the shiny package
# to the static lib directory. When the bug is fixed, this code can be removed.
for bootstrap_dir in libdir.glob("bootstrap-5.*"):
if not bootstrap_dir.is_dir() or (bootstrap_dir / "font.css").exists():
continue
bootstrap_src_dir = (
Path(list(shiny.__path__)[0]) / "www" / "shared" / "bootstrap"
)
if (bootstrap_src_dir / "font.css").exists():
shutil.copyfile(bootstrap_src_dir / "font.css", bootstrap_dir / "font.css")
shutil.copytree(bootstrap_src_dir / "fonts", bootstrap_dir / "fonts")
break

0 comments on commit 335b01a

Please sign in to comment.