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

Hot reload #25

Merged
merged 25 commits into from
May 22, 2020
Merged
Show file tree
Hide file tree
Changes from 10 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
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
command: |
python -m venv venv
. venv/bin/activate
git clone --depth 1 https://github.com/plotly/dash.git -b add-julia-runner dash-main
git clone --depth 1 https://github.com/plotly/dash.git -b dev dash-main
rpkyle marked this conversation as resolved.
Show resolved Hide resolved
cd dash-main && pip install -e .[dev,testing] --progress-bar off && cd ~/dashjl
export PATH=$PATH:/home/circleci/.local/bin/
pytest --headless --nopercyfinalize --junitxml=test-reports/dashjl.xml --percy-assets=test/assets/ test/integration/
Expand Down
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433"
MD5 = "6ac74813-4b46-53a4-afec-0b5dc9d7885c"
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
PlotlyBase = "a03496cd-edff-5a9b-9e67-9cda94a718b5"
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

[compat]
DataStructures = "0.17.5"
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import Pkg; Pkg.add(Pkg.PackageSpec(url = "https://github.com/plotly/Dash.jl.git

```jldoctest
julia> using Dash
julia> app = dash("Test app", external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
julia> app = dash(external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
rpkyle marked this conversation as resolved.
Show resolved Hide resolved

julia> app.layout = html_div() do
html_h1("Hello Dash"),
Expand Down Expand Up @@ -55,7 +55,7 @@ __Once you have run the code to create the Dashboard, go to `http://127.0.0.1:80
```jldoctest

julia> using Dash
julia> app = dash("Test app", external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
julia> app = dash(external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])

julia> app.layout = html_div() do
dcc_input(id = "my-id", value="initial value", type = "text"),
Expand All @@ -76,7 +76,7 @@ julia> run_server(app, "0.0.0.0", 8080)
### States and Multiple Outputs
```jldoctest
julia> using Dash
julia> app = dash("Test app", external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
julia> app = dash(external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])

julia> app.layout = html_div() do
dcc_input(id = "my-id", value="initial value", type = "text"),
Expand Down
6 changes: 3 additions & 3 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import Pkg; Pkg.add("Dashboards")
```jldoctest
julia> import HTTP
julia> using Dashboards
julia> app = Dash("Test app", external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]) do
julia> app = Dash(external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]) do
html_div() do
html_h1("Hello Dashboards"),
html_div("Dashboards: Julia interface for Dash"),
Expand Down Expand Up @@ -68,7 +68,7 @@ __Once you have run the code to create the Dashboard, go to `http://127.0.0.1:80
```jldoctest
julia> import HTTP
julia> using Dashboards
julia> app = Dash("Test app", external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]) do
julia> app = Dash(external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]) do
html_div() do
dcc_input(id = "my-id", value="initial value", type = "text"),
html_div(id = "my-div")
Expand All @@ -89,7 +89,7 @@ julia> HTTP.serve(handler, HTTP.Sockets.localhost, 8080)
```jldoctest
julia> import HTTP
julia> using Dashboards
julia> app = Dash("Test app", external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]) do
julia> app = Dash(external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]) do
html_div() do
dcc_input(id = "my-id", value="initial value", type = "text"),
html_div(id = "my-div"),
Expand Down
31 changes: 24 additions & 7 deletions src/Dash.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module Dash
import HTTP, JSON2, CodecZlib, MD5
using Sockets
using MacroTools
const ROOT_PATH = realpath(joinpath(@__DIR__, ".."))
include("Components.jl")
Expand All @@ -19,7 +20,6 @@ include("utils.jl")
include("app.jl")
include("resources/registry.jl")
include("resources/application.jl")
include("config.jl")
include("handlers.jl")

@doc """
Expand All @@ -31,7 +31,7 @@ Julia backend for [Plotly Dash](https://github.com/plotly/dash)
```julia

using Dash
app = dash("Test", external_stylesheets=["https://codepen.io/chriddyp/pen/bWLwgP.css"]) do
app = dash(external_stylesheets=["https://codepen.io/chriddyp/pen/bWLwgP.css"]) do
html_div() do
dcc_input(id="graphTitle", value="Let's Dance!", type = "text"),
html_div(id="outputID"),
Expand Down Expand Up @@ -76,7 +76,7 @@ Run Dash server

#Examples
```jldoctest
julia> app = dash("Test") do
julia> app = dash) do
waralex marked this conversation as resolved.
Show resolved Hide resolved
html_div() do
html_h1("Test Dashboard")
end
Expand Down Expand Up @@ -111,10 +111,27 @@ function run_server(app::DashApp, host = HTTP.Sockets.localhost, port = 8050;
dev_tools_silence_routes_logging = dev_tools_silence_routes_logging,
dev_tools_prune_errors = dev_tools_prune_errors
)
handler = make_handler(app);
@info string("Running on http://", host, ":", port)
HTTP.serve(handler, host, port)
main_func = () -> begin
ccall(:jl_exit_on_sigint, Cvoid, (Cint,), 0)
rpkyle marked this conversation as resolved.
Show resolved Hide resolved
handler = make_handler(app);
try
task = @async HTTP.serve(handler, host, port)
rpkyle marked this conversation as resolved.
Show resolved Hide resolved
@info string("Running on http://", host, ":", port)
wait(task)
catch e
if e isa InterruptException
@info "exited"
else
rethrow(e)
end

end
end
if get_devsetting(app, :hot_reload)
hot_restart(main_func, check_interval = get_devsetting(app, :hot_reload_watch_interval))
else
main_func()
end
end


end # module
2 changes: 1 addition & 1 deletion src/app/callbacks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Create a callback that updates the output by calling function `func`.
# Examples

```julia
app = dash("Test") do
app = dash() do
html_div() do
dcc_input(id="graphTitle", value="Let's Dance!", type = "text"),
dcc_input(id="graphTitle2", value="Let's Dance!", type = "text"),
Expand Down
22 changes: 15 additions & 7 deletions src/app/dashapp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@ Dash.jl's internal representation of a Dash application.
This `struct` is not intended to be called directly; developers should create their Dash application using the `dash` function instead.
"""
mutable struct DashApp
name ::String
root_path ::String
is_interactive ::Bool
config ::DashConfig
index_string ::Union{String, Nothing}
title ::String
layout ::Union{Nothing, Component, Function}
devtools ::DevTools
callbacks ::Dict{Symbol, Callback}

DashApp(name, config, index_string) = new(name, config, index_string, nothing, DevTools(dash_env(Bool, "debug", false)), Dict{Symbol, Callback}())
DashApp(root_path, is_interactive, config, index_string, title = "Dash") = new(root_path, is_interactive, config, index_string, title, nothing, DevTools(dash_env(Bool, "debug", false)), Dict{Symbol, Callback}())

end

Expand All @@ -41,6 +43,7 @@ function Base.setproperty!(app::DashApp, property::Symbol, value)
property == :name && return set_name!(app, value)
property == :index_string && return set_index_string!(app, value)
property == :layout && return set_layout!(app::DashApp, value)
property == :title && return set_title!(app::DashApp, value)

property in fieldnames(DashApp) && error("The property `$(property)` of `DashApp` is read-only")

Expand All @@ -51,6 +54,10 @@ function set_name!(app::DashApp, name)
setfield!(app, :name, name)
end

function set_title!(app::DashApp, title)
setfield!(app, :title, title)
end

get_name(app::DashApp) = app.name

function set_layout!(app::DashApp, component::Union{Component,Function})
Expand Down Expand Up @@ -106,6 +113,8 @@ get_devsetting(app::DashApp, name::Symbol) = getproperty(app.devtools, name)

get_setting(app::DashApp, name::Symbol) = getproperty(app.config, name)

get_assets_path(app::DashApp) = joinpath(app.root_path, get_setting(app, :assets_folder))

"""
dash(name::String;
external_stylesheets,
Expand Down Expand Up @@ -134,7 +143,6 @@ If a parameter can be set by an environment variable, that is listed as:
Values provided here take precedence over environment variables.

# Arguments
- `name::String` - The name of your application
- `assets_folder::String` - a path, relative to the current working directory,
for extra files to be used in the browser. Default `'assets'`. All .js and .css files will be loaded immediately unless excluded by `assets_ignore`, and other files such as images will be served if requested.

Expand Down Expand Up @@ -212,7 +220,7 @@ If a parameter can be set by an environment variable, that is listed as:
files and data served by HTTP.jl when supported by the client. Set to
``false`` to disable compression completely.
"""
function dash(name::String = dash_env("dash_name", "");
function dash(;
external_stylesheets = ExternalSrcType[],
external_scripts = ExternalSrcType[],
url_base_pathname = dash_env("url_base_pathname"),
Expand Down Expand Up @@ -242,7 +250,7 @@ function dash(name::String = dash_env("dash_name", "");
requests_pathname_prefix,
routes_pathname_prefix
)...,
absolute_assets_path(assets_folder),
assets_folder,
lstrip(assets_url_path, '/'),
assets_ignore,
serve_locally,
Expand All @@ -254,6 +262,6 @@ function dash(name::String = dash_env("dash_name", "");
show_undo_redo,
compress
)
result = DashApp(name, config, index_string)
result = DashApp(app_root_path(), isinteractive(), config, index_string)
return result
end
end
4 changes: 2 additions & 2 deletions src/app/devtools.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ struct DevTools
props_check::Bool
serve_dev_bundles::Bool
hot_reload::Bool
hot_reload_interval::Float32
hot_reload_watch_interval::Float32
hot_reload_interval::Float64
hot_reload_watch_interval::Float64
hot_reload_max_retry::Int
silence_routes_logging::Bool
prune_errors::Bool
Expand Down
53 changes: 50 additions & 3 deletions src/handler/handlers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ function process_dependencies(request::HTTP.Request, state::HandlerState)
end

function process_index(request::HTTP.Request, state::HandlerState)
get_cache(state).need_recache && rebuild_cache!(state)
return HTTP.Response(
200,
["Content-Type", "text/html"],
Expand Down Expand Up @@ -148,7 +149,7 @@ end

function process_assets(request::HTTP.Request, state::HandlerState; file_path::AbstractString)
app = state.app
filename = joinpath(get_setting(app, :assets_folder), file_path)
filename = joinpath(get_assets_path(app), file_path)

try
headers = Pair{String,String}[]
Expand All @@ -162,7 +163,47 @@ function process_assets(request::HTTP.Request, state::HandlerState; file_path::A

end

const dash_router = HTTP.Router()
function process_reload_hash(request::HTTP.Request, state::HandlerState)
reload_tuple = (
reloadHash = state.reload.hash,
hard = state.reload.hard,
packages = keys(state.cache.resources.files),
files = state.reload.changed_assets
)
state.reload.hard = false
state.reload.changed_assets = []
return HTTP.Response(200, ["Content-Type" => "application/json"], body = JSON2.write(reload_tuple))

end

function start_reaload_poll(state::HandlerState)
rpkyle marked this conversation as resolved.
Show resolved Hide resolved
folders = Set{String}()
push!(folders, get_assets_path(state.app))
push!(folders, state.registry.dash_renderer.path)
for pkg in values(state.registry.components)
push!(folders, pkg.path)
end
state.reload.task = @async poll_folders(folders; interval = get_devsetting(state.app, :hot_reload_watch_interval)) do file, ts, deleted
rpkyle marked this conversation as resolved.
Show resolved Hide resolved
state.reload.hard = true
state.reload.hash = generate_hash()
assets_path = get_assets_path(state.app)
if startswith(file, assets_path)
state.cache.need_recache = true
rel_path = lstrip(
replace(relpath(file, assets_path), '\\'=>'/'),
'/'
)
push!(state.reload.changed_assets,
ChangedAsset(
asset_path(state.app, rel_path),
trunc(Int, ts),
endswith(file, "css")
)
)
end

end
end

validate_layout(layout::Component) = validate(layout)

Expand All @@ -181,6 +222,7 @@ function make_handler(app::DashApp, registry::ResourcesRegistry; check_layout =
router = Router()
add_route!(process_layout, router, "$(prefix)_dash-layout")
add_route!(process_dependencies, router, "$(prefix)_dash-dependencies")
add_route!(process_reload_hash, router, "$(prefix)_reload-hash")
add_route!(process_resource, router, "$(prefix)_dash-component-suites/<namespace>/<path>")
add_route!(process_assets, router, "$(prefix)$(assets_url_path)/<file_path>")
add_route!(process_callback, router, "POST", "$(prefix)_dash-update-component")
Expand All @@ -190,7 +232,12 @@ function make_handler(app::DashApp, registry::ResourcesRegistry; check_layout =
handler = state_handler(router, state)
get_setting(app, :compress) && (handler = compress_handler(handler))

HTTP.handle(handler, HTTP.Request("GET", prefix)) #For handler precompilation
compile_request = HTTP.Request("GET", "/test_big")
rpkyle marked this conversation as resolved.
Show resolved Hide resolved
HTTP.setheader(compile_request, "Accept-Encoding" => "gzip")
HTTP.handle(handler, compile_request) #For handler precompilation

get_devsetting(app, :hot_reload) && start_reaload_poll(state)
rpkyle marked this conversation as resolved.
Show resolved Hide resolved

return handler
end

Expand Down
2 changes: 1 addition & 1 deletion src/handler/index_page.jl
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ function index_page(app::DashApp, resources::ApplicationResources)

result = interpolate_string(app.index_string,
metas = metas_html(app),
title = app.name,
title = app.title,
favicon = favicon_html(app),
css = css_html(app, resources),
app_entry = app_entry_html(),
Expand Down
24 changes: 21 additions & 3 deletions src/handler/state.jl
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
struct ChangedAsset
url ::String
modified ::Int
rpkyle marked this conversation as resolved.
Show resolved Hide resolved
is_css ::Bool
end
mutable struct StateReload
hash::Union{String, Nothing}
hard::Bool
changed_assets::Vector{ChangedAsset}
task ::Union{Nothing, Task}
StateReload(hash) = new(hash, false, ChangedAsset[], nothing)
end

mutable struct StateCache
resources::ApplicationResources
index_string ::String
dependencies_json ::String
StateCache(app, registry) = new(_cache_tuple(app, registry)...)
need_recache ::Bool
StateCache(app, registry) = new(_cache_tuple(app, registry)..., false)
end

function _dependencies_json(app::DashApp)
Expand All @@ -27,12 +41,16 @@ struct HandlerState
app::DashApp
registry::ResourcesRegistry
cache::StateCache
HandlerState(app, registry = main_registry()) = new(app, registry, StateCache(app, registry))
reload::StateReload
HandlerState(app, registry = main_registry()) = new(app, registry, StateCache(app, registry), make_reload_state(app))
end

make_reload_state(app::DashApp) = get_devsetting(app, :hot_reload) ? StateReload(generate_hash()) : StateReload(nothing)

get_cache(state::HandlerState) = state.cache

function rebuild_cache!(state::HandlerState)
cache = get_cache(state)
(cache.resources, cache.index_string, cache.dependencies) = _cache_tuple(state.app, state.registry)
(cache.resources, cache.index_string, cache.dependencies_json) = _cache_tuple(state.app, state.registry)
cache.need_recache = false
end
Loading