diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..45042aa
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,35 @@
+version: 2
+
+jobs:
+
+ test:
+ working_directory: /root/project/Dash
+
+ docker:
+ - image: julia:latest
+
+ steps:
+ - checkout
+
+ - run:
+ name: âšī¸ CI Context
+ command: |
+ echo "TRIGGERER: ${CIRCLE_USERNAME}"
+ echo "BUILD_NUMBER: ${CIRCLE_BUILD_NUM}"
+ echo "BUILD_URL: ${CIRCLE_BUILD_URL}"
+ echo "BRANCH: ${CIRCLE_BRANCH}"
+ echo "RUNNING JOB: ${CIRCLE_JOB}"
+ echo "JOB PARALLELISM: ${CIRCLE_NODE_TOTAL}"
+ echo "CIRCLE_REPOSITORY_URL: ${CIRCLE_REPOSITORY_URL}"
+ echo $CIRCLE_JOB > circlejob.txt
+
+ - run:
+ name: đ Unit tests
+ command: |
+ julia -e 'using Pkg; Pkg.update(); Pkg.add(PackageSpec(path=pwd())); Pkg.build("Dash"); Pkg.test("Dash", coverage=true);'
+
+workflows:
+ version: 2
+ build:
+ jobs:
+ - "test"
diff --git a/src/ComponentPackages.jl b/src/ComponentPackages.jl
index 24b1c37..6e53ee4 100644
--- a/src/ComponentPackages.jl
+++ b/src/ComponentPackages.jl
@@ -43,6 +43,17 @@ module ComponentPackages
)
end
+ function components_js_sources(prefix = ""; debug=false)
+ type = debug ? :dev : :prod
+ result = []
+
+ for p in values(_components_packages)
+ for script in p.scripts[type]
+ push!(result, "$(prefix)_dash-component-suites/$(p.package_name)/$(script)")
+ end
+ end
+ return result
+ end
diff --git a/src/Dash.jl b/src/Dash.jl
index 26024e3..962f19c 100644
--- a/src/Dash.jl
+++ b/src/Dash.jl
@@ -15,9 +15,11 @@ export dash, Component, Front, @use, <|, @callid_str, CallbackId, callback!,
run_server, PreventUpdate, no_update, @wildprop
ComponentPackages.@reg_components()
+include("utils.jl")
+include("config.jl")
include("app.jl")
+include("index_page.jl")
include("handlers.jl")
-include("utils.jl")
@doc """
@@ -91,4 +93,5 @@ function run_server(app::DashApp, host = HTTP.Sockets.localhost, port = 8080; de
HTTP.serve(handler, host, port)
end
+
end # module
\ No newline at end of file
diff --git a/src/app.jl b/src/app.jl
index b2381f1..5adf7ad 100644
--- a/src/app.jl
+++ b/src/app.jl
@@ -1,5 +1,25 @@
const IdProp = Tuple{Symbol, Symbol}
+const default_index = """
+
+
+ {%metas%}
+ {%title%}
+ {%favicon%}
+ {%css%}
+
+
+ {%app_entry%}
+
+
+"""
+
+
+
struct CallbackId
state ::Vector{IdProp}
input ::Vector{IdProp}
@@ -34,33 +54,43 @@ mutable struct Layout
component::Union{Nothing, Component}
end
+const ExternalSrcType = Union{String, Dict{String, String}}
+
+struct DashConfig
+ external_stylesheets ::Vector{ExternalSrcType}
+ external_scripts ::Vector{ExternalSrcType}
+ url_base_pathname ::Union{String, Nothing} #TODO This looks unused
+ requests_pathname_prefix ::String
+ routes_pathname_prefix ::String
+ assets_folder ::String
+ assets_url_path ::String
+ assets_ignore ::String
+ serve_locally ::Bool
+ suppress_callback_exceptions ::Bool
+ eager_loading ::Bool
+ meta_tags ::Vector{Dict{String, String}}
+ index_string ::Union{String, Nothing}
+ assets_external_path ::Union{String, Nothing}
+ include_assets_files ::Bool
+ show_undo_redo ::Bool
+end
+
"""
struct DashApp <: Any
-Representation of Dash application
+Representation of Dash application.
+
+Not meant to be constructed directly, use `dash` function instead.
"""
struct DashApp
name ::String
+ config ::DashConfig
layout ::Layout
callbacks ::Dict{Symbol, Callback}
- external_stylesheets ::Vector{String}
- external_scripts ::Vector{String}
- url_base_pathname ::String
- assets_folder ::String
- callable_components ::Dict{Symbol, Component}
- function DashApp(name::String;
- external_stylesheets ::Vector{String} = Vector{String}(),
- external_scripts ::Vector{String} = Vector{String}(),
- url_base_pathname="/",
- assets_folder::String = "assets")
-
- new(name, Layout(nothing), Dict{Symbol, Callback}(),
- external_stylesheets, external_scripts,
- url_base_pathname, assets_folder,
- Dict{Symbol, Component}()
-
- )
- end
+ callable_components ::Dict{Symbol, Component}
+
+ DashApp(name::String, config::DashConfig) = new(name, config, Layout(nothing), Dict{Symbol, Callback}(), Dict{Symbol, Component}())
+
end
function layout!(app::DashApp, component::Component)
@@ -78,59 +108,190 @@ function Base.getproperty(app::DashApp, name::Symbol)
name == :layout ? getlayout(app) : Base.getfield(app, name)
end
-
+
"""
- dash(name::String; external_stylesheets ::Vector{String} = Vector{String}(), url_base_pathname::String="/")
- dash(layout_maker::Function, name::String; external_stylesheets ::Vector{String} = Vector{String}(), url_base_pathname::String="/")
+ dash(name::String;
+ external_stylesheets,
+ external_scripts,
+ url_base_pathname,
+ requests_pathname_prefix,
+ routes_pathname_prefix,
+ assets_folder,
+ assets_url_path,
+ assets_ignore,
+ serve_locally,
+ suppress_callback_exceptions,
+ eager_loading ,
+ meta_tags,
+ index_string,
+ assets_external_path,
+ include_assets_files,
+ show_undo_redo
+ )
-Construct a dash app using callback for layout creation
+Construct a dash app
# Arguments
-- `layout_maker::Function` - function for layout creation. Must has signature ()::Component
-- `name::String` - Dashboard name
-- `external_stylesheets::Vector{String} = Vector{String}()` - vector of external css urls
-- `external_scripts::Vector{String} = Vector{String}()` - vector of external js scripts urls
-- `url_base_pathname::String="/"` - base url path for dashboard, default "/"
+- `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"`
+ for extra files to be used in the browser. Default ``'assets'``.
-# Examples
-```jldoctest
-julia> app = dash("Test") do
- html_div() do
- html_h1("Test Dashboard")
- end
-end
-```
-"""
+- `assets_url_path::String` - The local urls for assets will be:
+ ``requests_pathname_prefix * assets_url_path * "/" * asset_path``
+ where ``asset_path`` is the path to a file inside ``assets_folder``.
+ Default ``'assets'`.
+
+
+- `assets_ignore::String` - [IN DEVELOPMENT] A regex, as a string to pass to ``Regex``, for
+ assets to omit from immediate loading. Ignored files will still be
+ served if specifically requested. You cannot use this to prevent access
+ to sensitive files.
+ :type assets_ignore: string
+
+- `assets_external_path::String` - [IN DEVELOPMENT] an absolute URL from which to load assets.
+ Use with ``serve_locally=false``. Dash can still find js and css to
+ automatically load if you also keep local copies in your assets
+ folder that Dash can index, but external serving can improve
+ performance and reduce load on the Dash server.
+
+
+- `include_assets_files::Bool` - [IN DEVELOPMENT] Default ``true``, set to ``False`` to prevent
+ immediate loading of any assets. Assets will still be served if
+ specifically requested. You cannot use this to prevent access
+ to sensitive files.
+
+
+- `url_base_pathname::String`: A local URL prefix to use app-wide.
+ Default ``nothing``. Both `requests_pathname_prefix` and
+ `routes_pathname_prefix` default to `url_base_pathname`.
+
+
+- `requests_pathname_prefix::String`: A local URL prefix for file requests.
+ Defaults to `url_base_pathname`, and must end with
+ `routes_pathname_prefix`
+
+
+- `routes_pathname_prefix::String`: A local URL prefix for JSON requests.
+ Defaults to ``url_base_pathname``, and must start and end
+ with ``'/'``.
+
+- `serve_locally`: [IN DEVELOPMENT] If ``true`` (default), assets and dependencies
+ (Dash and Component js and css) will be served from local URLs.
+ If ``false`` we will use CDN links where available.
+
+- `meta_tags::Vector{Dict{String, String}}`: html tags to be added to the index page.
+ Each dict should have the attributes and values for one tag, eg:
+ ``Dict("name"=>"description", "content" => "My App")``
+
+- `index_string::String`: Override the standard Dash index page.
+ Must contain the correct insertion markers to interpolate various
+ content into it depending on the app config and components used.
+ See https://dash.plotly.com/external-resources for details.
+
+
+- `external_scripts::Vector`: Additional JS files to load with the page.
+ Each entry can be a String (the URL) or a Dict{String, String} with ``src`` (the URL)
+ and optionally other ``"""
- end, "\n")
-
- app_entry = """
-
-
- Loading...
-
-
- """
- config = (
- url_base_pathname = "$(app.url_base_pathname)",
- requests_pathname_prefix = "$(app.url_base_pathname)",
- ui = true,
- props_check = debug,
- show_undo_redo = false
- )
-
- scripts = ComponentPackages.components_js_include(app.url_base_pathname, debug = debug)
- """
-
-
- $(join(metas, ""))
- $(title)
- $(css)
-
-
- $(app_entry)
-
-
- """
-end
function dependencies_json(app::DashApp)
id_prop_named(p::IdProp) = (id = p[1], property = p[2])
@@ -108,8 +63,9 @@ function process_callback(app::DashApp, body::String)
end
function process_assets(app::DashApp, path)
- assets_path = "$(app.url_base_pathname)assets/"
- filename = joinpath(app.assets_folder, replace(path, assets_path=>""))
+ assets_path = "$(app.config.routes_pathname_prefix)" * strip(app.config.assets_url_path, '/') * "/"
+
+ filename = joinpath(app.config.assets_folder, replace(path, assets_path=>""))
try
return HTTP.Response(200, [], body = read(filename))
catch
@@ -119,22 +75,24 @@ end
function make_handler(app::DashApp; debug::Bool = false)
- function (req::HTTP.Request)
+ index_string::String = index_page(app, debug = debug)
+
+ return function (req::HTTP.Request)
uri = HTTP.URI(req.target)
- ComponentPackages.@register_js_sources(uri.path, app.url_base_pathname)
- if uri.path == "$(app.url_base_pathname)"
- return HTTP.Response(200, index_page(app, debug = debug))
+ ComponentPackages.@register_js_sources(uri.path, app.config.routes_pathname_prefix)
+ if uri.path == "$(app.config.routes_pathname_prefix)"
+ return HTTP.Response(200, index_string)
end
- if uri.path == "$(app.url_base_pathname)_dash-layout"
+ if uri.path == "$(app.config.routes_pathname_prefix)_dash-layout"
return HTTP.Response(200, ["Content-Type" => "application/json"], body = JSON2.write(app.layout))
end
- if uri.path == "$(app.url_base_pathname)_dash-dependencies"
+ if uri.path == "$(app.config.routes_pathname_prefix)_dash-dependencies"
return HTTP.Response(200, ["Content-Type" => "application/json"], body = dependencies_json(app))
end
- if startswith(uri.path, "$(app.url_base_pathname)assets/")
+ if startswith(uri.path, "$(app.config.routes_pathname_prefix)assets/")
return process_assets(app, uri.path)
end
- if uri.path == "$(app.url_base_pathname)_dash-update-component" && req.method == "POST"
+ if uri.path == "$(app.config.routes_pathname_prefix)_dash-update-component" && req.method == "POST"
try
return HTTP.Response(200, ["Content-Type" => "application/json"],
body = JSON2.write(
@@ -151,4 +109,5 @@ function make_handler(app::DashApp; debug::Bool = false)
end
return HTTP.Response(404)
end
+
end
\ No newline at end of file
diff --git a/src/index_page.jl b/src/index_page.jl
new file mode 100644
index 0000000..1336949
--- /dev/null
+++ b/src/index_page.jl
@@ -0,0 +1,72 @@
+make_css_tag(url::String) = """"""
+make_css_tag(dict::Dict{String, String}) = format_tag("link", dict, opened = true)
+
+make_script_tag(url::String) = """"""
+make_script_tag(dict::Dict{String, String}) = format_tag("script", dict)
+
+function metas_html(app::DashApp)
+ meta_tags = app.config.meta_tags
+ has_ie_compat = any(meta_tags) do tag
+ get(tag, "http-equiv", "") == "X-UA-Compatible"
+ end
+ has_charset = any(tag -> haskey(tag, "charset"), meta_tags)
+
+ result = String[]
+ !has_ie_compat && push!(result, "")
+ !has_charset && push!(result, "")
+
+ append!(result, format_tag.("meta", meta_tags, opened = true))
+ return join(result, "\n ")
+
+end
+
+css_html(app::DashApp) = join(map(make_css_tag, app.config.external_stylesheets), "\n ")
+
+app_entry_html() = """
+