diff --git a/DESCRIPTION b/DESCRIPTION index adb80a01d..f88471cac 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -49,6 +49,7 @@ Imports: withr (>= 2.1.2) Suggests: aws.s3 (>= 0.3.21), + bs4Dash(>= 0.5.0), clustermq (>= 0.8.9), curl (>= 4.3), dplyr (>= 1.0.0), @@ -60,6 +61,8 @@ Suggests: pkgload (>= 1.1.0), qs (>= 0.23.2), rstudioapi (>= 0.11), + shiny (>= 1.5.0), + shinycssloaders (>= 1.0.0), testthat (>= 3.0.0), torch (>= 0.1.0), usethis (>= 1.6.3), diff --git a/NAMESPACE b/NAMESPACE index b7b7efe71..b8f26ff87 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -255,6 +255,9 @@ export(tar_test) export(tar_traceback) export(tar_validate) export(tar_visnetwork) +export(tar_watch) +export(tar_watch_server) +export(tar_watch_ui) export(tar_workspace) export(tar_workspaces) export(target_run_worker) diff --git a/R/tar_watch.R b/R/tar_watch.R new file mode 100644 index 000000000..b19b13323 --- /dev/null +++ b/R/tar_watch.R @@ -0,0 +1,238 @@ +# nocov start # Tested in tests/interactive/test-tar_watch.R +#' @title Shiny app to watch the dependency graph. +#' @export +#' @description Calls [tar_visnetwork()] every few seconds. +#' @details The controls of the app are in the left panel. +#' The `seconds` control is the number of seconds between +#' refreshes of the graph, and the other settings match +#' the arguments of [`tar_visnetwork()`]. +#' @return A Shiny app. +#' @inheritParams tar_watch_ui +#' @examples +#' if (FALSE) { # Only run interactively. +#' tar_dir({ +#' tar_script({ +#' sleep_run <- function(...) { +#' Sys.sleep(10) +#' } +#' tar_pipeline( +#' tar_target(settings, sleep_run()), +#' tar_target(data1, sleep_run(settings)), +#' tar_target(data2, sleep_run(settings)), +#' tar_target(data3, sleep_run(settings)), +#' tar_target(model1, sleep_run(data1)), +#' tar_target(model2, sleep_run(data2)), +#' tar_target(model3, sleep_run(data3)), +#' tar_target(figure1, sleep_run(model1)), +#' tar_target(figure2, sleep_run(model2)), +#' tar_target(figure3, sleep_run(model3)), +#' tar_target(conclusions, sleep_run(c(figure1, figure2, figure3))) +#' ) +#' }) +#' px <- tar_make(callr_function = callr::r_bg) +#' tar_watch(seconds = 10, outdated = FALSE, targets_only = TRUE) +#' }) +#' } +tar_watch <- function( + seconds = 5, + seconds_min = 1, + seconds_max = 100, + seconds_step = 1, + targets_only = FALSE, + outdated = TRUE, + label = NULL, + level_separation = 150, + height = "700px" +) { + assert_package("bs4Dash") + assert_package("shiny") + assert_package("shinycssloaders") + assert_package("visNetwork") + assert_target_script() + assert_dbl(seconds, "seconds must be numeric.") + assert_dbl(seconds_min, "seconds_min must be numeric.") + assert_dbl(seconds_max, "seconds_max must be numeric.") + assert_dbl(seconds_step, "seconds_step must be numeric.") + assert_scalar(seconds, "seconds must have length 1.") + assert_scalar(seconds_min, "seconds_min must have length 1.") + assert_scalar(seconds_max, "seconds_max must have length 1.") + assert_scalar(seconds_step, "seconds_step must have length 1.") + seconds_min <- min(seconds_min, seconds) + seconds_max <- max(seconds_max, seconds) + seconds_step <- min(seconds_step, seconds_max) + ui <- tar_watch_app_ui( + seconds = seconds, + seconds_min = seconds_min, + seconds_max = seconds_max, + seconds_step = seconds_step, + targets_only = targets_only, + outdated = outdated, + label_tar_visnetwork = label, + level_separation = level_separation, + height = height + ) + server <- function(input, output, session) { + tar_watch_server("tar_watch_id", height = height) + } + shiny::shinyApp(ui = ui, server = server) +} + +tar_watch_app_ui <- function( + seconds, + seconds_min, + seconds_max, + seconds_step, + targets_only, + outdated, + label_tar_visnetwork, + level_separation, + height +) { + body <- bs4Dash::bs4DashBody( + tar_watch_ui( + id = "tar_watch_id", + label = "tar_watch_label", + seconds = seconds, + seconds_min = seconds_min, + seconds_max = seconds_max, + seconds_step = seconds_step, + targets_only = targets_only, + outdated = outdated, + label_tar_visnetwork = label_tar_visnetwork, + level_separation = level_separation, + height = height + ) + ) + bs4Dash::bs4DashPage( + title = "", + body = body, + navbar = bs4Dash::bs4DashNavbar(controlbarIcon = NULL), + sidebar = bs4Dash::bs4DashSidebar(disable = TRUE) + ) +} + +#' @title Shiny module UI for tar_watch() +#' @export +#' @description Use `tar_watch_ui()` and [tar_watch_server()] +#' to include [tar_watch()] as a Shiny module in an app. +#' @examples +#' str(tar_watch_ui("my_id")) +#' @return A Shiny module UI. +#' @inheritParams shiny::moduleServer +#' @inheritParams tar_watch_server +#' @inheritParams tar_visnetwork +#' @param label Label for the module. +#' @param seconds Numeric of length 1, +#' default number of seconds between refreshes of the graph. +#' Can be changed in the app controls. +#' @param seconds_min Numeric of length 1, lower bound of `seconds` +#' in the app controls. +#' @param seconds_max Numeric of length 1, upper bound of `seconds` +#' in the app controls. +#' @param seconds_step Numeric of length 1, step size of `seconds` +#' in the app controls. +#' @param label_tar_visnetwork Character vector, `label` argument to +#' [tar_visnetwork()]. +tar_watch_ui <- function( + id, + label = "tar_watch_label", + seconds = 5, + seconds_min = 1, + seconds_max = 60, + seconds_step = 1, + targets_only = FALSE, + outdated = TRUE, + label_tar_visnetwork = NULL, + level_separation = 150, + height = "700px" +) { + ns <- shiny::NS(id) + shiny::fluidRow( + shiny::column( + width = 4, + bs4Dash::bs4Card( + inputID = ns("control"), + title = "Control", + status = "primary", + closable = FALSE, + width = 12, + shiny::sliderInput( + ns("seconds"), + "seconds", + value = seconds, + min = seconds_min, + max = seconds_max, + step = seconds_step + ), + shiny::selectInput( + ns("targets_only"), + "targets_only", + choices = c("TRUE", "FALSE"), + selected = as.character(targets_only) + ), + shiny::selectInput( + ns("outdated"), + "outdated", + choices = c("TRUE", "FALSE"), + selected = as.character(outdated) + ), + shiny::selectInput( + ns("label"), + "label", + choices = c("time", "size", "branches"), + selected = as.character(label_tar_visnetwork), + multiple = TRUE + ), + shiny::sliderInput( + ns("level_separation"), + "level_separation", + value = as.numeric(level_separation), + min = 0, + max = 1000, + step = 10 + ) + ) + ), + shiny::column( + width = 8, + bs4Dash::bs4Card( + inputID = ns("graph"), + title = "Graph", + status = "primary", + closable = FALSE, + width = 12, + shinycssloaders::withSpinner( + visNetwork::visNetworkOutput(ns("graph"), height = height) + ) + ) + ) + ) +} + +#' @title Shiny module server for tar_watch() +#' @export +#' @description Use [tar_watch_ui()] and `tar_watch_server()` +#' to include [tar_watch()] as a Shiny module in an app. +#' @return A Shiny module server. +#' @inheritParams shiny::moduleServer +#' @param height Character of length 1, +#' height of the `visNetwork` widget. +#' @examples +#' # tar_watch_server("my_id") # Only call inside an app. +tar_watch_server <- function(id, height = "700px") { + shiny::moduleServer( + id, + function(input, output, session) { + output$graph <- visNetwork::renderVisNetwork({ + shiny::invalidateLater(millis = 1000 * as.numeric(input$seconds)) + tar_visnetwork( + targets_only = as.logical(input$targets_only), + outdated = as.logical(input$outdated), + label = as.character(input$label), + level_separation = as.numeric(input$level_separation) + ) + }) + } + ) +} +# nocov end diff --git a/_pkgdown.yml b/_pkgdown.yml index 2739414e5..d24389ddc 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -51,6 +51,9 @@ reference: - '`tar_glimpse`' - '`tar_network`' - '`tar_visnetwork`' + - '`tar_watch`' + - '`tar_watch_server`' + - '`tar_watch_ui`' - title: Inspect contents: - '`tar_deps`' diff --git a/inst/WORDLIST b/inst/WORDLIST index 792b8ee68..65d423e46 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -286,6 +286,7 @@ toolkits traceback txtq UD +UI un unexportable unhandled diff --git a/man/tar_watch.Rd b/man/tar_watch.Rd new file mode 100644 index 000000000..47d25ea9d --- /dev/null +++ b/man/tar_watch.Rd @@ -0,0 +1,93 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tar_watch.R +\name{tar_watch} +\alias{tar_watch} +\title{Shiny app to watch the dependency graph.} +\usage{ +tar_watch( + seconds = 5, + seconds_min = 1, + seconds_max = 100, + seconds_step = 1, + targets_only = FALSE, + outdated = TRUE, + label = NULL, + level_separation = 150, + height = "700px" +) +} +\arguments{ +\item{seconds}{Numeric of length 1, +default number of seconds between refreshes of the graph. +Can be changed in the app controls.} + +\item{seconds_min}{Numeric of length 1, lower bound of \code{seconds} +in the app controls.} + +\item{seconds_max}{Numeric of length 1, upper bound of \code{seconds} +in the app controls.} + +\item{seconds_step}{Numeric of length 1, step size of \code{seconds} +in the app controls.} + +\item{targets_only}{Logical, whether to restrict the output to just targets +(\code{FALSE}) or to also include imported global functions and objects.} + +\item{outdated}{Logical, whether to show colors to distinguish outdated +targets from up-to-date targets. (Global functions and objects +still show these colors.) Looking for outdated targets +takes a lot of time for large pipelines with lots of branches, +and setting \code{outdated} to \code{FALSE} is a nice way to speed up the graph +if you only want to see dependency relationships and build progress.} + +\item{label}{Label for the module.} + +\item{level_separation}{Numeric of length 1, +\code{levelSeparation} argument of \code{visNetwork::visHierarchicalLayout()}. +Controls the distance between hierarchical levels. +Consider changing the value if the aspect ratio of the graph +is far from 1. If \code{level_separation} is \code{NULL}, +the \code{levelSeparation} argument of \code{visHierarchicalLayout()} +defaults to \code{150}.} + +\item{height}{Character of length 1, +height of the \code{visNetwork} widget.} +} +\value{ +A Shiny app. +} +\description{ +Calls \code{\link[=tar_visnetwork]{tar_visnetwork()}} every few seconds. +} +\details{ +The controls of the app are in the left panel. +The \code{seconds} control is the number of seconds between +refreshes of the graph, and the other settings match +the arguments of \code{\link[=tar_visnetwork]{tar_visnetwork()}}. +} +\examples{ +if (FALSE) { # Only run interactively. +tar_dir({ +tar_script({ + sleep_run <- function(...) { + Sys.sleep(10) + } + tar_pipeline( + tar_target(settings, sleep_run()), + tar_target(data1, sleep_run(settings)), + tar_target(data2, sleep_run(settings)), + tar_target(data3, sleep_run(settings)), + tar_target(model1, sleep_run(data1)), + tar_target(model2, sleep_run(data2)), + tar_target(model3, sleep_run(data3)), + tar_target(figure1, sleep_run(model1)), + tar_target(figure2, sleep_run(model2)), + tar_target(figure3, sleep_run(model3)), + tar_target(conclusions, sleep_run(c(figure1, figure2, figure3))) + ) +}) +px <- tar_make(callr_function = callr::r_bg) +tar_watch(seconds = 10, outdated = FALSE, targets_only = TRUE) +}) +} +} diff --git a/man/tar_watch_server.Rd b/man/tar_watch_server.Rd new file mode 100644 index 000000000..fa34c24fd --- /dev/null +++ b/man/tar_watch_server.Rd @@ -0,0 +1,25 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tar_watch.R +\name{tar_watch_server} +\alias{tar_watch_server} +\title{Shiny module server for tar_watch()} +\usage{ +tar_watch_server(id, height = "700px") +} +\arguments{ +\item{id}{An ID string that corresponds with the ID used to call the module's +UI function.} + +\item{height}{Character of length 1, +height of the \code{visNetwork} widget.} +} +\value{ +A Shiny module server. +} +\description{ +Use \code{\link[=tar_watch_ui]{tar_watch_ui()}} and \code{tar_watch_server()} +to include \code{\link[=tar_watch]{tar_watch()}} as a Shiny module in an app. +} +\examples{ +# tar_watch_server("my_id") # Only call inside an app. +} diff --git a/man/tar_watch_ui.Rd b/man/tar_watch_ui.Rd new file mode 100644 index 000000000..d66ae1ac8 --- /dev/null +++ b/man/tar_watch_ui.Rd @@ -0,0 +1,73 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/tar_watch.R +\name{tar_watch_ui} +\alias{tar_watch_ui} +\title{Shiny module UI for tar_watch()} +\usage{ +tar_watch_ui( + id, + label = "tar_watch_label", + seconds = 5, + seconds_min = 1, + seconds_max = 60, + seconds_step = 1, + targets_only = FALSE, + outdated = TRUE, + label_tar_visnetwork = NULL, + level_separation = 150, + height = "700px" +) +} +\arguments{ +\item{id}{An ID string that corresponds with the ID used to call the module's +UI function.} + +\item{label}{Label for the module.} + +\item{seconds}{Numeric of length 1, +default number of seconds between refreshes of the graph. +Can be changed in the app controls.} + +\item{seconds_min}{Numeric of length 1, lower bound of \code{seconds} +in the app controls.} + +\item{seconds_max}{Numeric of length 1, upper bound of \code{seconds} +in the app controls.} + +\item{seconds_step}{Numeric of length 1, step size of \code{seconds} +in the app controls.} + +\item{targets_only}{Logical, whether to restrict the output to just targets +(\code{FALSE}) or to also include imported global functions and objects.} + +\item{outdated}{Logical, whether to show colors to distinguish outdated +targets from up-to-date targets. (Global functions and objects +still show these colors.) Looking for outdated targets +takes a lot of time for large pipelines with lots of branches, +and setting \code{outdated} to \code{FALSE} is a nice way to speed up the graph +if you only want to see dependency relationships and build progress.} + +\item{label_tar_visnetwork}{Character vector, \code{label} argument to +\code{\link[=tar_visnetwork]{tar_visnetwork()}}.} + +\item{level_separation}{Numeric of length 1, +\code{levelSeparation} argument of \code{visNetwork::visHierarchicalLayout()}. +Controls the distance between hierarchical levels. +Consider changing the value if the aspect ratio of the graph +is far from 1. If \code{level_separation} is \code{NULL}, +the \code{levelSeparation} argument of \code{visHierarchicalLayout()} +defaults to \code{150}.} + +\item{height}{Character of length 1, +height of the \code{visNetwork} widget.} +} +\value{ +A Shiny module UI. +} +\description{ +Use \code{tar_watch_ui()} and \code{\link[=tar_watch_server]{tar_watch_server()}} +to include \code{\link[=tar_watch]{tar_watch()}} as a Shiny module in an app. +} +\examples{ +str(tar_watch_ui("my_id")) +} diff --git a/tests/interactive/test-tar_watch.R b/tests/interactive/test-tar_watch.R new file mode 100644 index 000000000..a9e861214 --- /dev/null +++ b/tests/interactive/test-tar_watch.R @@ -0,0 +1,23 @@ +library(targets) +tar_script({ + sleep_run <- function(...) { + Sys.sleep(10) + } + tar_pipeline( + tar_target(settings, sleep_run()), + tar_target(data1, sleep_run(settings)), + tar_target(data2, sleep_run(settings)), + tar_target(data3, sleep_run(settings)), + tar_target(model1, sleep_run(data1)), + tar_target(model2, sleep_run(data2)), + tar_target(model3, sleep_run(data3)), + tar_target(figure1, sleep_run(model1)), + tar_target(figure2, sleep_run(model2)), + tar_target(figure3, sleep_run(model3)), + tar_target(conclusions, sleep_run(c(figure1, figure2, figure3))) + ) +}) +px <- tar_make(callr_function = callr::r_bg) +tar_watch(seconds = 10, outdated = FALSE, targets_only = TRUE) +tar_destroy() +unlink("_targets.R")