Skip to content

Commit

Permalink
Merge pull request #2630 from plotly/feature/layout-hooks
Browse files Browse the repository at this point in the history
Feature: Add hooks for layout "pre" and "post"
  • Loading branch information
KoolADE85 authored Aug 25, 2023
2 parents cc7fde3 + e22bc10 commit 6cbac0e
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 8 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ This project adheres to [Semantic Versioning](https://semver.org/).

- [#2610](https://github.com/plotly/dash/pull/2610) Load plotly.js bundle/version from plotly.py

## Added

- [#2630](https://github.com/plotly/dash/pull/2630) New layout hooks in the renderer


## [2.12.1] - 2023-08-16

## Fixed
Expand Down
10 changes: 10 additions & 0 deletions dash/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import string
from html import escape
from functools import wraps
from typing import Union
from dash.types import RendererHooks

logger = logging.getLogger()

Expand Down Expand Up @@ -267,3 +269,11 @@ def coerce_to_list(obj):

def clean_property_name(name: str):
return name.split("@")[0]


def hooks_to_js_object(hooks: Union[RendererHooks, None]) -> str:
if hooks is None:
return ""
hook_str = ",".join(f"{key}: {val}" for key, val in hooks.items())

return f"{{{hook_str}}}"
9 changes: 9 additions & 0 deletions dash/dash-renderer/src/APIController.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,21 @@ function storeEffect(props, events, setErrorLoading) {
dispatch,
error,
graphs,
hooks,
layout,
layoutRequest
} = props;

if (isEmpty(layoutRequest)) {
if (typeof hooks.layout_pre === 'function') {
hooks.layout_pre();
}
dispatch(apiThunk('_dash-layout', 'GET', 'layoutRequest'));
} else if (layoutRequest.status === STATUS.OK) {
if (isEmpty(layout)) {
if (typeof hooks.layout_post === 'function') {
hooks.layout_post(layoutRequest.content);
}
const finalLayout = applyPersistence(
layoutRequest.content,
dispatch
Expand Down Expand Up @@ -198,6 +205,7 @@ UnconnectedContainer.propTypes = {
dispatch: PropTypes.func,
dependenciesRequest: PropTypes.object,
graphs: PropTypes.object,
hooks: PropTypes.object,
layoutRequest: PropTypes.object,
layout: PropTypes.object,
loadingMap: PropTypes.any,
Expand All @@ -211,6 +219,7 @@ const Container = connect(
state => ({
appLifecycle: state.appLifecycle,
dependenciesRequest: state.dependenciesRequest,
hooks: state.hooks,
layoutRequest: state.layoutRequest,
layout: state.layout,
loadingMap: state.loadingMap,
Expand Down
2 changes: 2 additions & 0 deletions dash/dash-renderer/src/AppContainer.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class UnconnectedAppContainer extends React.Component {
constructor(props) {
super(props);
if (
props.hooks.layout_pre !== null ||
props.hooks.layout_post !== null ||
props.hooks.request_pre !== null ||
props.hooks.request_post !== null ||
props.hooks.callback_resolved !== null ||
Expand Down
4 changes: 4 additions & 0 deletions dash/dash-renderer/src/AppProvider.react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const AppProvider = ({hooks}: any) => {

AppProvider.propTypes = {
hooks: PropTypes.shape({
layout_pre: PropTypes.func,
layout_post: PropTypes.func,
request_pre: PropTypes.func,
request_post: PropTypes.func,
callback_resolved: PropTypes.func,
Expand All @@ -25,6 +27,8 @@ AppProvider.propTypes = {

AppProvider.defaultProps = {
hooks: {
layout_pre: null,
layout_post: null,
request_pre: null,
request_post: null,
callback_resolved: null,
Expand Down
2 changes: 2 additions & 0 deletions dash/dash-renderer/src/reducers/hooks.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const customHooks = (
state = {
layout_pre: null,
layout_post: null,
request_pre: null,
request_post: null,
callback_resolved: null,
Expand Down
15 changes: 9 additions & 6 deletions dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import base64
import traceback
from urllib.parse import urlparse
from typing import Union

import flask

Expand Down Expand Up @@ -52,6 +53,7 @@
to_json,
convert_to_AttributeDict,
gen_salt,
hooks_to_js_object,
)
from . import _callback
from . import _get_paths
Expand All @@ -70,6 +72,7 @@
_import_layouts_from_pages,
)
from ._jupyter import jupyter_dash, JupyterDisplayMode
from .types import RendererHooks

# Add explicit mapping for map files
mimetypes.add_type("application/json", ".map", True)
Expand Down Expand Up @@ -134,7 +137,6 @@


def _get_traceback(secret, error: Exception):

try:
# pylint: disable=import-outside-toplevel
from werkzeug.debug import tbtools
Expand Down Expand Up @@ -339,6 +341,10 @@ class Dash:
:param add_log_handler: Automatically add a StreamHandler to the app logger
if not added previously.
:param hooks: Extend Dash renderer functionality by passing a dictionary of
javascript functions. To hook into the layout, use dict keys "layout_pre" and
"layout_post". To hook into the callbacks, use keys "request_pre" and "request_post"
"""

_plotlyjs_url: str
Expand Down Expand Up @@ -375,6 +381,7 @@ def __init__( # pylint: disable=too-many-statements
long_callback_manager=None,
background_callback_manager=None,
add_log_handler=True,
hooks: Union[RendererHooks, None] = None,
**obsolete,
):
_validate.check_obsolete(obsolete)
Expand Down Expand Up @@ -468,7 +475,7 @@ def __init__( # pylint: disable=too-many-statements
self._favicon = None

# default renderer string
self.renderer = "var renderer = new DashRenderer();"
self.renderer = f"var renderer = new DashRenderer({hooks_to_js_object(hooks)});"

# static files from the packages
self.css = Css(serve_locally)
Expand Down Expand Up @@ -1327,7 +1334,6 @@ def _setup_server(self):

# Copy over global callback data structures assigned with `dash.callback`
for k in list(_callback.GLOBAL_CALLBACK_MAP):

if k in self.callback_map:
raise DuplicateCallback(
f"The callback `{k}` provided with `dash.callback` was already "
Expand All @@ -1354,7 +1360,6 @@ def _setup_server(self):

if cancels:
for cancel_input, manager in cancels.items():

# pylint: disable=cell-var-from-loop
@self.callback(
Output(cancel_input.component_id, "id"),
Expand Down Expand Up @@ -1745,7 +1750,6 @@ def enable_dev_tools(
_reload.watch_thread.start()

if debug:

if jupyter_dash.active:
jupyter_dash.configure_callback_exception_handling(
self, dev_tools.prune_errors
Expand Down Expand Up @@ -1779,7 +1783,6 @@ def _after_request(response):
dash_total["dur"] = round((time.time() - dash_total["dur"]) * 1000)

for name, info in timing_information.items():

value = name
if info.get("desc") is not None:
value += f';desc="{info["desc"]}"'
Expand Down
10 changes: 10 additions & 0 deletions dash/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from typing_extensions import TypedDict, NotRequired


class RendererHooks(TypedDict):
layout_pre: NotRequired[str]
layout_post: NotRequired[str]
request_pre: NotRequired[str]
request_post: NotRequired[str]
callback_resolved: NotRequired[str]
request_refresh_jwt: NotRequired[str]
31 changes: 29 additions & 2 deletions tests/integration/renderer/test_request_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import pytest

from dash import Dash, Output, Input, html, dcc
from dash.types import RendererHooks
from werkzeug.exceptions import HTTPException


Expand Down Expand Up @@ -115,7 +116,6 @@ def update_output(value):


def test_rdrh002_with_custom_renderer_interpolated(dash_duo):

renderer = """
<script id="_dash-renderer" type="application/javascript">
console.log('firing up a custom renderer!')
Expand Down Expand Up @@ -198,7 +198,6 @@ def update_output(value):

@pytest.mark.parametrize("expiry_code", [401, 400])
def test_rdrh003_refresh_jwt(expiry_code, dash_duo):

app = Dash(__name__)

app.index_string = """<!DOCTYPE html>
Expand Down Expand Up @@ -295,3 +294,31 @@ def wrap(*args, **kwargs):
dash_duo.wait_for_text_to_equal("#output-token", "..")

assert len(dash_duo.get_logs()) == 2


def test_rdrh004_layout_hooks(dash_duo):
hooks: RendererHooks = {
"layout_pre": """
() => {
var layoutPre = document.createElement('div');
layoutPre.setAttribute('id', 'layout-pre');
layoutPre.innerHTML = 'layout_pre generated this text';
document.body.appendChild(layoutPre);
}
""",
"layout_post": """
(response) => {
response.props.children = "layout_post generated this text";
}
""",
}

app = Dash(__name__, hooks=hooks)
app.layout = html.Div(id="layout")

dash_duo.start_server(app)

dash_duo.wait_for_text_to_equal("#layout-pre", "layout_pre generated this text")
dash_duo.wait_for_text_to_equal("#layout", "layout_post generated this text")

assert dash_duo.get_logs() == []

0 comments on commit 6cbac0e

Please sign in to comment.