Skip to content

Commit b50e5dc

Browse files
authored
Merge pull request #25 from plotly/hot_reload
Hot reload
2 parents d034466 + ff2adcc commit b50e5dc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+663
-276
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. This projec
33

44
### [UNRELEASED]
55
### Added
6+
- Support for hot reloading on application or asset changes [#25](plotly/Dash.jl#25)
67
- Asset serving of CSS, JavaScript, and other resources [#18](plotly/Dash.jl#18)
78
- Support for passing functions as layouts [#18](plotly/Dash.jl#18)
89
- Resource registry for component assets [#18](plotly/Dash.jl#18)

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ LibGit2 = "76f85450-5226-5b5a-8eaa-529ad045b433"
1313
MD5 = "6ac74813-4b46-53a4-afec-0b5dc9d7885c"
1414
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
1515
PlotlyBase = "a03496cd-edff-5a9b-9e67-9cda94a718b5"
16+
Sockets = "6462fe0b-24de-5631-8697-dd941f90decc"
1617
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
18+
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
1719

1820
[compat]
1921
DataStructures = "0.17.5"

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import Pkg; Pkg.add(Pkg.PackageSpec(url = "https://github.com/plotly/Dash.jl.git
2121

2222
```jldoctest
2323
julia> using Dash
24-
julia> app = dash("Test app", external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
24+
julia> app = dash(external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
2525
2626
julia> app.layout = html_div() do
2727
html_h1("Hello Dash"),
@@ -55,7 +55,7 @@ __Once you have run the code to create the Dashboard, go to `http://127.0.0.1:80
5555
```jldoctest
5656
5757
julia> using Dash
58-
julia> app = dash("Test app", external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
58+
julia> app = dash(external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
5959
6060
julia> app.layout = html_div() do
6161
dcc_input(id = "my-id", value="initial value", type = "text"),
@@ -76,7 +76,7 @@ julia> run_server(app, "0.0.0.0", 8080)
7676
### States and Multiple Outputs
7777
```jldoctest
7878
julia> using Dash
79-
julia> app = dash("Test app", external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
79+
julia> app = dash(external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"])
8080
8181
julia> app.layout = html_div() do
8282
dcc_input(id = "my-id", value="initial value", type = "text"),

docs/src/index.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import Pkg; Pkg.add("Dashboards")
3333
```jldoctest
3434
julia> import HTTP
3535
julia> using Dashboards
36-
julia> app = Dash("Test app", external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]) do
36+
julia> app = Dash(external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]) do
3737
html_div() do
3838
html_h1("Hello Dashboards"),
3939
html_div("Dashboards: Julia interface for Dash"),
@@ -68,7 +68,7 @@ __Once you have run the code to create the Dashboard, go to `http://127.0.0.1:80
6868
```jldoctest
6969
julia> import HTTP
7070
julia> using Dashboards
71-
julia> app = Dash("Test app", external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]) do
71+
julia> app = Dash(external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]) do
7272
html_div() do
7373
dcc_input(id = "my-id", value="initial value", type = "text"),
7474
html_div(id = "my-div")
@@ -89,7 +89,7 @@ julia> HTTP.serve(handler, HTTP.Sockets.localhost, 8080)
8989
```jldoctest
9090
julia> import HTTP
9191
julia> using Dashboards
92-
julia> app = Dash("Test app", external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]) do
92+
julia> app = Dash(external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"]) do
9393
html_div() do
9494
dcc_input(id = "my-id", value="initial value", type = "text"),
9595
html_div(id = "my-div"),

src/Dash.jl

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module Dash
22
import HTTP, JSON2, CodecZlib, MD5
3+
using Sockets
34
using MacroTools
45
const ROOT_PATH = realpath(joinpath(@__DIR__, ".."))
56
include("Components.jl")
@@ -19,7 +20,6 @@ include("utils.jl")
1920
include("app.jl")
2021
include("resources/registry.jl")
2122
include("resources/application.jl")
22-
include("config.jl")
2323
include("handlers.jl")
2424

2525
@doc """
@@ -31,7 +31,7 @@ Julia backend for [Plotly Dash](https://github.com/plotly/dash)
3131
```julia
3232
3333
using Dash
34-
app = dash("Test", external_stylesheets=["https://codepen.io/chriddyp/pen/bWLwgP.css"]) do
34+
app = dash(external_stylesheets=["https://codepen.io/chriddyp/pen/bWLwgP.css"]) do
3535
html_div() do
3636
dcc_input(id="graphTitle", value="Let's Dance!", type = "text"),
3737
html_div(id="outputID"),
@@ -76,7 +76,7 @@ Run Dash server
7676
7777
#Examples
7878
```jldoctest
79-
julia> app = dash("Test") do
79+
julia> app = dash() do
8080
html_div() do
8181
html_h1("Test Dashboard")
8282
end
@@ -111,10 +111,53 @@ function run_server(app::DashApp, host = HTTP.Sockets.localhost, port = 8050;
111111
dev_tools_silence_routes_logging = dev_tools_silence_routes_logging,
112112
dev_tools_prune_errors = dev_tools_prune_errors
113113
)
114-
handler = make_handler(app);
115-
@info string("Running on http://", host, ":", port)
116-
HTTP.serve(handler, host, port)
117-
end
114+
main_func = () -> begin
115+
ccall(:jl_exit_on_sigint, Cvoid, (Cint,), 0)
116+
handler = make_handler(app);
117+
try
118+
task = @async HTTP.serve(handler, host, port)
119+
@info string("Running on http://", host, ":", port)
120+
wait(task)
121+
catch e
122+
if e isa InterruptException
123+
@info "exited"
124+
else
125+
rethrow(e)
126+
end
127+
128+
end
129+
end
130+
start_server = () -> begin
131+
handler = make_handler(app);
132+
server = Sockets.listen(get_inetaddr(host, port))
133+
task = @async HTTP.serve(handler, host, port; server = server)
134+
@info string("Running on http://", host, ":", port)
135+
return (server, task)
136+
end
118137

138+
if get_devsetting(app, :hot_reload) && !is_hot_restart_available()
139+
@warn "Hot reloading is disabled for interactive sessions. Please run your app using julia from the command line to take advantage of this feature."
140+
end
141+
142+
if get_devsetting(app, :hot_reload) && is_hot_restart_available()
143+
hot_restart(start_server, check_interval = get_devsetting(app, :hot_reload_watch_interval))
144+
else
145+
(server, task) = start_server()
146+
try
147+
wait(task)
148+
println(task)
149+
catch e
150+
close(server)
151+
if e isa InterruptException
152+
println("finished")
153+
return
154+
else
155+
rethrow(e)
156+
end
157+
end
158+
end
159+
end
160+
get_inetaddr(host::String, port::Integer) = Sockets.InetAddr(parse(IPAddr, host), port)
161+
get_inetaddr(host::IPAddr, port::Integer) = Sockets.InetAddr(host, port)
119162

120163
end # module

src/app/callbacks.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Create a callback that updates the output by calling function `func`.
1919
# Examples
2020
2121
```julia
22-
app = dash("Test") do
22+
app = dash() do
2323
html_div() do
2424
dcc_input(id="graphTitle", value="Let's Dance!", type = "text"),
2525
dcc_input(id="graphTitle2", value="Let's Dance!", type = "text"),

src/app/dashapp.jl

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,16 @@ Dash.jl's internal representation of a Dash application.
2525
This `struct` is not intended to be called directly; developers should create their Dash application using the `dash` function instead.
2626
"""
2727
mutable struct DashApp
28-
name ::String
28+
root_path ::String
29+
is_interactive ::Bool
2930
config ::DashConfig
3031
index_string ::Union{String, Nothing}
32+
title ::String
3133
layout ::Union{Nothing, Component, Function}
3234
devtools ::DevTools
3335
callbacks ::Dict{Symbol, Callback}
3436

35-
DashApp(name, config, index_string) = new(name, config, index_string, nothing, DevTools(dash_env(Bool, "debug", false)), Dict{Symbol, Callback}())
37+
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}())
3638

3739
end
3840

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

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

@@ -51,6 +54,10 @@ function set_name!(app::DashApp, name)
5154
setfield!(app, :name, name)
5255
end
5356

57+
function set_title!(app::DashApp, title)
58+
setfield!(app, :title, title)
59+
end
60+
5461
get_name(app::DashApp) = app.name
5562

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

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

116+
get_assets_path(app::DashApp) = joinpath(app.root_path, get_setting(app, :assets_folder))
117+
109118
"""
110119
dash(name::String;
111120
external_stylesheets,
@@ -134,7 +143,6 @@ If a parameter can be set by an environment variable, that is listed as:
134143
Values provided here take precedence over environment variables.
135144
136145
# Arguments
137-
- `name::String` - The name of your application
138146
- `assets_folder::String` - a path, relative to the current working directory,
139147
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.
140148
@@ -212,7 +220,7 @@ If a parameter can be set by an environment variable, that is listed as:
212220
files and data served by HTTP.jl when supported by the client. Set to
213221
``false`` to disable compression completely.
214222
"""
215-
function dash(name::String = dash_env("dash_name", "");
223+
function dash(;
216224
external_stylesheets = ExternalSrcType[],
217225
external_scripts = ExternalSrcType[],
218226
url_base_pathname = dash_env("url_base_pathname"),
@@ -242,7 +250,7 @@ function dash(name::String = dash_env("dash_name", "");
242250
requests_pathname_prefix,
243251
routes_pathname_prefix
244252
)...,
245-
absolute_assets_path(assets_folder),
253+
assets_folder,
246254
lstrip(assets_url_path, '/'),
247255
assets_ignore,
248256
serve_locally,
@@ -254,6 +262,6 @@ function dash(name::String = dash_env("dash_name", "");
254262
show_undo_redo,
255263
compress
256264
)
257-
result = DashApp(name, config, index_string)
265+
result = DashApp(app_root_path(), isinteractive(), config, index_string)
258266
return result
259-
end
267+
end

src/app/devtools.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ struct DevTools
33
props_check::Bool
44
serve_dev_bundles::Bool
55
hot_reload::Bool
6-
hot_reload_interval::Float32
7-
hot_reload_watch_interval::Float32
6+
hot_reload_interval::Float64
7+
hot_reload_watch_interval::Float64
88
hot_reload_max_retry::Int
99
silence_routes_logging::Bool
1010
prune_errors::Bool

src/handler/handlers.jl

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ function process_dependencies(request::HTTP.Request, state::HandlerState)
1818
end
1919

2020
function process_index(request::HTTP.Request, state::HandlerState)
21+
get_cache(state).need_recache && rebuild_cache!(state)
2122
return HTTP.Response(
2223
200,
2324
["Content-Type" => "text/html"],
@@ -147,7 +148,7 @@ end
147148

148149
function process_assets(request::HTTP.Request, state::HandlerState; file_path::AbstractString)
149150
app = state.app
150-
filename = joinpath(get_setting(app, :assets_folder), file_path)
151+
filename = joinpath(get_assets_path(app), file_path)
151152

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

162163
end
163164

164-
const dash_router = HTTP.Router()
165+
function process_reload_hash(request::HTTP.Request, state::HandlerState)
166+
reload_tuple = (
167+
reloadHash = state.reload.hash,
168+
hard = state.reload.hard,
169+
packages = keys(state.cache.resources.files),
170+
files = state.reload.changed_assets
171+
)
172+
state.reload.hard = false
173+
state.reload.changed_assets = []
174+
return HTTP.Response(200, ["Content-Type" => "application/json"], body = JSON2.write(reload_tuple))
175+
176+
end
177+
178+
function start_reload_poll(state::HandlerState)
179+
folders = Set{String}()
180+
push!(folders, get_assets_path(state.app))
181+
push!(folders, state.registry.dash_renderer.path)
182+
for pkg in values(state.registry.components)
183+
push!(folders, pkg.path)
184+
end
185+
state.reload.task = @async poll_folders(folders; interval = get_devsetting(state.app, :hot_reload_watch_interval)) do file, ts, deleted
186+
state.reload.hard = true
187+
state.reload.hash = generate_hash()
188+
assets_path = get_assets_path(state.app)
189+
if startswith(file, assets_path)
190+
state.cache.need_recache = true
191+
rel_path = lstrip(
192+
replace(relpath(file, assets_path), '\\'=>'/'),
193+
'/'
194+
)
195+
push!(state.reload.changed_assets,
196+
ChangedAsset(
197+
asset_path(state.app, rel_path),
198+
trunc(Int, ts),
199+
endswith(file, "css")
200+
)
201+
)
202+
end
203+
204+
end
205+
end
165206

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

@@ -180,6 +221,7 @@ function make_handler(app::DashApp, registry::ResourcesRegistry; check_layout =
180221
router = Router()
181222
add_route!(process_layout, router, "$(prefix)_dash-layout")
182223
add_route!(process_dependencies, router, "$(prefix)_dash-dependencies")
224+
add_route!(process_reload_hash, router, "$(prefix)_reload-hash")
183225
add_route!(process_resource, router, "$(prefix)_dash-component-suites/<namespace>/<path>")
184226
add_route!(process_assets, router, "$(prefix)$(assets_url_path)/<file_path>")
185227
add_route!(process_callback, router, "POST", "$(prefix)_dash-update-component")
@@ -189,7 +231,12 @@ function make_handler(app::DashApp, registry::ResourcesRegistry; check_layout =
189231
handler = state_handler(router, state)
190232
get_setting(app, :compress) && (handler = compress_handler(handler))
191233

192-
HTTP.handle(handler, HTTP.Request("GET", prefix)) #For handler precompilation
234+
compile_request = HTTP.Request("GET", prefix)
235+
HTTP.setheader(compile_request, "Accept-Encoding" => "gzip")
236+
HTTP.handle(handler, compile_request) #For handler precompilation
237+
238+
get_devsetting(app, :hot_reload) && start_reload_poll(state)
239+
193240
return handler
194241
end
195242

src/handler/index_page.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ function index_page(app::DashApp, resources::ApplicationResources)
9999

100100
result = interpolate_string(app.index_string,
101101
metas = metas_html(app),
102-
title = app.name,
102+
title = app.title,
103103
favicon = favicon_html(app),
104104
css = css_html(app, resources),
105105
app_entry = app_entry_html(),

0 commit comments

Comments
 (0)