Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,10 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- Dash for R now supports user-defined routes and redirects via the `app$server_route` and `app$redirect` methods. [#225](https://github.com/plotly/dashR/pull/225)

## [0.7.1] - 2020-07-30
### Fixed
- Fixes a minor bug in debug mode that prevented display of user-defined error messages when induced by invoking the `stop` function. [#220](https://github.com/plotly/dashR/pull/220).
Expand Down
162 changes: 160 additions & 2 deletions R/dash.R
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,12 +103,13 @@ Dash <- R6::R6Class(
self$config$external_stylesheets <- external_stylesheets
self$config$show_undo_redo <- show_undo_redo
self$config$update_title <- update_title

# ------------------------------------------------------------
# Initialize a route stack and register a static resource route
# ------------------------------------------------------------
router <- routr::RouteStack$new()

server$set_data("user-routes", list()) # placeholder for custom routes

# ensure that assets_folder is neither NULL nor character(0)
if (!(is.null(private$assets_folder)) & length(private$assets_folder) != 0) {
if (!(dir.exists(private$assets_folder)) && gsub("/+", "", assets_folder) != "assets") {
Expand DownExpand Up@@ -550,6 +551,130 @@ Dash <- R6::R6Class(
self$server <- server
},

# ------------------------------------------------------------------------
# methods to add custom server routes
# ------------------------------------------------------------------------
#' @description
#' Connect a URL to a custom server route
#' @details
#' `fiery`, the underlying web service framework upon which Dash for R is based,
#' supports custom routing through plugins. While convenient, the plugin API
#' providing this functionality is different from that provided by Flask, as
#' used by Dash for Python. This method wraps the pluggable routing of `routr`
#' routes in a manner that should feel slightly more idiomatic to Dash users.
#' ## Querying User-Defined Routes:
#' It is possible to retrieve the list of user-defined routes by invoking the
#' `get_data` method. For example, if your Dash application object is `app`, use
#' `app$server$get_data("user-routes")`.
#'
#' If you wish to erase all user-defined routes without instantiating a new Dash
#' application object, one option is to clear the routes manually:
#' `app$server$set_data("user-routes", list())`.
#' @param path Character. Represents a URL path comprised of strings, parameters
#' (strings prefixed with :), and wildcards (*), separated by /. Wildcards can
#' be used to match any path element, rather than restricting (as by default) to
#' a single path element. For example, it is possible to catch requests to multiple
#' subpaths using a wildcard. For more information, see \link{Route}.
#' @param handler Function. Adds a handler function to the specified method and path.
#' For more information, see \link{Route}.
#' @param methods Character. A string indicating the request method (in lower case,
#' e.g. 'get', 'put', etc.), as used by `reqres`. The default is `get`.
#' For more information, see \link{Route}.
#' @examples
#' library(dash)
#' app <- Dash$new()
#'
#' # A handler to redirect requests with `307` status code (temporary redirects);
#' # for permanent redirects (`301`), see the `redirect` method described below
#' #
#' # A simple single path-to-path redirect
#' app$server_route('/getting-started', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', '/layout')
#' TRUE
#' })
#'
#' # Example of a redirect with a wildcard for subpaths
#' app$server_route('/getting-started/*', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', '/layout')
#' TRUE
#' })
Comment thread
alexcjohnson marked this conversation as resolved.
#'
#' # Example of a parameterized redirect with wildcard for subpaths
#' app$server_route('/accounts/:user_id/*', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', paste0('/users/', keys$user_id))
#' TRUE
#' })
server_route = function(path = NULL, handler = NULL, methods = "get") {
if (is.null(path) || is.null(handler)) {
stop("The server_route method requires that a path and handler function are specified. Please ensure these arguments are non-missing.", call.=FALSE)
}

user_routes <- self$server$get_data("user-routes")

user_routes[[path]] <- list("path" = path,
"handler" = handler,
"methods" = methods)

self$server$set_data("user-routes", user_routes)
},

#' @description
#' Redirect a Dash application URL path
#' @details
#' This is a convenience method to simplify adding redirects
#' for your Dash application which automatically return a `301`
#' HTTP status code and direct the client to load an alternate URL.
#' @param old_path Character. Represents the URL path to redirect,
#' comprised of strings, parameters (strings prefixed with :), and
#' wildcards (*), separated by /. Wildcards can be used to match any
#' path element, rather than restricting (as by default) to a single
#' path element. For example, it is possible to catch requests to multiple
#' subpaths using a wildcard. For more information, see \link{Route}.
#' @param new_path Character or function. Same as `old_path`, but represents the
#' new path which the client should load instead. If a function is
#' provided instead of a string, it should have `keys` within its formals.
#' @param methods Character. A string indicating the request method
#' (in lower case, e.g. 'get', 'put', etc.), as used by `reqres`. The
#' default is `get`. For more information, see \link{Route}.
#' @examples
#' library(dash)
#' app <- Dash$new()
#'
#' # example of a simple single path-to-path redirect
#' app$redirect("/getting-started", "/layout")
#'
#' # example of a redirect using wildcards
#' app$redirect("/getting-started/*", "/layout/*")
#'
#' # example of a parameterized redirect using a function for new_path,
#' # which requires passing in keys to take advantage of subpaths within
#' # old_path that are preceded by a colon (e.g. :user_id):
#' app$redirect("/accounts/:user_id/*", function(keys) paste0("/users/", keys$user_id))
redirect = function(old_path = NULL, new_path = NULL, methods = "get") {
if (is.null(old_path) || is.null(new_path)) {
stop("The redirect method requires that both an old path and a new path are specified. Please ensure these arguments are non-missing.", call.=FALSE)
}

if (is.function(new_path)) {
handler <- function(request, response, keys, ...) {
response$status <- 301L
response$set_header('Location', new_path(keys))
TRUE
}
} else {
handler <- function(request, response, keys, ...) {
response$status <- 301L
response$set_header('Location', new_path)
TRUE
}
}

self$server_route(old_path, handler)
},

# ------------------------------------------------------------------------
# dash layout methods
# ------------------------------------------------------------------------
Expand DownExpand Up@@ -1029,6 +1154,39 @@ Dash <- R6::R6Class(

private$prune_errors <- getServerParam(dev_tools_prune_errors, "logical", TRUE)

# attach user-defined routes, if they exist
if (length(self$server$get_data("user-routes")) > 0) {

plugin <- list(
on_attach = function(server) {
user_routes <- server$get_data("user-routes")

# adding an additional route will fail if the
# route already exists, so remove user-routes
# if present and reload; user_routes will still
# have all the relevant routes in place anyhow
if (server$plugins$request_routr$has_route("user-routes"))
server$plugins$request_routr$remove_route("user-routes")

router <- server$plugins$request_routr

route <- routr::Route$new()

for (routing in user_routes) {
route$add_handler(method=routing$methods,
path=routing$path,
handler=routing$handler)
}

router$add_route(route, "user-routes")
},
name = "user_routes",
require = "request_routr"
)

self$server$attach(plugin, force = TRUE)
}

if(getAppPath() != FALSE) {
source_dir <- dirname(getAppPath())
private$app_root_modtime <- modtimeFromPath(source_dir, recursive = TRUE, asset_path = private$assets_folder)
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Add support for user-defined server routes by rpkyle · Pull Request #225 · plotly/dashR · GitHub
Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,10 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- Dash for R now supports user-defined routes and redirects via the `app$server_route` and `app$redirect` methods. [#225](https://github.com/plotly/dashR/pull/225)

## [0.7.1] - 2020-07-30
### Fixed
- Fixes a minor bug in debug mode that prevented display of user-defined error messages when induced by invoking the `stop` function. [#220](https://github.com/plotly/dashR/pull/220).
Expand Down
162 changes: 160 additions & 2 deletions R/dash.R
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,12 +103,13 @@ Dash <- R6::R6Class(
self$config$external_stylesheets <- external_stylesheets
self$config$show_undo_redo <- show_undo_redo
self$config$update_title <- update_title

# ------------------------------------------------------------
# Initialize a route stack and register a static resource route
# ------------------------------------------------------------
router <- routr::RouteStack$new()

server$set_data("user-routes", list()) # placeholder for custom routes

# ensure that assets_folder is neither NULL nor character(0)
if (!(is.null(private$assets_folder)) & length(private$assets_folder) != 0) {
if (!(dir.exists(private$assets_folder)) && gsub("/+", "", assets_folder) != "assets") {
Expand DownExpand Up@@ -550,6 +551,130 @@ Dash <- R6::R6Class(
self$server <- server
},

# ------------------------------------------------------------------------
# methods to add custom server routes
# ------------------------------------------------------------------------
#' @description
#' Connect a URL to a custom server route
#' @details
#' `fiery`, the underlying web service framework upon which Dash for R is based,
#' supports custom routing through plugins. While convenient, the plugin API
#' providing this functionality is different from that provided by Flask, as
#' used by Dash for Python. This method wraps the pluggable routing of `routr`
#' routes in a manner that should feel slightly more idiomatic to Dash users.
#' ## Querying User-Defined Routes:
#' It is possible to retrieve the list of user-defined routes by invoking the
#' `get_data` method. For example, if your Dash application object is `app`, use
#' `app$server$get_data("user-routes")`.
#'
#' If you wish to erase all user-defined routes without instantiating a new Dash
#' application object, one option is to clear the routes manually:
#' `app$server$set_data("user-routes", list())`.
#' @param path Character. Represents a URL path comprised of strings, parameters
#' (strings prefixed with :), and wildcards (*), separated by /. Wildcards can
#' be used to match any path element, rather than restricting (as by default) to
#' a single path element. For example, it is possible to catch requests to multiple
#' subpaths using a wildcard. For more information, see \link{Route}.
#' @param handler Function. Adds a handler function to the specified method and path.
#' For more information, see \link{Route}.
#' @param methods Character. A string indicating the request method (in lower case,
#' e.g. 'get', 'put', etc.), as used by `reqres`. The default is `get`.
#' For more information, see \link{Route}.
#' @examples
#' library(dash)
#' app <- Dash$new()
#'
#' # A handler to redirect requests with `307` status code (temporary redirects);
#' # for permanent redirects (`301`), see the `redirect` method described below
#' #
#' # A simple single path-to-path redirect
#' app$server_route('/getting-started', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', '/layout')
#' TRUE
#' })
#'
#' # Example of a redirect with a wildcard for subpaths
#' app$server_route('/getting-started/*', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', '/layout')
#' TRUE
#' })
Comment thread
alexcjohnson marked this conversation as resolved.
#'
#' # Example of a parameterized redirect with wildcard for subpaths
#' app$server_route('/accounts/:user_id/*', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', paste0('/users/', keys$user_id))
#' TRUE
#' })
server_route = function(path = NULL, handler = NULL, methods = "get") {
if (is.null(path) || is.null(handler)) {
stop("The server_route method requires that a path and handler function are specified. Please ensure these arguments are non-missing.", call.=FALSE)
}

user_routes <- self$server$get_data("user-routes")

user_routes[[path]] <- list("path" = path,
"handler" = handler,
"methods" = methods)

self$server$set_data("user-routes", user_routes)
},

#' @description
#' Redirect a Dash application URL path
#' @details
#' This is a convenience method to simplify adding redirects
#' for your Dash application which automatically return a `301`
#' HTTP status code and direct the client to load an alternate URL.
#' @param old_path Character. Represents the URL path to redirect,
#' comprised of strings, parameters (strings prefixed with :), and
#' wildcards (*), separated by /. Wildcards can be used to match any
#' path element, rather than restricting (as by default) to a single
#' path element. For example, it is possible to catch requests to multiple
#' subpaths using a wildcard. For more information, see \link{Route}.
#' @param new_path Character or function. Same as `old_path`, but represents the
#' new path which the client should load instead. If a function is
#' provided instead of a string, it should have `keys` within its formals.
#' @param methods Character. A string indicating the request method
#' (in lower case, e.g. 'get', 'put', etc.), as used by `reqres`. The
#' default is `get`. For more information, see \link{Route}.
#' @examples
#' library(dash)
#' app <- Dash$new()
#'
#' # example of a simple single path-to-path redirect
#' app$redirect("/getting-started", "/layout")
#'
#' # example of a redirect using wildcards
#' app$redirect("/getting-started/*", "/layout/*")
#'
#' # example of a parameterized redirect using a function for new_path,
#' # which requires passing in keys to take advantage of subpaths within
#' # old_path that are preceded by a colon (e.g. :user_id):
#' app$redirect("/accounts/:user_id/*", function(keys) paste0("/users/", keys$user_id))
redirect = function(old_path = NULL, new_path = NULL, methods = "get") {
if (is.null(old_path) || is.null(new_path)) {
stop("The redirect method requires that both an old path and a new path are specified. Please ensure these arguments are non-missing.", call.=FALSE)
}

if (is.function(new_path)) {
handler <- function(request, response, keys, ...) {
response$status <- 301L
response$set_header('Location', new_path(keys))
TRUE
}
} else {
handler <- function(request, response, keys, ...) {
response$status <- 301L
response$set_header('Location', new_path)
TRUE
}
}

self$server_route(old_path, handler)
},

# ------------------------------------------------------------------------
# dash layout methods
# ------------------------------------------------------------------------
Expand DownExpand Up@@ -1029,6 +1154,39 @@ Dash <- R6::R6Class(

private$prune_errors <- getServerParam(dev_tools_prune_errors, "logical", TRUE)

# attach user-defined routes, if they exist
if (length(self$server$get_data("user-routes")) > 0) {

plugin <- list(
on_attach = function(server) {
user_routes <- server$get_data("user-routes")

# adding an additional route will fail if the
# route already exists, so remove user-routes
# if present and reload; user_routes will still
# have all the relevant routes in place anyhow
if (server$plugins$request_routr$has_route("user-routes"))
server$plugins$request_routr$remove_route("user-routes")

router <- server$plugins$request_routr

route <- routr::Route$new()

for (routing in user_routes) {
route$add_handler(method=routing$methods,
path=routing$path,
handler=routing$handler)
}

router$add_route(route, "user-routes")
},
name = "user_routes",
require = "request_routr"
)

self$server$attach(plugin, force = TRUE)
}

if(getAppPath() != FALSE) {
source_dir <- dirname(getAppPath())
private$app_root_modtime <- modtimeFromPath(source_dir, recursive = TRUE, asset_path = private$assets_folder)
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add support for user-defined server routes by rpkyle · Pull Request #225 · plotly/dashR · GitHub
Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,10 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- Dash for R now supports user-defined routes and redirects via the `app$server_route` and `app$redirect` methods. [#225](https://github.com/plotly/dashR/pull/225)

## [0.7.1] - 2020-07-30
### Fixed
- Fixes a minor bug in debug mode that prevented display of user-defined error messages when induced by invoking the `stop` function. [#220](https://github.com/plotly/dashR/pull/220).
Expand Down
162 changes: 160 additions & 2 deletions R/dash.R
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,12 +103,13 @@ Dash <- R6::R6Class(
self$config$external_stylesheets <- external_stylesheets
self$config$show_undo_redo <- show_undo_redo
self$config$update_title <- update_title

# ------------------------------------------------------------
# Initialize a route stack and register a static resource route
# ------------------------------------------------------------
router <- routr::RouteStack$new()

server$set_data("user-routes", list()) # placeholder for custom routes

# ensure that assets_folder is neither NULL nor character(0)
if (!(is.null(private$assets_folder)) & length(private$assets_folder) != 0) {
if (!(dir.exists(private$assets_folder)) && gsub("/+", "", assets_folder) != "assets") {
Expand DownExpand Up@@ -550,6 +551,130 @@ Dash <- R6::R6Class(
self$server <- server
},

# ------------------------------------------------------------------------
# methods to add custom server routes
# ------------------------------------------------------------------------
#' @description
#' Connect a URL to a custom server route
#' @details
#' `fiery`, the underlying web service framework upon which Dash for R is based,
#' supports custom routing through plugins. While convenient, the plugin API
#' providing this functionality is different from that provided by Flask, as
#' used by Dash for Python. This method wraps the pluggable routing of `routr`
#' routes in a manner that should feel slightly more idiomatic to Dash users.
#' ## Querying User-Defined Routes:
#' It is possible to retrieve the list of user-defined routes by invoking the
#' `get_data` method. For example, if your Dash application object is `app`, use
#' `app$server$get_data("user-routes")`.
#'
#' If you wish to erase all user-defined routes without instantiating a new Dash
#' application object, one option is to clear the routes manually:
#' `app$server$set_data("user-routes", list())`.
#' @param path Character. Represents a URL path comprised of strings, parameters
#' (strings prefixed with :), and wildcards (*), separated by /. Wildcards can
#' be used to match any path element, rather than restricting (as by default) to
#' a single path element. For example, it is possible to catch requests to multiple
#' subpaths using a wildcard. For more information, see \link{Route}.
#' @param handler Function. Adds a handler function to the specified method and path.
#' For more information, see \link{Route}.
#' @param methods Character. A string indicating the request method (in lower case,
#' e.g. 'get', 'put', etc.), as used by `reqres`. The default is `get`.
#' For more information, see \link{Route}.
#' @examples
#' library(dash)
#' app <- Dash$new()
#'
#' # A handler to redirect requests with `307` status code (temporary redirects);
#' # for permanent redirects (`301`), see the `redirect` method described below
#' #
#' # A simple single path-to-path redirect
#' app$server_route('/getting-started', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', '/layout')
#' TRUE
#' })
#'
#' # Example of a redirect with a wildcard for subpaths
#' app$server_route('/getting-started/*', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', '/layout')
#' TRUE
#' })
Comment thread
alexcjohnson marked this conversation as resolved.
#'
#' # Example of a parameterized redirect with wildcard for subpaths
#' app$server_route('/accounts/:user_id/*', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', paste0('/users/', keys$user_id))
#' TRUE
#' })
server_route = function(path = NULL, handler = NULL, methods = "get") {
if (is.null(path) || is.null(handler)) {
stop("The server_route method requires that a path and handler function are specified. Please ensure these arguments are non-missing.", call.=FALSE)
}

user_routes <- self$server$get_data("user-routes")

user_routes[[path]] <- list("path" = path,
"handler" = handler,
"methods" = methods)

self$server$set_data("user-routes", user_routes)
},

#' @description
#' Redirect a Dash application URL path
#' @details
#' This is a convenience method to simplify adding redirects
#' for your Dash application which automatically return a `301`
#' HTTP status code and direct the client to load an alternate URL.
#' @param old_path Character. Represents the URL path to redirect,
#' comprised of strings, parameters (strings prefixed with :), and
#' wildcards (*), separated by /. Wildcards can be used to match any
#' path element, rather than restricting (as by default) to a single
#' path element. For example, it is possible to catch requests to multiple
#' subpaths using a wildcard. For more information, see \link{Route}.
#' @param new_path Character or function. Same as `old_path`, but represents the
#' new path which the client should load instead. If a function is
#' provided instead of a string, it should have `keys` within its formals.
#' @param methods Character. A string indicating the request method
#' (in lower case, e.g. 'get', 'put', etc.), as used by `reqres`. The
#' default is `get`. For more information, see \link{Route}.
#' @examples
#' library(dash)
#' app <- Dash$new()
#'
#' # example of a simple single path-to-path redirect
#' app$redirect("/getting-started", "/layout")
#'
#' # example of a redirect using wildcards
#' app$redirect("/getting-started/*", "/layout/*")
#'
#' # example of a parameterized redirect using a function for new_path,
#' # which requires passing in keys to take advantage of subpaths within
#' # old_path that are preceded by a colon (e.g. :user_id):
#' app$redirect("/accounts/:user_id/*", function(keys) paste0("/users/", keys$user_id))
redirect = function(old_path = NULL, new_path = NULL, methods = "get") {
if (is.null(old_path) || is.null(new_path)) {
stop("The redirect method requires that both an old path and a new path are specified. Please ensure these arguments are non-missing.", call.=FALSE)
}

if (is.function(new_path)) {
handler <- function(request, response, keys, ...) {
response$status <- 301L
response$set_header('Location', new_path(keys))
TRUE
}
} else {
handler <- function(request, response, keys, ...) {
response$status <- 301L
response$set_header('Location', new_path)
TRUE
}
}

self$server_route(old_path, handler)
},

# ------------------------------------------------------------------------
# dash layout methods
# ------------------------------------------------------------------------
Expand DownExpand Up@@ -1029,6 +1154,39 @@ Dash <- R6::R6Class(

private$prune_errors <- getServerParam(dev_tools_prune_errors, "logical", TRUE)

# attach user-defined routes, if they exist
if (length(self$server$get_data("user-routes")) > 0) {

plugin <- list(
on_attach = function(server) {
user_routes <- server$get_data("user-routes")

# adding an additional route will fail if the
# route already exists, so remove user-routes
# if present and reload; user_routes will still
# have all the relevant routes in place anyhow
if (server$plugins$request_routr$has_route("user-routes"))
server$plugins$request_routr$remove_route("user-routes")

router <- server$plugins$request_routr

route <- routr::Route$new()

for (routing in user_routes) {
route$add_handler(method=routing$methods,
path=routing$path,
handler=routing$handler)
}

router$add_route(route, "user-routes")
},
name = "user_routes",
require = "request_routr"
)

self$server$attach(plugin, force = TRUE)
}

if(getAppPath() != FALSE) {
source_dir <- dirname(getAppPath())
private$app_root_modtime <- modtimeFromPath(source_dir, recursive = TRUE, asset_path = private$assets_folder)
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add support for user-defined server routes by rpkyle · Pull Request #225 · plotly/dashR · GitHub
Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,10 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- Dash for R now supports user-defined routes and redirects via the `app$server_route` and `app$redirect` methods. [#225](https://github.com/plotly/dashR/pull/225)

## [0.7.1] - 2020-07-30
### Fixed
- Fixes a minor bug in debug mode that prevented display of user-defined error messages when induced by invoking the `stop` function. [#220](https://github.com/plotly/dashR/pull/220).
Expand Down
162 changes: 160 additions & 2 deletions R/dash.R
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,12 +103,13 @@ Dash <- R6::R6Class(
self$config$external_stylesheets <- external_stylesheets
self$config$show_undo_redo <- show_undo_redo
self$config$update_title <- update_title

# ------------------------------------------------------------
# Initialize a route stack and register a static resource route
# ------------------------------------------------------------
router <- routr::RouteStack$new()

server$set_data("user-routes", list()) # placeholder for custom routes

# ensure that assets_folder is neither NULL nor character(0)
if (!(is.null(private$assets_folder)) & length(private$assets_folder) != 0) {
if (!(dir.exists(private$assets_folder)) && gsub("/+", "", assets_folder) != "assets") {
Expand DownExpand Up@@ -550,6 +551,130 @@ Dash <- R6::R6Class(
self$server <- server
},

# ------------------------------------------------------------------------
# methods to add custom server routes
# ------------------------------------------------------------------------
#' @description
#' Connect a URL to a custom server route
#' @details
#' `fiery`, the underlying web service framework upon which Dash for R is based,
#' supports custom routing through plugins. While convenient, the plugin API
#' providing this functionality is different from that provided by Flask, as
#' used by Dash for Python. This method wraps the pluggable routing of `routr`
#' routes in a manner that should feel slightly more idiomatic to Dash users.
#' ## Querying User-Defined Routes:
#' It is possible to retrieve the list of user-defined routes by invoking the
#' `get_data` method. For example, if your Dash application object is `app`, use
#' `app$server$get_data("user-routes")`.
#'
#' If you wish to erase all user-defined routes without instantiating a new Dash
#' application object, one option is to clear the routes manually:
#' `app$server$set_data("user-routes", list())`.
#' @param path Character. Represents a URL path comprised of strings, parameters
#' (strings prefixed with :), and wildcards (*), separated by /. Wildcards can
#' be used to match any path element, rather than restricting (as by default) to
#' a single path element. For example, it is possible to catch requests to multiple
#' subpaths using a wildcard. For more information, see \link{Route}.
#' @param handler Function. Adds a handler function to the specified method and path.
#' For more information, see \link{Route}.
#' @param methods Character. A string indicating the request method (in lower case,
#' e.g. 'get', 'put', etc.), as used by `reqres`. The default is `get`.
#' For more information, see \link{Route}.
#' @examples
#' library(dash)
#' app <- Dash$new()
#'
#' # A handler to redirect requests with `307` status code (temporary redirects);
#' # for permanent redirects (`301`), see the `redirect` method described below
#' #
#' # A simple single path-to-path redirect
#' app$server_route('/getting-started', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', '/layout')
#' TRUE
#' })
#'
#' # Example of a redirect with a wildcard for subpaths
#' app$server_route('/getting-started/*', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', '/layout')
#' TRUE
#' })
Comment thread
alexcjohnson marked this conversation as resolved.
#'
#' # Example of a parameterized redirect with wildcard for subpaths
#' app$server_route('/accounts/:user_id/*', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', paste0('/users/', keys$user_id))
#' TRUE
#' })
server_route = function(path = NULL, handler = NULL, methods = "get") {
if (is.null(path) || is.null(handler)) {
stop("The server_route method requires that a path and handler function are specified. Please ensure these arguments are non-missing.", call.=FALSE)
}

user_routes <- self$server$get_data("user-routes")

user_routes[[path]] <- list("path" = path,
"handler" = handler,
"methods" = methods)

self$server$set_data("user-routes", user_routes)
},

#' @description
#' Redirect a Dash application URL path
#' @details
#' This is a convenience method to simplify adding redirects
#' for your Dash application which automatically return a `301`
#' HTTP status code and direct the client to load an alternate URL.
#' @param old_path Character. Represents the URL path to redirect,
#' comprised of strings, parameters (strings prefixed with :), and
#' wildcards (*), separated by /. Wildcards can be used to match any
#' path element, rather than restricting (as by default) to a single
#' path element. For example, it is possible to catch requests to multiple
#' subpaths using a wildcard. For more information, see \link{Route}.
#' @param new_path Character or function. Same as `old_path`, but represents the
#' new path which the client should load instead. If a function is
#' provided instead of a string, it should have `keys` within its formals.
#' @param methods Character. A string indicating the request method
#' (in lower case, e.g. 'get', 'put', etc.), as used by `reqres`. The
#' default is `get`. For more information, see \link{Route}.
#' @examples
#' library(dash)
#' app <- Dash$new()
#'
#' # example of a simple single path-to-path redirect
#' app$redirect("/getting-started", "/layout")
#'
#' # example of a redirect using wildcards
#' app$redirect("/getting-started/*", "/layout/*")
#'
#' # example of a parameterized redirect using a function for new_path,
#' # which requires passing in keys to take advantage of subpaths within
#' # old_path that are preceded by a colon (e.g. :user_id):
#' app$redirect("/accounts/:user_id/*", function(keys) paste0("/users/", keys$user_id))
redirect = function(old_path = NULL, new_path = NULL, methods = "get") {
if (is.null(old_path) || is.null(new_path)) {
stop("The redirect method requires that both an old path and a new path are specified. Please ensure these arguments are non-missing.", call.=FALSE)
}

if (is.function(new_path)) {
handler <- function(request, response, keys, ...) {
response$status <- 301L
response$set_header('Location', new_path(keys))
TRUE
}
} else {
handler <- function(request, response, keys, ...) {
response$status <- 301L
response$set_header('Location', new_path)
TRUE
}
}

self$server_route(old_path, handler)
},

# ------------------------------------------------------------------------
# dash layout methods
# ------------------------------------------------------------------------
Expand DownExpand Up@@ -1029,6 +1154,39 @@ Dash <- R6::R6Class(

private$prune_errors <- getServerParam(dev_tools_prune_errors, "logical", TRUE)

# attach user-defined routes, if they exist
if (length(self$server$get_data("user-routes")) > 0) {

plugin <- list(
on_attach = function(server) {
user_routes <- server$get_data("user-routes")

# adding an additional route will fail if the
# route already exists, so remove user-routes
# if present and reload; user_routes will still
# have all the relevant routes in place anyhow
if (server$plugins$request_routr$has_route("user-routes"))
server$plugins$request_routr$remove_route("user-routes")

router <- server$plugins$request_routr

route <- routr::Route$new()

for (routing in user_routes) {
route$add_handler(method=routing$methods,
path=routing$path,
handler=routing$handler)
}

router$add_route(route, "user-routes")
},
name = "user_routes",
require = "request_routr"
)

self$server$attach(plugin, force = TRUE)
}

if(getAppPath() != FALSE) {
source_dir <- dirname(getAppPath())
private$app_root_modtime <- modtimeFromPath(source_dir, recursive = TRUE, asset_path = private$assets_folder)
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Add support for user-defined server routes by rpkyle · Pull Request #225 · plotly/dashR · GitHub
Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,10 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- Dash for R now supports user-defined routes and redirects via the `app$server_route` and `app$redirect` methods. [#225](https://github.com/plotly/dashR/pull/225)

## [0.7.1] - 2020-07-30
### Fixed
- Fixes a minor bug in debug mode that prevented display of user-defined error messages when induced by invoking the `stop` function. [#220](https://github.com/plotly/dashR/pull/220).
Expand Down
162 changes: 160 additions & 2 deletions R/dash.R
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,12 +103,13 @@ Dash <- R6::R6Class(
self$config$external_stylesheets <- external_stylesheets
self$config$show_undo_redo <- show_undo_redo
self$config$update_title <- update_title

# ------------------------------------------------------------
# Initialize a route stack and register a static resource route
# ------------------------------------------------------------
router <- routr::RouteStack$new()

server$set_data("user-routes", list()) # placeholder for custom routes

# ensure that assets_folder is neither NULL nor character(0)
if (!(is.null(private$assets_folder)) & length(private$assets_folder) != 0) {
if (!(dir.exists(private$assets_folder)) && gsub("/+", "", assets_folder) != "assets") {
Expand DownExpand Up@@ -550,6 +551,130 @@ Dash <- R6::R6Class(
self$server <- server
},

# ------------------------------------------------------------------------
# methods to add custom server routes
# ------------------------------------------------------------------------
#' @description
#' Connect a URL to a custom server route
#' @details
#' `fiery`, the underlying web service framework upon which Dash for R is based,
#' supports custom routing through plugins. While convenient, the plugin API
#' providing this functionality is different from that provided by Flask, as
#' used by Dash for Python. This method wraps the pluggable routing of `routr`
#' routes in a manner that should feel slightly more idiomatic to Dash users.
#' ## Querying User-Defined Routes:
#' It is possible to retrieve the list of user-defined routes by invoking the
#' `get_data` method. For example, if your Dash application object is `app`, use
#' `app$server$get_data("user-routes")`.
#'
#' If you wish to erase all user-defined routes without instantiating a new Dash
#' application object, one option is to clear the routes manually:
#' `app$server$set_data("user-routes", list())`.
#' @param path Character. Represents a URL path comprised of strings, parameters
#' (strings prefixed with :), and wildcards (*), separated by /. Wildcards can
#' be used to match any path element, rather than restricting (as by default) to
#' a single path element. For example, it is possible to catch requests to multiple
#' subpaths using a wildcard. For more information, see \link{Route}.
#' @param handler Function. Adds a handler function to the specified method and path.
#' For more information, see \link{Route}.
#' @param methods Character. A string indicating the request method (in lower case,
#' e.g. 'get', 'put', etc.), as used by `reqres`. The default is `get`.
#' For more information, see \link{Route}.
#' @examples
#' library(dash)
#' app <- Dash$new()
#'
#' # A handler to redirect requests with `307` status code (temporary redirects);
#' # for permanent redirects (`301`), see the `redirect` method described below
#' #
#' # A simple single path-to-path redirect
#' app$server_route('/getting-started', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', '/layout')
#' TRUE
#' })
#'
#' # Example of a redirect with a wildcard for subpaths
#' app$server_route('/getting-started/*', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', '/layout')
#' TRUE
#' })
Comment thread
alexcjohnson marked this conversation as resolved.
#'
#' # Example of a parameterized redirect with wildcard for subpaths
#' app$server_route('/accounts/:user_id/*', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', paste0('/users/', keys$user_id))
#' TRUE
#' })
server_route = function(path = NULL, handler = NULL, methods = "get") {
if (is.null(path) || is.null(handler)) {
stop("The server_route method requires that a path and handler function are specified. Please ensure these arguments are non-missing.", call.=FALSE)
}

user_routes <- self$server$get_data("user-routes")

user_routes[[path]] <- list("path" = path,
"handler" = handler,
"methods" = methods)

self$server$set_data("user-routes", user_routes)
},

#' @description
#' Redirect a Dash application URL path
#' @details
#' This is a convenience method to simplify adding redirects
#' for your Dash application which automatically return a `301`
#' HTTP status code and direct the client to load an alternate URL.
#' @param old_path Character. Represents the URL path to redirect,
#' comprised of strings, parameters (strings prefixed with :), and
#' wildcards (*), separated by /. Wildcards can be used to match any
#' path element, rather than restricting (as by default) to a single
#' path element. For example, it is possible to catch requests to multiple
#' subpaths using a wildcard. For more information, see \link{Route}.
#' @param new_path Character or function. Same as `old_path`, but represents the
#' new path which the client should load instead. If a function is
#' provided instead of a string, it should have `keys` within its formals.
#' @param methods Character. A string indicating the request method
#' (in lower case, e.g. 'get', 'put', etc.), as used by `reqres`. The
#' default is `get`. For more information, see \link{Route}.
#' @examples
#' library(dash)
#' app <- Dash$new()
#'
#' # example of a simple single path-to-path redirect
#' app$redirect("/getting-started", "/layout")
#'
#' # example of a redirect using wildcards
#' app$redirect("/getting-started/*", "/layout/*")
#'
#' # example of a parameterized redirect using a function for new_path,
#' # which requires passing in keys to take advantage of subpaths within
#' # old_path that are preceded by a colon (e.g. :user_id):
#' app$redirect("/accounts/:user_id/*", function(keys) paste0("/users/", keys$user_id))
redirect = function(old_path = NULL, new_path = NULL, methods = "get") {
if (is.null(old_path) || is.null(new_path)) {
stop("The redirect method requires that both an old path and a new path are specified. Please ensure these arguments are non-missing.", call.=FALSE)
}

if (is.function(new_path)) {
handler <- function(request, response, keys, ...) {
response$status <- 301L
response$set_header('Location', new_path(keys))
TRUE
}
} else {
handler <- function(request, response, keys, ...) {
response$status <- 301L
response$set_header('Location', new_path)
TRUE
}
}

self$server_route(old_path, handler)
},

# ------------------------------------------------------------------------
# dash layout methods
# ------------------------------------------------------------------------
Expand DownExpand Up@@ -1029,6 +1154,39 @@ Dash <- R6::R6Class(

private$prune_errors <- getServerParam(dev_tools_prune_errors, "logical", TRUE)

# attach user-defined routes, if they exist
if (length(self$server$get_data("user-routes")) > 0) {

plugin <- list(
on_attach = function(server) {
user_routes <- server$get_data("user-routes")

# adding an additional route will fail if the
# route already exists, so remove user-routes
# if present and reload; user_routes will still
# have all the relevant routes in place anyhow
if (server$plugins$request_routr$has_route("user-routes"))
server$plugins$request_routr$remove_route("user-routes")

router <- server$plugins$request_routr

route <- routr::Route$new()

for (routing in user_routes) {
route$add_handler(method=routing$methods,
path=routing$path,
handler=routing$handler)
}

router$add_route(route, "user-routes")
},
name = "user_routes",
require = "request_routr"
)

self$server$attach(plugin, force = TRUE)
}

if(getAppPath() != FALSE) {
source_dir <- dirname(getAppPath())
private$app_root_modtime <- modtimeFromPath(source_dir, recursive = TRUE, asset_path = private$assets_folder)
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Add support for user-defined server routes by rpkyle · Pull Request #225 · plotly/dashR · GitHub
Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,10 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- Dash for R now supports user-defined routes and redirects via the `app$server_route` and `app$redirect` methods. [#225](https://github.com/plotly/dashR/pull/225)

## [0.7.1] - 2020-07-30
### Fixed
- Fixes a minor bug in debug mode that prevented display of user-defined error messages when induced by invoking the `stop` function. [#220](https://github.com/plotly/dashR/pull/220).
Expand Down
162 changes: 160 additions & 2 deletions R/dash.R
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,12 +103,13 @@ Dash <- R6::R6Class(
self$config$external_stylesheets <- external_stylesheets
self$config$show_undo_redo <- show_undo_redo
self$config$update_title <- update_title

# ------------------------------------------------------------
# Initialize a route stack and register a static resource route
# ------------------------------------------------------------
router <- routr::RouteStack$new()

server$set_data("user-routes", list()) # placeholder for custom routes

# ensure that assets_folder is neither NULL nor character(0)
if (!(is.null(private$assets_folder)) & length(private$assets_folder) != 0) {
if (!(dir.exists(private$assets_folder)) && gsub("/+", "", assets_folder) != "assets") {
Expand DownExpand Up@@ -550,6 +551,130 @@ Dash <- R6::R6Class(
self$server <- server
},

# ------------------------------------------------------------------------
# methods to add custom server routes
# ------------------------------------------------------------------------
#' @description
#' Connect a URL to a custom server route
#' @details
#' `fiery`, the underlying web service framework upon which Dash for R is based,
#' supports custom routing through plugins. While convenient, the plugin API
#' providing this functionality is different from that provided by Flask, as
#' used by Dash for Python. This method wraps the pluggable routing of `routr`
#' routes in a manner that should feel slightly more idiomatic to Dash users.
#' ## Querying User-Defined Routes:
#' It is possible to retrieve the list of user-defined routes by invoking the
#' `get_data` method. For example, if your Dash application object is `app`, use
#' `app$server$get_data("user-routes")`.
#'
#' If you wish to erase all user-defined routes without instantiating a new Dash
#' application object, one option is to clear the routes manually:
#' `app$server$set_data("user-routes", list())`.
#' @param path Character. Represents a URL path comprised of strings, parameters
#' (strings prefixed with :), and wildcards (*), separated by /. Wildcards can
#' be used to match any path element, rather than restricting (as by default) to
#' a single path element. For example, it is possible to catch requests to multiple
#' subpaths using a wildcard. For more information, see \link{Route}.
#' @param handler Function. Adds a handler function to the specified method and path.
#' For more information, see \link{Route}.
#' @param methods Character. A string indicating the request method (in lower case,
#' e.g. 'get', 'put', etc.), as used by `reqres`. The default is `get`.
#' For more information, see \link{Route}.
#' @examples
#' library(dash)
#' app <- Dash$new()
#'
#' # A handler to redirect requests with `307` status code (temporary redirects);
#' # for permanent redirects (`301`), see the `redirect` method described below
#' #
#' # A simple single path-to-path redirect
#' app$server_route('/getting-started', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', '/layout')
#' TRUE
#' })
#'
#' # Example of a redirect with a wildcard for subpaths
#' app$server_route('/getting-started/*', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', '/layout')
#' TRUE
#' })
Comment thread
alexcjohnson marked this conversation as resolved.
#'
#' # Example of a parameterized redirect with wildcard for subpaths
#' app$server_route('/accounts/:user_id/*', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', paste0('/users/', keys$user_id))
#' TRUE
#' })
server_route = function(path = NULL, handler = NULL, methods = "get") {
if (is.null(path) || is.null(handler)) {
stop("The server_route method requires that a path and handler function are specified. Please ensure these arguments are non-missing.", call.=FALSE)
}

user_routes <- self$server$get_data("user-routes")

user_routes[[path]] <- list("path" = path,
"handler" = handler,
"methods" = methods)

self$server$set_data("user-routes", user_routes)
},

#' @description
#' Redirect a Dash application URL path
#' @details
#' This is a convenience method to simplify adding redirects
#' for your Dash application which automatically return a `301`
#' HTTP status code and direct the client to load an alternate URL.
#' @param old_path Character. Represents the URL path to redirect,
#' comprised of strings, parameters (strings prefixed with :), and
#' wildcards (*), separated by /. Wildcards can be used to match any
#' path element, rather than restricting (as by default) to a single
#' path element. For example, it is possible to catch requests to multiple
#' subpaths using a wildcard. For more information, see \link{Route}.
#' @param new_path Character or function. Same as `old_path`, but represents the
#' new path which the client should load instead. If a function is
#' provided instead of a string, it should have `keys` within its formals.
#' @param methods Character. A string indicating the request method
#' (in lower case, e.g. 'get', 'put', etc.), as used by `reqres`. The
#' default is `get`. For more information, see \link{Route}.
#' @examples
#' library(dash)
#' app <- Dash$new()
#'
#' # example of a simple single path-to-path redirect
#' app$redirect("/getting-started", "/layout")
#'
#' # example of a redirect using wildcards
#' app$redirect("/getting-started/*", "/layout/*")
#'
#' # example of a parameterized redirect using a function for new_path,
#' # which requires passing in keys to take advantage of subpaths within
#' # old_path that are preceded by a colon (e.g. :user_id):
#' app$redirect("/accounts/:user_id/*", function(keys) paste0("/users/", keys$user_id))
redirect = function(old_path = NULL, new_path = NULL, methods = "get") {
if (is.null(old_path) || is.null(new_path)) {
stop("The redirect method requires that both an old path and a new path are specified. Please ensure these arguments are non-missing.", call.=FALSE)
}

if (is.function(new_path)) {
handler <- function(request, response, keys, ...) {
response$status <- 301L
response$set_header('Location', new_path(keys))
TRUE
}
} else {
handler <- function(request, response, keys, ...) {
response$status <- 301L
response$set_header('Location', new_path)
TRUE
}
}

self$server_route(old_path, handler)
},

# ------------------------------------------------------------------------
# dash layout methods
# ------------------------------------------------------------------------
Expand DownExpand Up@@ -1029,6 +1154,39 @@ Dash <- R6::R6Class(

private$prune_errors <- getServerParam(dev_tools_prune_errors, "logical", TRUE)

# attach user-defined routes, if they exist
if (length(self$server$get_data("user-routes")) > 0) {

plugin <- list(
on_attach = function(server) {
user_routes <- server$get_data("user-routes")

# adding an additional route will fail if the
# route already exists, so remove user-routes
# if present and reload; user_routes will still
# have all the relevant routes in place anyhow
if (server$plugins$request_routr$has_route("user-routes"))
server$plugins$request_routr$remove_route("user-routes")

router <- server$plugins$request_routr

route <- routr::Route$new()

for (routing in user_routes) {
route$add_handler(method=routing$methods,
path=routing$path,
handler=routing$handler)
}

router$add_route(route, "user-routes")
},
name = "user_routes",
require = "request_routr"
)

self$server$attach(plugin, force = TRUE)
}

if(getAppPath() != FALSE) {
source_dir <- dirname(getAppPath())
private$app_root_modtime <- modtimeFromPath(source_dir, recursive = TRUE, asset_path = private$assets_folder)
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Add support for user-defined server routes by rpkyle · Pull Request #225 · plotly/dashR · GitHub
Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,10 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
### Added
- Dash for R now supports user-defined routes and redirects via the `app$server_route` and `app$redirect` methods. [#225](https://github.com/plotly/dashR/pull/225)

## [0.7.1] - 2020-07-30
### Fixed
- Fixes a minor bug in debug mode that prevented display of user-defined error messages when induced by invoking the `stop` function. [#220](https://github.com/plotly/dashR/pull/220).
Expand Down
162 changes: 160 additions & 2 deletions R/dash.R
Original file line numberDiff line numberDiff line change
Expand Up@@ -103,12 +103,13 @@ Dash <- R6::R6Class(
self$config$external_stylesheets <- external_stylesheets
self$config$show_undo_redo <- show_undo_redo
self$config$update_title <- update_title

# ------------------------------------------------------------
# Initialize a route stack and register a static resource route
# ------------------------------------------------------------
router <- routr::RouteStack$new()

server$set_data("user-routes", list()) # placeholder for custom routes

# ensure that assets_folder is neither NULL nor character(0)
if (!(is.null(private$assets_folder)) & length(private$assets_folder) != 0) {
if (!(dir.exists(private$assets_folder)) && gsub("/+", "", assets_folder) != "assets") {
Expand DownExpand Up@@ -550,6 +551,130 @@ Dash <- R6::R6Class(
self$server <- server
},

# ------------------------------------------------------------------------
# methods to add custom server routes
# ------------------------------------------------------------------------
#' @description
#' Connect a URL to a custom server route
#' @details
#' `fiery`, the underlying web service framework upon which Dash for R is based,
#' supports custom routing through plugins. While convenient, the plugin API
#' providing this functionality is different from that provided by Flask, as
#' used by Dash for Python. This method wraps the pluggable routing of `routr`
#' routes in a manner that should feel slightly more idiomatic to Dash users.
#' ## Querying User-Defined Routes:
#' It is possible to retrieve the list of user-defined routes by invoking the
#' `get_data` method. For example, if your Dash application object is `app`, use
#' `app$server$get_data("user-routes")`.
#'
#' If you wish to erase all user-defined routes without instantiating a new Dash
#' application object, one option is to clear the routes manually:
#' `app$server$set_data("user-routes", list())`.
#' @param path Character. Represents a URL path comprised of strings, parameters
#' (strings prefixed with :), and wildcards (*), separated by /. Wildcards can
#' be used to match any path element, rather than restricting (as by default) to
#' a single path element. For example, it is possible to catch requests to multiple
#' subpaths using a wildcard. For more information, see \link{Route}.
#' @param handler Function. Adds a handler function to the specified method and path.
#' For more information, see \link{Route}.
#' @param methods Character. A string indicating the request method (in lower case,
#' e.g. 'get', 'put', etc.), as used by `reqres`. The default is `get`.
#' For more information, see \link{Route}.
#' @examples
#' library(dash)
#' app <- Dash$new()
#'
#' # A handler to redirect requests with `307` status code (temporary redirects);
#' # for permanent redirects (`301`), see the `redirect` method described below
#' #
#' # A simple single path-to-path redirect
#' app$server_route('/getting-started', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', '/layout')
#' TRUE
#' })
#'
#' # Example of a redirect with a wildcard for subpaths
#' app$server_route('/getting-started/*', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', '/layout')
#' TRUE
#' })
Comment thread
alexcjohnson marked this conversation as resolved.
#'
#' # Example of a parameterized redirect with wildcard for subpaths
#' app$server_route('/accounts/:user_id/*', function(request, response, keys, ...) {
#' response$status <- 307L
#' response$set_header('Location', paste0('/users/', keys$user_id))
#' TRUE
#' })
server_route = function(path = NULL, handler = NULL, methods = "get") {
if (is.null(path) || is.null(handler)) {
stop("The server_route method requires that a path and handler function are specified. Please ensure these arguments are non-missing.", call.=FALSE)
}

user_routes <- self$server$get_data("user-routes")

user_routes[[path]] <- list("path" = path,
"handler" = handler,
"methods" = methods)

self$server$set_data("user-routes", user_routes)
},

#' @description
#' Redirect a Dash application URL path
#' @details
#' This is a convenience method to simplify adding redirects
#' for your Dash application which automatically return a `301`
#' HTTP status code and direct the client to load an alternate URL.
#' @param old_path Character. Represents the URL path to redirect,
#' comprised of strings, parameters (strings prefixed with :), and
#' wildcards (*), separated by /. Wildcards can be used to match any
#' path element, rather than restricting (as by default) to a single
#' path element. For example, it is possible to catch requests to multiple
#' subpaths using a wildcard. For more information, see \link{Route}.
#' @param new_path Character or function. Same as `old_path`, but represents the
#' new path which the client should load instead. If a function is
#' provided instead of a string, it should have `keys` within its formals.
#' @param methods Character. A string indicating the request method
#' (in lower case, e.g. 'get', 'put', etc.), as used by `reqres`. The
#' default is `get`. For more information, see \link{Route}.
#' @examples
#' library(dash)
#' app <- Dash$new()
#'
#' # example of a simple single path-to-path redirect
#' app$redirect("/getting-started", "/layout")
#'
#' # example of a redirect using wildcards
#' app$redirect("/getting-started/*", "/layout/*")
#'
#' # example of a parameterized redirect using a function for new_path,
#' # which requires passing in keys to take advantage of subpaths within
#' # old_path that are preceded by a colon (e.g. :user_id):
#' app$redirect("/accounts/:user_id/*", function(keys) paste0("/users/", keys$user_id))
redirect = function(old_path = NULL, new_path = NULL, methods = "get") {
if (is.null(old_path) || is.null(new_path)) {
stop("The redirect method requires that both an old path and a new path are specified. Please ensure these arguments are non-missing.", call.=FALSE)
}

if (is.function(new_path)) {
handler <- function(request, response, keys, ...) {
response$status <- 301L
response$set_header('Location', new_path(keys))
TRUE
}
} else {
handler <- function(request, response, keys, ...) {
response$status <- 301L
response$set_header('Location', new_path)
TRUE
}
}

self$server_route(old_path, handler)
},

# ------------------------------------------------------------------------
# dash layout methods
# ------------------------------------------------------------------------
Expand DownExpand Up@@ -1029,6 +1154,39 @@ Dash <- R6::R6Class(

private$prune_errors <- getServerParam(dev_tools_prune_errors, "logical", TRUE)

# attach user-defined routes, if they exist
if (length(self$server$get_data("user-routes")) > 0) {

plugin <- list(
on_attach = function(server) {
user_routes <- server$get_data("user-routes")

# adding an additional route will fail if the
# route already exists, so remove user-routes
# if present and reload; user_routes will still
# have all the relevant routes in place anyhow
if (server$plugins$request_routr$has_route("user-routes"))
server$plugins$request_routr$remove_route("user-routes")

router <- server$plugins$request_routr

route <- routr::Route$new()

for (routing in user_routes) {
route$add_handler(method=routing$methods,
path=routing$path,
handler=routing$handler)
}

router$add_route(route, "user-routes")
},
name = "user_routes",
require = "request_routr"
)

self$server$attach(plugin, force = TRUE)
}

if(getAppPath() != FALSE) {
source_dir <- dirname(getAppPath())
private$app_root_modtime <- modtimeFromPath(source_dir, recursive = TRUE, asset_path = private$assets_folder)
Expand Down
Loading