diff --git a/DESCRIPTION b/DESCRIPTION index a176251acb..32e22fa6df 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -90,11 +90,6 @@ LazyData: true Roxygen: list(markdown = TRUE) RoxygenNote: 7.3.2 Collate: - '1.0_filter_panel.R' - '1.0_module_data.R' - '1.0_module_data_summary.R' - '1.0_module_teal_transform.R' - '1.0_validate_reactive_teal_data.R' 'TealAppDriver.R' 'dummy_functions.R' 'get_rcode_utils.R' @@ -103,10 +98,15 @@ Collate: 'init.R' 'landing_popup_module.R' 'module_bookmark_manager.R' + 'module_data_summary.R' + 'module_filter_data.R' 'module_filter_manager.R' + 'module_init_data.R' 'module_nested_tabs.R' 'module_snapshot_manager.R' 'module_teal.R' + 'module_teal_data.R' + 'module_transform_data.R' 'reporter_previewer_module.R' 'show_rcode_modal.R' 'tdata.R' diff --git a/NAMESPACE b/NAMESPACE index 17d050f5cd..543405a83a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -36,6 +36,7 @@ export(srv_teal) export(tdata2env) export(teal_data_module) export(teal_slices) +export(teal_transform_module) export(ui_teal) export(validate_has_data) export(validate_has_elements) diff --git a/R/1.0_module_teal_transform.R b/R/1.0_module_teal_transform.R deleted file mode 100644 index 2d4f8a671f..0000000000 --- a/R/1.0_module_teal_transform.R +++ /dev/null @@ -1,179 +0,0 @@ -#' `teal_data` transform/load module -#' -#' Module consumes `teal_data_module` elements and returns validated data: -#' - `srv/ui_teal_data_module`: executes a single `teal_data_module` -#' - `srv/ui_teal_data_modules` executes multiple `teal_data_module` elements successively by passing -#' output of previous module to the next one. -#' -#' This is a low level module to handle data-loading or data-transformation as in both cases output -#' is a reactive and validated `teal_data`. Data loading can be considered as transformation module -#' of empty (initial) data object. -#' -#' Output `reactive` `teal_data` is validated by [`validate_reactive_teal_data`]. -#' Module makes sure that returned data doesn't break an app, so the [.fallback_on_failure()] is -#' implemented. -#' -#' @param id (`character(1)`) Module id -#' @param data (`reactive teal_data`) -#' @param transformers,transformer (`list of teal_data_module` or `teal_data_module`) -#' @param modules (`teal_modules` or `teal_module`) For `datanames` validation purpose -#' @param validate_shiny_silent_error (`logical(1)`) -#' @param class (`character`) Additional CSS class for whole wrapper div (optional) -#' -#' @return `reactive` `teal_data` -#' -#' @rdname module_teal_data_module -#' @name module_teal_data_module -#' @keywords internal -NULL - -#' @rdname module_teal_data_module -#' @keywords internal -ui_teal_data_modules <- function(id, transformers, class = "") { - checkmate::assert_string(id) - checkmate::assert_list(transformers, "teal_data_module", null.ok = TRUE) - ns <- NS(id) - - labels <- lapply(transformers, function(x) attr(x, "label")) - ids <- get_unique_labels(labels) - names(transformers) <- ids - - lapply( - names(transformers), - function(name) { - data_mod <- transformers[[name]] - wrapper_id <- ns(sprintf("wrapper_%s", name)) - div( # todo: accordion? - # class .teal_validated changes the color of the boarder on error in ui_validate_reactive_teal_data - # For details see tealValidate.js file. - class = c(class, "teal_validated"), - title = attr(data_mod, "label"), - tags$span( - class = "text-primary mb-4", - icon("square-pen", lib = "font-awesome"), - attr(data_mod, "label") - ), - tags$i( - class = "remove pull-right fa fa-angle-down", - title = "fold/expand transform panel", - onclick = sprintf("togglePanelItem(this, '%s', 'fa-angle-right', 'fa-angle-down');", wrapper_id) - ), - div( - id = wrapper_id, - ui_teal_data_module(id = ns(name), transformer = transformers[[name]]) - ) - ) - } - ) -} - -#' @rdname module_teal_data_module -#' @keywords internal -srv_teal_data_modules <- function(id, data, transformers, modules) { - checkmate::assert_string(id) - checkmate::assert_class(data, "reactive") - checkmate::assert_list(transformers, "teal_data_module", null.ok = TRUE) - checkmate::assert_class(modules, "teal_module") - - if (length(transformers) == 0L) { - return(data) - } - - labels <- lapply(transformers, function(x) attr(x, "label")) - ids <- get_unique_labels(labels) - names(transformers) <- ids - - moduleServer(id, function(input, output, session) { - logger::log_debug("srv_teal_data_modules initializing.") - Reduce( - function(previous_result, name) { - srv_teal_data_module( - id = name, - data = previous_result, - transformer = transformers[[name]], - modules = modules - ) - }, - x = names(transformers), - init = data - ) - }) -} - -#' @rdname module_teal_data_module -#' @keywords internal -ui_teal_data_module <- function(id, transformer) { - checkmate::assert_string(id) - checkmate::assert_class(transformer, "teal_data_module") - ns <- NS(id) - shiny::tagList( - transformer$ui(id = ns("data")), - ui_validate_reactive_teal_data(ns("validate")) - ) -} - - -#' @rdname module_teal_data_module -#' @keywords internal -srv_teal_data_module <- function(id, - data, - transformer, - modules = NULL, - validate_shiny_silent_error = TRUE) { - checkmate::assert_string(id) - checkmate::assert_class(data, "reactive") - checkmate::assert_class(transformer, "teal_data_module") - checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"), null.ok = TRUE) - - moduleServer(id, function(input, output, session) { - logger::log_debug("srv_teal_data_module initializing.") - - data_out <- if (is_arg_used(transformer$server, "data")) { - transformer$server(id = "data", data = data) - } else { - transformer$server(id = "data") - } - - data_validated <- srv_validate_reactive_teal_data( - id = "validate", - data = data_out, - modules = modules, - validate_shiny_silent_error = validate_shiny_silent_error - ) - - .fallback_on_failure( - this = data_validated, - that = data, - label = sprintf("Data element '%s' for module '%s'", id, modules$label) - ) - }) -} - -#' Fallback on failure -#' -#' Function returns the previous reactive if the current reactive is invalid (throws error or returns NULL). -#' Application: In `teal` we try to prevent the error from being thrown and instead we replace failing -#' transform module data output with data input from the previous module (or from previous `teal` reactive -#' tree elements). -#' -#' @param this (`reactive`) Current reactive. -#' @param that (`reactive`) Previous reactive. -#' @param label (`character`) Label for identifying problematic `teal_data_module` transform in logging. -#' @return `reactive` `teal_data` -#' @keywords internal -.fallback_on_failure <- function(this, that, label) { - checkmate::assert_class(this, "reactive") - checkmate::assert_class(that, "reactive") - checkmate::assert_string(label) - - reactive({ - res <- try(this(), silent = TRUE) - if (inherits(res, "teal_data")) { - logger::log_debug("{ label } evaluated successfully.") - res - } else { - logger::log_debug("{ label } failed, falling back to previous data.") - isolate(that()) - } - }) -} diff --git a/R/1.0_validate_reactive_teal_data.R b/R/1.0_validate_reactive_teal_data.R deleted file mode 100644 index 0606d166da..0000000000 --- a/R/1.0_validate_reactive_teal_data.R +++ /dev/null @@ -1,116 +0,0 @@ -#' Validate reactive `teal_data` -#' -#' @section data validation: -#' `data` is invalid if: -#' - [teal_data_module()] is invalid if server doesn't return `reactive`. -#' - `reactive` throws a `shiny.error` - happens when module creating [teal_data()] fails. -#' - `reactive` returns `qenv.error` - happens when [teal_data()] evaluates a failing code. -#' - `reactive` object doesn't return [teal_data()]. -#' - [teal_data()] object lacks any `datanames` specified in the `modules` argument. -#' -#' Any errors or warnings are displayed in the app pointing out to the reason of failure. -#' In all above, reactive cycle is halted and `teal` doesn't continue sending data further. On `init`, -#' halting reactive cycle stops an app load, while on subsequent reactive cycles, data just remains -#' unchanged and user is able to continue using the app. -#' -#' @inheritParams module_data -#' @return (`reactive` returning `teal_data`) -#' @rdname validate_reactive_teal_data -#' @name validate_reactive_teal_data -#' @keywords internal -NULL - -#' @rdname validate_reactive_teal_data -#' @keywords internal -ui_validate_reactive_teal_data <- function(id) { - tagList( - uiOutput(NS(id, "shiny_errors")), - uiOutput(NS(id, "shiny_warnings")) - ) -} - -#' @rdname validate_reactive_teal_data -#' @param validate_shiny_silent_error (`logical`) If `TRUE`, then `shiny.silent.error` is validated and -#' error message is displayed. -#' Default is `FALSE` to handle empty reactive cycle on `init`. -#' @keywords internal -srv_validate_reactive_teal_data <- function(id, # nolint: object_length - data, - modules = NULL, - validate_shiny_silent_error = FALSE) { - moduleServer(id, function(input, output, session) { - if (!is.reactive(data)) { - stop("The `teal_data_module` passed to `data` must return a reactive expression.", call. = FALSE) - } - - data_out_rv <- reactive(tryCatch(data(), error = function(e) e)) - - data_validated <- reactive({ - # custom module can return error - data_out <- data_out_rv() - - # there is an empty reactive cycle on init! - if (inherits(data_out, "shiny.silent.error") && identical(data_out$message, "")) { - if (!validate_shiny_silent_error) { - return(NULL) - } else { - validate( - need( - FALSE, - paste( - data_out$message, - "Check your inputs or contact app developer if error persists.", - sep = ifelse(identical(data_out$message, ""), "", "\n") - ) - ) - ) - } - } - - # to handle errors and qenv.error(s) - if (inherits(data_out, c("qenv.error", "error"))) { - validate( - need( - FALSE, - paste( - "Error when executing `teal_data_module` passed to `data`:\n ", - paste(data_out$message, collapse = "\n"), - "\n Check your inputs or contact app developer if error persists." - ) - ) - ) - } - - validate( - need( - inherits(data_out, "teal_data"), - paste( - "Error: `teal_data_module` passed to `data` failed to return `teal_data` object, returned", - toString(sQuote(class(data_out))), - "instead.", - "\n Check your inputs or contact app developer if error persists." - ) - ) - ) - - - data_out - }) - - output$shiny_errors <- renderUI({ - data_validated() - NULL - }) - - output$shiny_warnings <- renderUI({ - if (inherits(data_out_rv(), "teal_data")) { - is_modules_ok <- check_modules_datanames(modules = modules, datanames = teal_data_ls(data_validated())) - if (!isTRUE(is_modules_ok)) { - tags$div(is_modules_ok, class = "teal-output-warning") - } - } - }) - - data_validated - }) -} diff --git a/R/TealAppDriver.R b/R/TealAppDriver.R index 2253713e4e..2ea9774ec2 100644 --- a/R/TealAppDriver.R +++ b/R/TealAppDriver.R @@ -272,9 +272,10 @@ TealAppDriver <- R6::R6Class( # nolint: object_name. get_active_module_table_output = function(table_id, which = 1) { checkmate::check_number(which, lower = 1) checkmate::check_string(table_id) - table <- self$active_module_element(table_id) %>% - self$get_html_rvest() %>% - rvest::html_table(fill = TRUE) + table <- rvest::html_table( + self$get_html_rvest(self$active_module_element(table_id)), + fill = TRUE + ) if (length(table) == 0) { data.frame() } else { @@ -336,11 +337,10 @@ TealAppDriver <- R6::R6Class( # nolint: object_name. #' Get the active data summary table #' @return `data.frame` get_active_data_summary_table = function() { - summary_table <- - self$active_data_summary_element("table") %>% - self$get_html_rvest() %>% - rvest::html_table(fill = TRUE) %>% - .[[1]] + summary_table <- rvest::html_table( + self$get_html_rvest(self$active_data_summary_element("table")), + fill = TRUE + )[[1]] col_names <- unlist(summary_table[1, ], use.names = FALSE) summary_table <- summary_table[-1, ] @@ -400,14 +400,17 @@ TealAppDriver <- R6::R6Class( # nolint: object_name. active_filters <- lapply( datasets, function(x) { - var_names <- self$get_text( - sprintf( - "#%s-filters-%s .filter-card-varname", - self$active_filters_ns(), - x + var_names <- gsub( + pattern = "\\s", + replacement = "", + self$get_text( + sprintf( + "#%s-filters-%s .filter-card-varname", + self$active_filters_ns(), + x + ) ) - ) %>% - gsub(pattern = "\\s", replacement = "") + ) structure( lapply(var_names, private$get_active_filter_selection, dataset_name = x), names = var_names @@ -580,9 +583,10 @@ TealAppDriver <- R6::R6Class( # nolint: object_name. #' #' @return The `character` vector. get_attr = function(selector, attribute) { - self$get_html_rvest("html") %>% - rvest::html_nodes(selector) %>% - rvest::html_attr(attribute) + rvest::html_attr( + rvest::html_nodes(self$get_html_rvest("html"), selector), + attribute + ) }, #' @description #' Wrapper around `get_html` that passes the output directly to `rvest::read_html`. @@ -640,14 +644,13 @@ TealAppDriver <- R6::R6Class( # nolint: object_name. all_inputs <- self$get_values()$input active_tab_inputs <- all_inputs[grepl("-active_tab$", names(all_inputs))] - tab_ns <- lapply(names(active_tab_inputs), function(name) { + tab_ns <- unlist(lapply(names(active_tab_inputs), function(name) { gsub( pattern = "-active_tab$", replacement = sprintf("-%s", active_tab_inputs[[name]]), name ) - }) %>% - unlist() + })) active_ns <- tab_ns[1] if (length(tab_ns) > 1) { for (i in 2:length(tab_ns)) { diff --git a/R/get_rcode_utils.R b/R/get_rcode_utils.R index b112306629..96abf33ed2 100644 --- a/R/get_rcode_utils.R +++ b/R/get_rcode_utils.R @@ -5,19 +5,17 @@ #' @return Character vector of `library()` calls. #' @keywords internal get_rcode_libraries <- function() { - vapply( + libraries <- vapply( utils::sessionInfo()$otherPkgs, function(x) { paste0("library(", x$Package, ")") }, character(1) - ) %>% - # put it into reverse order to correctly simulate executed code - rev() %>% - paste0(sep = "\n") %>% - paste0(collapse = "") + ) + paste0(paste0(rev(libraries), sep = "\n"), collapse = "") } + #' @noRd #' @keywords internal get_rcode_str_install <- function() { diff --git a/R/module_bookmark_manager.R b/R/module_bookmark_manager.R index 358bc16c9d..6f1af5b630 100644 --- a/R/module_bookmark_manager.R +++ b/R/module_bookmark_manager.R @@ -43,8 +43,6 @@ NULL #' @rdname module_bookmark_manager -#' @keywords internal -#' ui_bookmark_panel <- function(id, modules) { ns <- NS(id) @@ -72,7 +70,6 @@ ui_bookmark_panel <- function(id, modules) { } #' @rdname module_bookmark_manager -#' @keywords internal srv_bookmark_panel <- function(id, modules) { checkmate::assert_character(id) checkmate::assert_class(modules, "teal_modules") @@ -153,7 +150,6 @@ srv_bookmark_panel <- function(id, modules) { #' @rdname module_bookmark_manager -#' @keywords internal get_bookmarking_option <- function() { bookmark_option <- getShinyOption("bookmarkStore") if (is.null(bookmark_option) && identical(getOption("shiny.bookmarkStore"), "server")) { @@ -163,7 +159,6 @@ get_bookmarking_option <- function() { } #' @rdname module_bookmark_manager -#' @keywords internal need_bookmarking <- function(modules) { unlist(rapply2( modules_bookmarkable(modules), diff --git a/R/1.0_module_data_summary.R b/R/module_data_summary.R similarity index 97% rename from R/1.0_module_data_summary.R rename to R/module_data_summary.R index 1b0fff71a1..fd4297eeeb 100644 --- a/R/1.0_module_data_summary.R +++ b/R/module_data_summary.R @@ -12,13 +12,14 @@ #' `shiny` module instance id. #' @param teal_data (`reactive` returning `teal_data`) #' +#' #' @name module_data_summary #' @rdname module_data_summary +#' @keywords internal #' @return `NULL`. NULL #' @rdname module_data_summary -#' @keywords internal ui_data_summary <- function(id) { ns <- NS(id) content_id <- ns("filters_overview_contents") @@ -51,7 +52,6 @@ ui_data_summary <- function(id) { } #' @rdname module_data_summary -#' @keywords internal srv_data_summary <- function(id, teal_data) { checkmate::check_class(teal_data, "reactive") moduleServer( @@ -127,7 +127,6 @@ srv_data_summary <- function(id, teal_data) { } #' @rdname module_data_summary -#' @keywords internal get_filter_overview <- function(teal_data) { datanames <- teal.data::datanames(teal_data()) joinkeys <- teal.data::join_keys(teal_data()) @@ -149,7 +148,6 @@ get_filter_overview <- function(teal_data) { # - Obs and Subjects # - Obs only # - Subjects only - # todo: summary table should be ordered by topological order # todo (for later): summary table should be displayed in a way that child datasets # are indented under their parent dataset to form a tree structure subject_keys <- if (length(parent) > 0) { @@ -171,7 +169,9 @@ get_filter_overview <- function(teal_data) { } #' @rdname module_data_summary -#' @keywords internal +#' @param filtered_data (`list`) of filtered objects +#' @param unfiltered_data (`list`) of unfiltered objects +#' @param dataname (`character(1)`) get_object_filter_overview <- function(filtered_data, unfiltered_data, dataname, subject_keys) { if (inherits(filtered_data, c("data.frame", "DataFrame", "array", "Matrix", "SummarizedExperiment"))) { get_object_filter_overview_array(filtered_data, unfiltered_data, dataname, subject_keys) @@ -189,7 +189,6 @@ get_object_filter_overview <- function(filtered_data, unfiltered_data, dataname, } #' @rdname module_data_summary -#' @keywords internal get_object_filter_overview_array <- function(filtered_data, # nolint: object_length. unfiltered_data, dataname, @@ -214,7 +213,6 @@ get_object_filter_overview_array <- function(filtered_data, # nolint: object_len } #' @rdname module_data_summary -#' @keywords internal get_object_filter_overview_MultiAssayExperiment <- function(filtered_data, # nolint: object_length, object_name. unfiltered_data, dataname) { diff --git a/R/1.0_filter_panel.R b/R/module_filter_data.R similarity index 90% rename from R/1.0_filter_panel.R rename to R/module_filter_data.R index 56878185c2..13940321de 100644 --- a/R/1.0_filter_panel.R +++ b/R/module_filter_data.R @@ -1,7 +1,7 @@ #' Filter panel module in teal #' #' Creates filter panel module from `teal_data` object and returns `teal_data`. It is build in a way -#' that filter panel changes and anything what happens before (e.g. [`module_data`]) is triggering +#' that filter panel changes and anything what happens before (e.g. [`module_init_data`]) is triggering #' further reactive events only if something has changed and if the module is visible. Thanks to #' this special implementation all modules' data are recalculated only for those modules which are #' currently displayed. @@ -12,20 +12,18 @@ #' #' @inheritParams module_teal_module #' @param active_datanames (`reactive` returning `character`) this module's data names -#' @name module_filter_panel +#' @name module_filter_data #' @keywords internal NULL -#' @keywords internal -#' @rdname module_filter_panel -ui_filter_panel <- function(id) { +#' @rdname module_filter_data +ui_filter_data <- function(id) { ns <- shiny::NS(id) uiOutput(ns("panel")) } -#' @keywords internal -#' @rdname module_filter_panel -srv_filter_panel <- function(id, datasets, active_datanames, data_rv, is_active) { +#' @rdname module_filter_data +srv_filter_data <- function(id, datasets, active_datanames, data_rv, is_active) { checkmate::assert_class(datasets, "reactive") moduleServer(id, function(input, output, session) { output$panel <- renderUI({ @@ -49,8 +47,7 @@ srv_filter_panel <- function(id, datasets, active_datanames, data_rv, is_active) }) } -#' @keywords internal -#' @rdname module_filter_panel +#' @rdname module_filter_data .make_filtered_teal_data <- function(modules, data, datasets = NULL, datanames) { new_datasets <- c( # Filtered data @@ -81,8 +78,7 @@ srv_filter_panel <- function(id, datasets, active_datanames, data_rv, is_active) tdata } -#' @rdname module_filter_panel -#' @keywords internal +#' @rdname module_filter_data .observe_active_filter_changed <- function(datasets, is_active, active_datanames, data_rv) { previous_signature <- reactiveVal(NULL) filter_changed <- reactive({ diff --git a/R/module_filter_manager.R b/R/module_filter_manager.R index ecb22eb8e6..5c78179173 100644 --- a/R/module_filter_manager.R +++ b/R/module_filter_manager.R @@ -52,7 +52,6 @@ NULL #' @rdname module_filter_manager -#' @keywords internal ui_filter_manager_panel <- function(id) { ns <- NS(id) tags$button( @@ -86,9 +85,7 @@ srv_filter_manager_panel <- function(id, slices_global) { }) } - #' @rdname module_filter_manager -#' @keywords internal ui_filter_manager <- function(id) { ns <- NS(id) actionButton(ns("filter_manager"), NULL, icon = icon("filter")) @@ -99,7 +96,6 @@ ui_filter_manager <- function(id) { } #' @rdname module_filter_manager -#' @keywords internal srv_filter_manager <- function(id, slices_global) { checkmate::assert_string(id) checkmate::assert_class(slices_global, ".slicesGlobal") @@ -169,7 +165,6 @@ srv_filter_manager <- function(id, slices_global) { } #' @rdname module_filter_manager -#' @keywords internal srv_module_filter_manager <- function(id, module_fd, slices_global) { checkmate::assert_string(id) checkmate::assert_class(module_fd, "reactive") @@ -238,10 +233,8 @@ srv_module_filter_manager <- function(id, module_fd, slices_global) { methods::setOldClass("reactiveVal") methods::setOldClass("reactivevalues") - #' @importFrom methods new #' @rdname module_filter_manager -#' @keywords internal .slicesGlobal <- methods::setRefClass(".slicesGlobal", # nolint fields = list( all_slices = "reactiveVal", diff --git a/R/1.0_module_data.R b/R/module_init_data.R similarity index 84% rename from R/1.0_module_data.R rename to R/module_init_data.R index 045430dff5..6793833d61 100644 --- a/R/1.0_module_data.R +++ b/R/module_init_data.R @@ -1,14 +1,15 @@ #' Data module for teal #' -#' Fundamental data class for teal is [teal.data::teal_data()]. Data can be -#' passed in multiple ways: -#' 1. Directly as a [teal.data::teal_data()] object. +#' Module handles `data` argument to the `srv_teal`. `teal` uses [teal_data()] within +#' the whole framework and it could be provided in several way: +#' 1. Directly as a [teal.data::teal_data()] object. This will be automatically converted +#' to `reactive` `teal_data`. #' 2. As a `reactive` object returning [teal.data::teal_data()]. [See section](#reactive-teal_data). #' #' @section Reactive `teal_data`: #' -#' [teal.data::teal_data()] can change depending on the reactive context and `srv_teal` will rebuild -#' the app accordingly. There are two ways of interacting with the data: +#' Data included to the application can be reactively changed and [srv_teal()] will rebuild +#' the content respectively. There are two ways of making interactive `teal_data`: #' 1. Using a `reactive` object passed from outside the `teal` application. In this case, reactivity #' is controlled by external module and `srv_teal` will trigger accordingly to the changes. #' 2. Using [teal_data_module()] which is embedded in the `teal` application and data can be @@ -19,39 +20,36 @@ #' The difference is that in the first case the data is controlled from outside the app and in the #' second case the data is controlled from custom module called inside of the app. #' -#' see [`validate_reactive_teal_data`] for more details. +#' see [`module_teal_data`] for more details. #' #' @inheritParams init #' #' @param data (`teal_data`, `teal_data_module` or `reactive` returning `teal_data`) -#' @return A `reactiveVal` which is set to: +#' @return A `reactive` which returns: #' - `teal_data` when the object is validated -#' - `NULL` when not validated. -#' Important: `srv_data` suppress validate messages and returns `NULL` so that `srv_teal` can -#' stop the reactive cycle as `observeEvent` calls based on the data have `ignoreNULL = TRUE`. +#' - `shiny.silent.error` when not validated. #' -#' @rdname module_data -#' @name module_data +#' @rdname module_init_data +#' @name module_init_data +#' @keywords internal NULL -#' @rdname module_data -#' @keywords internal -ui_data <- function(id, data, title, header, footer) { +#' @rdname module_init_data +ui_init_data <- function(id, data) { ns <- shiny::NS(id) shiny::div( id = ns("content"), style = "display: inline-block;", if (inherits(data, "teal_data_module")) { - ui_teal_data_module(ns("teal_data_module"), transformer = data) + ui_teal_data(ns("teal_data_module"), data_module = data) } else { NULL } ) } -#' @rdname module_data -#' @keywords internal -srv_data <- function(id, data, modules, filter = teal_slices()) { +#' @rdname module_init_data +srv_init_data <- function(id, data, modules, filter = teal_slices()) { checkmate::assert_character(id, max.len = 1, any.missing = FALSE) checkmate::assert_multi_class(data, c("teal_data", "teal_data_module", "reactive", "reactiveVal")) checkmate::assert_class(modules, "teal_modules") @@ -67,10 +65,10 @@ srv_data <- function(id, data, modules, filter = teal_slices()) { # data_rv contains teal_data object # either passed to teal::init or returned from teal_data_module data_validated <- if (inherits(data, "teal_data_module")) { - srv_teal_data_module( + srv_teal_data( "teal_data_module", data = reactive(req(FALSE)), # to .fallback_on_failure to shiny.silent.error - transformer = data, + data_module = data, modules = modules, validate_shiny_silent_error = FALSE ) diff --git a/R/module_nested_tabs.R b/R/module_nested_tabs.R index 3606295a7f..8626877591 100644 --- a/R/module_nested_tabs.R +++ b/R/module_nested_tabs.R @@ -1,15 +1,11 @@ #' Create a UI of nested tabs of `teal_modules` #' -#' @section `ui_teal_module`: -#' Each `teal_modules` is translated to a `bslib::navset_tab` and each +#' On the UI side each `teal_modules` is translated to a `tabsetPanel` and each #' of its children is another tab-module called recursively. The UI of a #' `teal_module` is obtained by calling its UI function. #' -#' The `datasets` argument is required to resolve the `teal` arguments in an -#' isolated context (with respect to reactivity). -#' -#' @section `srv_teal_module`: -#' This module recursively calls all elements of `modules` and returns currently active one. +#' On the server side module recursively calls all elements of `modules` and returns currently +#' active one. #' - `teal_module` returns self as a active module. #' - `teal_modules` also returns module active within self which is determined by the `input$active_tab`. #' @@ -117,9 +113,9 @@ ui_teal_module.teal_module <- function(id, modules, depth = 0L) { column( width = 3, ui_data_summary(ns("data_summary")), - ui_filter_panel(ns("filter_panel")), + ui_filter_data(ns("filter_panel")), if (length(modules$transformers) > 0 && !isTRUE(attr(modules$transformers, "custom_ui"))) { - ui_teal_data_modules(ns("data_transform"), modules$transformers, class = "well") + ui_transform_data(ns("data_transform"), transforms = modules$transformers, class = "well") }, class = "teal_secondary_col" ) @@ -217,7 +213,7 @@ srv_teal_module.teal_module <- function(id, # Because available_teal_slices is used in FilteredData$srv_available_slices (via srv_filter_panel) # and if it is not set, then it won't be available in the srv_filter_panel srv_module_filter_manager(modules$label, module_fd = datasets, slices_global = slices_global) - filtered_teal_data <- srv_filter_panel( + filtered_teal_data <- srv_filter_data( "filter_panel", datasets = datasets, active_datanames = active_datanames, @@ -225,10 +221,10 @@ srv_teal_module.teal_module <- function(id, is_active = is_active ) - transformed_teal_data <- srv_teal_data_modules( + transformed_teal_data <- srv_transform_data( "data_transform", data = filtered_teal_data, - transformers = modules$transformers, + transforms = modules$transformers, modules = modules ) diff --git a/R/module_snapshot_manager.R b/R/module_snapshot_manager.R index 5ab945056b..604804826f 100644 --- a/R/module_snapshot_manager.R +++ b/R/module_snapshot_manager.R @@ -85,7 +85,6 @@ NULL #' @rdname module_snapshot_manager -#' @keywords internal ui_snapshot_manager_panel <- function(id) { ns <- NS(id) tags$button( @@ -97,8 +96,6 @@ ui_snapshot_manager_panel <- function(id) { } #' @rdname module_snapshot_manager -#' @keywords internal -#' srv_snapshot_manager_panel <- function(id, slices_global) { moduleServer(id, function(input, output, session) { logger::log_debug("srv_snapshot_manager_panel initializing") @@ -120,9 +117,6 @@ srv_snapshot_manager_panel <- function(id, slices_global) { } #' @rdname module_snapshot_manager -#' @keywords internal -#' @keywords internal -#' ui_snapshot_manager <- function(id) { ns <- NS(id) tags$div( @@ -140,8 +134,6 @@ ui_snapshot_manager <- function(id) { } #' @rdname module_snapshot_manager -#' @keywords internal -#' srv_snapshot_manager <- function(id, slices_global) { checkmate::assert_character(id) diff --git a/R/module_teal.R b/R/module_teal.R index ff326c6fb9..0ebb550d48 100644 --- a/R/module_teal.R +++ b/R/module_teal.R @@ -2,25 +2,26 @@ #' `teal` main app module #' -#' This module is a central point of the `teal` app. It is called by [teal::init()] but can be also -#' used as a standalone module in your custom application. It is responsible for creating the main -#' `shiny` app layout and initializing all the necessary components: -#' - [`module_data`] - for handling the `data`. -#' - [`module_teal_module`] - for handling the `modules`. -#' - [`module_filter_manager`] - for handling the `filter`. -#' - [`module_snapshot_manager`] - for handling the `snapshots`. -#' - [`module_bookmark_manager`] - for handling the `bookmarks`. +#' Module to create a `teal` app. This module (`ui` and `server`) is called directly by [init()] after +#' initial argument checking and setting default values. This module can be called directly and +#' included in your custom application. +#' Module is responsible for creating the main `shiny` app layout and initializing all the necessary +#' components. This module establishes reactive connection between the input `data` and every other +#' component in the app. Reactive change of the `data` triggers reload of the app and possibly +#' keeping all inputs settings the same so the user can continue where one left off. #' -#' This module establishes reactive connection between the `data` and every other component in the app. -#' Reactive change of the `data` triggers reload of the app and possibly keeping all inputs settings -#' the same so the user can continue where one left off. -#' Similar applies to [`module_bookmark_manager`] which allows to start a new session with restored -#' inputs. +#' @section data flow in `teal` application: +#' `teal` supports multiple data inputs (see `data` in [`module_init_data`]) but eventually, they are +#' all converted to `reactive` returning `teal_data` in `module_teal`. There are several operations +#' on this `reactive teal_data` object: +#' - data loading in [`module_init_data`] +#' - data filtering in [`module_filter_data`] +#' - data transformation in [`module_transform_data`] #' #' @rdname module_teal #' @name module_teal #' -#' @inheritParams module_data +#' @inheritParams module_init_data #' @inheritParams init #' #' @return @@ -84,7 +85,7 @@ ui_teal <- function(id, ) bookmark_panel_ui <- ui_bookmark_panel(ns("bookmark_manager"), modules) - data_elem <- ui_data(ns("data"), data = data, title = title, header = header, footer = footer) + data_elem <- ui_init_data(ns("data"), data = data) if (!is.null(data)) { modules$children <- c(list(teal_data_module = data_elem), modules$children) } @@ -182,7 +183,7 @@ srv_teal <- function(id, data, modules, filter = teal_slices()) { ) # todo: introduce option `run_once` to not show data icon when app is loaded (in case when data don't change). - data_rv <- srv_data("data", data = data, modules = modules, filter = filter) + data_rv <- srv_init_data("data", data = data, modules = modules, filter = filter) datasets_rv <- if (!isTRUE(attr(filter, "module_specific"))) { eventReactive(data_rv(), { if (!inherits(data_rv(), "teal_data")) { diff --git a/R/module_teal_data.R b/R/module_teal_data.R new file mode 100644 index 0000000000..05ab1942ef --- /dev/null +++ b/R/module_teal_data.R @@ -0,0 +1,203 @@ +#' Execute and validate `teal_data_module` +#' +#' This is a low level module to handle `teal_data_module` execution and validation. +#' [teal_transform_module()] inherits from [teal_data_module()] so it is handled by this module too. +#' [srv_teal()] accepts various `data` objects and eventually they are all transformed to `reactive` +#' [teal_data()] which is a standard data class in whole `teal` framework. +#' +#' @section data validation: +#' +#' Executed [teal_data_module()] is validated and output is validated for consistency. +#' Output `data` is invalid if: +#' 1. [teal_data_module()] is invalid if server doesn't return `reactive`. **Immediately crashes an app!** +#' 2. `reactive` throws a `shiny.error` - happens when module creating [teal_data()] fails. +#' 3. `reactive` returns `qenv.error` - happens when [teal_data()] evaluates a failing code. +#' 4. `reactive` object doesn't return [teal_data()]. +#' 5. [teal_data()] object lacks any `datanames` specified in the `modules` argument. +#' +#' `teal` (observers in `srv_teal`) always waits to render an app until `reactive` `teal_data` is +#' returned. If error 2-4 occurs, relevant error message is displayed to app user and after issue is +#' resolved app will continue to run. `teal` guarantees that errors in a data don't crash an app +#' (except error 1). This is possible thanks to `.fallback_on_failure` which returns input-data +#' when output-data fails +#' +#' +#' @param id (`character(1)`) Module id +#' @param data (`reactive teal_data`) +#' @param data_module (`teal_data_module`) +#' @param modules (`teal_modules` or `teal_module`) For `datanames` validation purpose +#' @param validate_shiny_silent_error (`logical`) If `TRUE`, then `shiny.silent.error` is validated and +#' error message is displayed. +#' Default is `FALSE` to handle empty reactive cycle on `init`. +#' +#' @return `reactive` `teal_data` +#' +#' @rdname module_teal_data +#' @name module_teal_data +#' @keywords internal +NULL + +#' @rdname module_teal_data +ui_teal_data <- function(id, data_module) { + checkmate::assert_string(id) + checkmate::assert_class(data_module, "teal_data_module") + ns <- NS(id) + shiny::tagList( + data_module$ui(id = ns("data")), + ui_validate_reactive_teal_data(ns("validate")) + ) +} + +#' @rdname module_teal_data +srv_teal_data <- function(id, + data, + data_module, + modules = NULL, + validate_shiny_silent_error = TRUE) { + checkmate::assert_string(id) + checkmate::assert_class(data, "reactive") + checkmate::assert_class(data_module, "teal_data_module") + checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"), null.ok = TRUE) + + moduleServer(id, function(input, output, session) { + logger::log_debug("srv_teal_data initializing.") + + data_out <- if (is_arg_used(data_module$server, "data")) { + data_module$server(id = "data", data = data) + } else { + data_module$server(id = "data") + } + + data_validated <- srv_validate_reactive_teal_data( + id = "validate", + data = data_out, + modules = modules, + validate_shiny_silent_error = validate_shiny_silent_error + ) + + .fallback_on_failure( + this = data_validated, + that = data, + label = sprintf("Data element '%s' for module '%s'", id, modules$label) + ) + }) +} + +#' @rdname module_teal_data +ui_validate_reactive_teal_data <- function(id) { + tagList( + uiOutput(NS(id, "shiny_errors")), + uiOutput(NS(id, "shiny_warnings")) + ) +} + +#' @rdname module_teal_data +srv_validate_reactive_teal_data <- function(id, # nolint: object_length + data, + modules = NULL, + validate_shiny_silent_error = FALSE) { + moduleServer(id, function(input, output, session) { + if (!is.reactive(data)) { + stop("The `teal_data_module` passed to `data` must return a reactive expression.", call. = FALSE) + } + + data_out_rv <- reactive(tryCatch(data(), error = function(e) e)) + + data_validated <- reactive({ + # custom module can return error + data_out <- data_out_rv() + + # there is an empty reactive cycle on init! + if (inherits(data_out, "shiny.silent.error") && identical(data_out$message, "")) { + if (!validate_shiny_silent_error) { + return(NULL) + } else { + validate( + need( + FALSE, + paste( + data_out$message, + "Check your inputs or contact app developer if error persists.", + sep = ifelse(identical(data_out$message, ""), "", "\n") + ) + ) + ) + } + } + + # to handle errors and qenv.error(s) + if (inherits(data_out, c("qenv.error", "error"))) { + validate( + need( + FALSE, + paste( + "Error when executing `teal_data_module` passed to `data`:\n ", + paste(data_out$message, collapse = "\n"), + "\n Check your inputs or contact app developer if error persists." + ) + ) + ) + } + + validate( + need( + inherits(data_out, "teal_data"), + paste( + "Error: `teal_data_module` passed to `data` failed to return `teal_data` object, returned", + toString(sQuote(class(data_out))), + "instead.", + "\n Check your inputs or contact app developer if error persists." + ) + ) + ) + + + data_out + }) + + output$shiny_errors <- renderUI({ + data_validated() + NULL + }) + + output$shiny_warnings <- renderUI({ + if (inherits(data_out_rv(), "teal_data")) { + is_modules_ok <- check_modules_datanames(modules = modules, datanames = teal_data_ls(data_validated())) + if (!isTRUE(is_modules_ok)) { + tags$div(is_modules_ok, class = "teal-output-warning") + } + } + }) + + data_validated + }) +} + +#' Fallback on failure +#' +#' Function returns the previous reactive if the current reactive is invalid (throws error or returns NULL). +#' Application: In `teal` we try to prevent the error from being thrown and instead we replace failing +#' transform module data output with data input from the previous module (or from previous `teal` reactive +#' tree elements). +#' +#' @param this (`reactive`) Current reactive. +#' @param that (`reactive`) Previous reactive. +#' @param label (`character`) Label for identifying problematic `teal_data_module` transform in logging. +#' @return `reactive` `teal_data` +#' @keywords internal +.fallback_on_failure <- function(this, that, label) { + checkmate::assert_class(this, "reactive") + checkmate::assert_class(that, "reactive") + checkmate::assert_string(label) + + reactive({ + res <- try(this(), silent = TRUE) + if (inherits(res, "teal_data")) { + logger::log_debug("{ label } evaluated successfully.") + res + } else { + logger::log_debug("{ label } failed, falling back to previous data.") + isolate(that()) + } + }) +} diff --git a/R/module_transform_data.R b/R/module_transform_data.R new file mode 100644 index 0000000000..ac85c3d022 --- /dev/null +++ b/R/module_transform_data.R @@ -0,0 +1,83 @@ +#' Module to transform `reactive` `teal_data` +#' +#' Module calls multiple [module_teal_data] in sequence so that `reactive `teal_data` output +#' from one module is handed over to the following module's input. +#' +#' @inheritParams module_teal_data +#' @inheritParams teal_modules +#' @return `reactive` `teal_data` +#' +#' +#' @name module_transform_data +#' @keywords internal +NULL + +#' @rdname module_transform_data +ui_transform_data <- function(id, transforms, class = "well") { + checkmate::assert_string(id) + checkmate::assert_list(transforms, "teal_transform_module", null.ok = TRUE) + ns <- NS(id) + labels <- lapply(transforms, function(x) attr(x, "label")) + ids <- get_unique_labels(labels) + names(transforms) <- ids + + lapply( + names(transforms), + function(name) { + data_mod <- transforms[[name]] + wrapper_id <- ns(sprintf("wrapper_%s", name)) + div( # todo: accordion? + # class .teal_validated changes the color of the boarder on error in ui_validate_reactive_teal_data + # For details see tealValidate.js file. + class = c(class, "teal_validated"), + title = attr(data_mod, "label"), + tags$span( + class = "text-primary mb-4", + icon("square-pen", lib = "font-awesome"), + attr(data_mod, "label") + ), + tags$i( + class = "remove pull-right fa fa-angle-down", + title = "fold/expand transform panel", + onclick = sprintf("togglePanelItem(this, '%s', 'fa-angle-right', 'fa-angle-down');", wrapper_id) + ), + div( + id = wrapper_id, + ui_teal_data(id = ns(name), data_module = transforms[[name]]) + ) + ) + } + ) +} + +#' @rdname module_transform_data +srv_transform_data <- function(id, data, transforms, modules) { + checkmate::assert_string(id) + checkmate::assert_class(data, "reactive") + checkmate::assert_list(transforms, "teal_transform_module", null.ok = TRUE) + checkmate::assert_class(modules, "teal_module") + + if (length(transforms) == 0L) { + return(data) + } + + labels <- lapply(transforms, function(x) attr(x, "label")) + ids <- get_unique_labels(labels) + names(transforms) <- ids + + moduleServer(id, function(input, output, session) { + logger::log_debug("srv_teal_data_modules initializing.") + Reduce( + function(previous_result, name) { + srv_teal_data( + id = name, + data = previous_result, + data_module = transforms[[name]], + modules = modules + ) + }, + x = names(transforms), + init = data + ) + }) +} diff --git a/R/modules.R b/R/modules.R index 1f375eef19..c62c30e428 100644 --- a/R/modules.R +++ b/R/modules.R @@ -49,7 +49,7 @@ #' @param ui_args (named `list`) with additional arguments passed on to the UI function. #' @param x (`teal_module` or `teal_modules`) Object to format/print. #' @param indent (`integer(1)`) Indention level; each nested element is indented one level more. -#' @param transformers (`list`) with `teal_data_module` that will be applied to transform the data. +#' @param transformers (`list` of `teal_data_module`) that will be applied to transform the data. #' Each transform module UI will appear in the `teal` application, unless the `custom_ui` attribute is set on the list. #' If so, the module developer is responsible to display the UI in the module itself. #' @@ -134,7 +134,7 @@ module <- function(label = "module", datanames = "all", server_args = NULL, ui_args = NULL, - transformers = NULL) { + transformers = list()) { # argument checking (independent) ## `label` checkmate::assert_string(label) @@ -242,7 +242,7 @@ module <- function(label = "module", } ## `transformers` - checkmate::assert_list(transformers, null.ok = TRUE, types = "teal_data_module") + checkmate::assert_list(transformers, types = "teal_data_module") structure( list( diff --git a/R/tdata.R b/R/tdata.R index 698cc90885..446e89c842 100644 --- a/R/tdata.R +++ b/R/tdata.R @@ -171,7 +171,10 @@ get_metadata.default <- function(data, dataname) { #' #' @examples #' td <- teal_data() -#' td <- within(td, iris <- iris) %>% within(mtcars <- mtcars) +#' td <- within( +#' within(td, iris <- iris), +#' mtcars <- mtcars +#' ) #' td #' as_tdata(td) #' as_tdata(reactive(td)) diff --git a/R/teal_data_module.R b/R/teal_data_module.R index ff375d184e..ea37f62ba1 100644 --- a/R/teal_data_module.R +++ b/R/teal_data_module.R @@ -61,11 +61,7 @@ #' @export teal_data_module <- function(ui, server, label = "data module", once = TRUE) { checkmate::assert_function(ui, args = "id", nargs = 1) - checkmate::assert( - checkmate::check_function(server, args = "id", nargs = 1), - # todo: allow for teal_data_module$server to have 'data' argument or break this in teal_transformer_module - checkmate::check_function(server, args = c("id", "data"), nargs = 2) - ) + checkmate::assert_function(server, args = "id", nargs = 1) structure( list(ui = ui, server = server), label = label, @@ -73,3 +69,63 @@ teal_data_module <- function(ui, server, label = "data module", once = TRUE) { once = once ) } + +#' Data module for `teal` transformers. +#' +#' @description +#' `r lifecycle::badge("experimental")` +#' +#' Create a `teal_data_module` object for custom transformation of data for pre-processing +#' before passing the data into the module. +#' +#' @details +#' `teal_transform_module` creates a `teal_data_module` object to transform data in a `teal` +#' application. This transformation happens after the data has passed through the filtering activity +#' in teal. The transformed data is then sent to the server of the [teal_module()]. +#' +#' See vignette `vignette("data-transform-as-shiny-module", package = "teal")` for more details. +#' +#' +#' @inheritParams teal_data_module +#' @param server (`function(id, data)`) +#' `shiny` module server function; that takes `id` and `data` argument, +#' where the `id` is the module id and `data` is the reactive `teal_data` input. +#' The server function must return reactive expression containing `teal_data` object. +#' @examples +#' my_transformers <- list( +#' teal_transform_module( +#' label = "Custom transform for iris", +#' ui = function(id) { +#' ns <- NS(id) +#' tags$div( +#' numericInput(ns("n_rows"), "Subset n rows", value = 6, min = 1, max = 150, step = 1) +#' ) +#' }, +#' server = function(id, data) { +#' moduleServer(id, function(input, output, session) { +#' reactive({ +#' within(data(), +#' { +#' iris <- head(iris, num_rows) +#' }, +#' num_rows = input$n_rows +#' ) +#' }) +#' }) +#' } +#' ) +#' ) +#' +#' @name teal_transform_module +#' @seealso [`teal_data_module`], [teal_data_module()] +#' +#' @export +teal_transform_module <- function(ui, server, label = "transform module") { + checkmate::assert_function(ui, args = "id", nargs = 1) + checkmate::assert_function(server, args = c("id", "data"), nargs = 2) + structure( + list(ui = ui, server = server), + label = label, + class = c("teal_transform_module", "teal_data_module") + ) +} diff --git a/_pkgdown.yml b/_pkgdown.yml index 81eae88ee1..cdfa451992 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -64,6 +64,7 @@ articles: contents: - including-data-in-teal-applications - data-as-shiny-module + - data-transform-as-shiny-module - title: Extending `teal` navbar: Extending `teal` contents: @@ -96,6 +97,7 @@ reference: contents: - init - teal_data_module + - teal_transform_module - module_teal - module - modules diff --git a/inst/WORDLIST b/inst/WORDLIST index 6aef77c6ae..8c079861d8 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -21,6 +21,7 @@ lockfile omics pre programmatically +reactively repo reproducibility summarization diff --git a/man/dot-add_signature_to_data.Rd b/man/dot-add_signature_to_data.Rd index 12889beef9..b7e242c32e 100644 --- a/man/dot-add_signature_to_data.Rd +++ b/man/dot-add_signature_to_data.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/1.0_module_data.R +% Please edit documentation in R/module_init_data.R \name{.add_signature_to_data} \alias{.add_signature_to_data} \title{Adds signature protection to the \code{datanames} in the data} diff --git a/man/dot-fallback_on_failure.Rd b/man/dot-fallback_on_failure.Rd index 4d11cca78c..5d8f168e2d 100644 --- a/man/dot-fallback_on_failure.Rd +++ b/man/dot-fallback_on_failure.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/1.0_module_teal_transform.R +% Please edit documentation in R/module_teal_data.R \name{.fallback_on_failure} \alias{.fallback_on_failure} \title{Fallback on failure} diff --git a/man/dot-get_hashes_code.Rd b/man/dot-get_hashes_code.Rd index 39209e6636..527a9617b8 100644 --- a/man/dot-get_hashes_code.Rd +++ b/man/dot-get_hashes_code.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/1.0_module_data.R +% Please edit documentation in R/module_init_data.R \name{.get_hashes_code} \alias{.get_hashes_code} \title{Get code that tests the integrity of the reproducible data} diff --git a/man/example_module.Rd b/man/example_module.Rd index fd29aad6c1..8227e44e45 100644 --- a/man/example_module.Rd +++ b/man/example_module.Rd @@ -20,7 +20,7 @@ filters in the listed datasets. \code{NULL} will hide the filter panel, and the keyword \code{"all"} will show filters of all datasets. \code{datanames} also determines a subset of datasets which are appended to the \code{data} argument in server function.} -\item{transformers}{(\code{list}) with \code{teal_data_module} that will be applied to transform the data. +\item{transformers}{(\code{list} of \code{teal_data_module}) that will be applied to transform the data. Each transform module UI will appear in the \code{teal} application, unless the \code{custom_ui} attribute is set on the list. If so, the module developer is responsible to display the UI in the module itself. diff --git a/man/figures/filter_state_reactivity.jpg b/man/figures/filter_state_reactivity.jpg deleted file mode 100644 index cd646939cc..0000000000 Binary files a/man/figures/filter_state_reactivity.jpg and /dev/null differ diff --git a/man/figures/module_nested_tabs.jpg b/man/figures/module_nested_tabs.jpg deleted file mode 100644 index d86ba39127..0000000000 Binary files a/man/figures/module_nested_tabs.jpg and /dev/null differ diff --git a/man/figures/notification.jpg b/man/figures/notification.jpg deleted file mode 100644 index a80c024f69..0000000000 Binary files a/man/figures/notification.jpg and /dev/null differ diff --git a/man/module_data_summary.Rd b/man/module_data_summary.Rd index c4838bd892..2bc009a17a 100644 --- a/man/module_data_summary.Rd +++ b/man/module_data_summary.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/1.0_module_data_summary.R +% Please edit documentation in R/module_data_summary.R \name{module_data_summary} \alias{module_data_summary} \alias{ui_data_summary} @@ -41,6 +41,12 @@ get_object_filter_overview_MultiAssayExperiment( \code{shiny} module instance id.} \item{teal_data}{(\code{reactive} returning \code{teal_data})} + +\item{filtered_data}{(\code{list}) of filtered objects} + +\item{unfiltered_data}{(\code{list}) of unfiltered objects} + +\item{dataname}{(\code{character(1)})} } \value{ \code{NULL}. diff --git a/man/module_filter_panel.Rd b/man/module_filter_data.Rd similarity index 87% rename from man/module_filter_panel.Rd rename to man/module_filter_data.Rd index e27bdecb0a..bcd50e5329 100644 --- a/man/module_filter_panel.Rd +++ b/man/module_filter_data.Rd @@ -1,16 +1,16 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/1.0_filter_panel.R -\name{module_filter_panel} -\alias{module_filter_panel} -\alias{ui_filter_panel} -\alias{srv_filter_panel} +% Please edit documentation in R/module_filter_data.R +\name{module_filter_data} +\alias{module_filter_data} +\alias{ui_filter_data} +\alias{srv_filter_data} \alias{.make_filtered_teal_data} \alias{.observe_active_filter_changed} \title{Filter panel module in teal} \usage{ -ui_filter_panel(id) +ui_filter_data(id) -srv_filter_panel(id, datasets, active_datanames, data_rv, is_active) +srv_filter_data(id, datasets, active_datanames, data_rv, is_active) .make_filtered_teal_data(modules, data, datasets = NULL, datanames) @@ -44,7 +44,7 @@ A \code{eventReactive} which triggers only if all conditions are met: } \description{ Creates filter panel module from \code{teal_data} object and returns \code{teal_data}. It is build in a way -that filter panel changes and anything what happens before (e.g. \code{\link{module_data}}) is triggering +that filter panel changes and anything what happens before (e.g. \code{\link{module_init_data}}) is triggering further reactive events only if something has changed and if the module is visible. Thanks to this special implementation all modules' data are recalculated only for those modules which are currently displayed. diff --git a/man/module_data.Rd b/man/module_init_data.Rd similarity index 60% rename from man/module_data.Rd rename to man/module_init_data.Rd index cca4701d76..9f141f793f 100644 --- a/man/module_data.Rd +++ b/man/module_init_data.Rd @@ -1,14 +1,14 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/1.0_module_data.R -\name{module_data} -\alias{module_data} -\alias{ui_data} -\alias{srv_data} +% Please edit documentation in R/module_init_data.R +\name{module_init_data} +\alias{module_init_data} +\alias{ui_init_data} +\alias{srv_init_data} \title{Data module for teal} \usage{ -ui_data(id, data, title, header, footer) +ui_init_data(id, data) -srv_data(id, data, modules, filter = teal_slices()) +srv_init_data(id, data, modules, filter = teal_slices()) } \arguments{ \item{id}{(\code{character}) optional @@ -17,17 +17,6 @@ rather than a standalone \code{shiny} app. This is a legacy feature.} \item{data}{(\code{teal_data}, \code{teal_data_module} or \code{reactive} returning \code{teal_data})} -\item{title}{(\code{shiny.tag} or \code{character(1)}) -The browser window title. Defaults to a title "teal app" with the icon of NEST. -Can be created using the \code{build_app_title()} or -by passing a valid \code{shiny.tag} which is a head tag with title and link tag.} - -\item{header}{(\code{shiny.tag} or \code{character(1)}) -The header of the app.} - -\item{footer}{(\code{shiny.tag} or \code{character(1)}) -The footer of the app.} - \item{modules}{(\code{list} or \code{teal_modules} or \code{teal_module}) nested list of \code{teal_modules} or \code{teal_module} objects or a single \code{teal_modules} or \code{teal_module} object. These are the specific output modules which @@ -38,27 +27,26 @@ more details.} Specifies the initial filter using \code{\link[=teal_slices]{teal_slices()}}.} } \value{ -A \code{reactiveVal} which is set to: +A \code{reactive} which returns: \itemize{ \item \code{teal_data} when the object is validated -\item \code{NULL} when not validated. -Important: \code{srv_data} suppress validate messages and returns \code{NULL} so that \code{srv_teal} can -stop the reactive cycle as \code{observeEvent} calls based on the data have \code{ignoreNULL = TRUE}. +\item \code{shiny.silent.error} when not validated. } } \description{ -Fundamental data class for teal is \code{\link[teal.data:teal_data]{teal.data::teal_data()}}. Data can be -passed in multiple ways: +Module handles \code{data} argument to the \code{srv_teal}. \code{teal} uses \code{\link[=teal_data]{teal_data()}} within +the whole framework and it could be provided in several way: \enumerate{ -\item Directly as a \code{\link[teal.data:teal_data]{teal.data::teal_data()}} object. +\item Directly as a \code{\link[teal.data:teal_data]{teal.data::teal_data()}} object. This will be automatically converted +to \code{reactive} \code{teal_data}. \item As a \code{reactive} object returning \code{\link[teal.data:teal_data]{teal.data::teal_data()}}. \href{#reactive-teal_data}{See section}. } } \section{Reactive \code{teal_data}}{ -\code{\link[teal.data:teal_data]{teal.data::teal_data()}} can change depending on the reactive context and \code{srv_teal} will rebuild -the app accordingly. There are two ways of interacting with the data: +Data included to the application can be reactively changed and \code{\link[=srv_teal]{srv_teal()}} will rebuild +the content respectively. There are two ways of making interactive \code{teal_data}: \enumerate{ \item Using a \code{reactive} object passed from outside the \code{teal} application. In this case, reactivity is controlled by external module and \code{srv_teal} will trigger accordingly to the changes. @@ -71,7 +59,7 @@ both scenarios (1) and (2) are having the same effect for the reactivity of a \c The difference is that in the first case the data is controlled from outside the app and in the second case the data is controlled from custom module called inside of the app. -see \code{\link{validate_reactive_teal_data}} for more details. +see \code{\link{module_teal_data}} for more details. } \keyword{internal} diff --git a/man/module_snapshot_manager.Rd b/man/module_snapshot_manager.Rd index 723fcd3ccb..64f490717a 100644 --- a/man/module_snapshot_manager.Rd +++ b/man/module_snapshot_manager.Rd @@ -109,4 +109,3 @@ Then that snapshot, and the previous snapshot history are dumped into the \code{ \author{ Aleksander Chlebowski } -\keyword{internal} diff --git a/man/module_teal.Rd b/man/module_teal.Rd index 28ce70232e..8ef2843fc0 100644 --- a/man/module_teal.Rd +++ b/man/module_teal.Rd @@ -48,21 +48,23 @@ Specifies the initial filter using \code{\link[=teal_slices]{teal_slices()}}.} Returns a \code{reactive} expression which returns the currently active module. } \description{ -This module is a central point of the \code{teal} app. It is called by \code{\link[=init]{init()}} but can be also -used as a standalone module in your custom application. It is responsible for creating the main -\code{shiny} app layout and initializing all the necessary components: -\itemize{ -\item \code{\link{module_data}} - for handling the \code{data}. -\item \code{\link{module_teal_module}} - for handling the \code{modules}. -\item \code{\link{module_filter_manager}} - for handling the \code{filter}. -\item \code{\link{module_snapshot_manager}} - for handling the \code{snapshots}. -\item \code{\link{module_bookmark_manager}} - for handling the \code{bookmarks}. +Module to create a \code{teal} app. This module (\code{ui} and \code{server}) is called directly by \code{\link[=init]{init()}} after +initial argument checking and setting default values. This module can be called directly and +included in your custom application. +Module is responsible for creating the main \code{shiny} app layout and initializing all the necessary +components. This module establishes reactive connection between the input \code{data} and every other +component in the app. Reactive change of the \code{data} triggers reload of the app and possibly +keeping all inputs settings the same so the user can continue where one left off. } +\section{data flow in \code{teal} application}{ + +\code{teal} supports multiple data inputs (see \code{data} in \code{\link{module_init_data}}) but eventually, they are +all converted to \code{reactive} returning \code{teal_data} in \code{module_teal}. There are several operations +on this \verb{reactive teal_data} object: +\itemize{ +\item data loading in \code{\link{module_init_data}} +\item data filtering in \code{\link{module_filter_data}} +\item data transformation in \code{\link{module_transform_data}} } -\details{ -This module establishes reactive connection between the \code{data} and every other component in the app. -Reactive change of the \code{data} triggers reload of the app and possibly keeping all inputs settings -the same so the user can continue where one left off. -Similar applies to \code{\link{module_bookmark_manager}} which allows to start a new session with restored -inputs. } + diff --git a/man/module_teal_data.Rd b/man/module_teal_data.Rd new file mode 100644 index 0000000000..abcbe048ff --- /dev/null +++ b/man/module_teal_data.Rd @@ -0,0 +1,72 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/module_teal_data.R +\name{module_teal_data} +\alias{module_teal_data} +\alias{ui_teal_data} +\alias{srv_teal_data} +\alias{ui_validate_reactive_teal_data} +\alias{srv_validate_reactive_teal_data} +\title{Execute and validate \code{teal_data_module}} +\usage{ +ui_teal_data(id, data_module) + +srv_teal_data( + id, + data, + data_module, + modules = NULL, + validate_shiny_silent_error = TRUE +) + +ui_validate_reactive_teal_data(id) + +srv_validate_reactive_teal_data( + id, + data, + modules = NULL, + validate_shiny_silent_error = FALSE +) +} +\arguments{ +\item{id}{(\code{character(1)}) Module id} + +\item{data_module}{(\code{teal_data_module})} + +\item{data}{(\verb{reactive teal_data})} + +\item{modules}{(\code{teal_modules} or \code{teal_module}) For \code{datanames} validation purpose} + +\item{validate_shiny_silent_error}{(\code{logical}) If \code{TRUE}, then \code{shiny.silent.error} is validated and +error message is displayed. +Default is \code{FALSE} to handle empty reactive cycle on \code{init}.} +} +\value{ +\code{reactive} \code{teal_data} +} +\description{ +This is a low level module to handle \code{teal_data_module} execution and validation. +\code{\link[=teal_transform_module]{teal_transform_module()}} inherits from \code{\link[=teal_data_module]{teal_data_module()}} so it is handled by this module too. +\code{\link[=srv_teal]{srv_teal()}} accepts various \code{data} objects and eventually they are all transformed to \code{reactive} +\code{\link[=teal_data]{teal_data()}} which is a standard data class in whole \code{teal} framework. +} +\section{data validation}{ + + +Executed \code{\link[=teal_data_module]{teal_data_module()}} is validated and output is validated for consistency. +Output \code{data} is invalid if: +\enumerate{ +\item \code{\link[=teal_data_module]{teal_data_module()}} is invalid if server doesn't return \code{reactive}. \strong{Immediately crashes an app!} +\item \code{reactive} throws a \code{shiny.error} - happens when module creating \code{\link[=teal_data]{teal_data()}} fails. +\item \code{reactive} returns \code{qenv.error} - happens when \code{\link[=teal_data]{teal_data()}} evaluates a failing code. +\item \code{reactive} object doesn't return \code{\link[=teal_data]{teal_data()}}. +\item \code{\link[=teal_data]{teal_data()}} object lacks any \code{datanames} specified in the \code{modules} argument. +} + +\code{teal} (observers in \code{srv_teal}) always waits to render an app until \code{reactive} \code{teal_data} is +returned. If error 2-4 occurs, relevant error message is displayed to app user and after issue is +resolved app will continue to run. \code{teal} guarantees that errors in a data don't crash an app +(except error 1). This is possible thanks to \code{.fallback_on_failure} which returns input-data +when output-data fails +} + +\keyword{internal} diff --git a/man/module_teal_data_module.Rd b/man/module_teal_data_module.Rd deleted file mode 100644 index aac929f290..0000000000 --- a/man/module_teal_data_module.Rd +++ /dev/null @@ -1,58 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/1.0_module_teal_transform.R -\name{module_teal_data_module} -\alias{module_teal_data_module} -\alias{ui_teal_data_modules} -\alias{srv_teal_data_modules} -\alias{ui_teal_data_module} -\alias{srv_teal_data_module} -\title{\code{teal_data} transform/load module} -\usage{ -ui_teal_data_modules(id, transformers, class = "") - -srv_teal_data_modules(id, data, transformers, modules) - -ui_teal_data_module(id, transformer) - -srv_teal_data_module( - id, - data, - transformer, - modules = NULL, - validate_shiny_silent_error = TRUE -) -} -\arguments{ -\item{id}{(\code{character(1)}) Module id} - -\item{transformers, transformer}{(\verb{list of teal_data_module} or \code{teal_data_module})} - -\item{class}{(\code{character}) Additional CSS class for whole wrapper div (optional)} - -\item{data}{(\verb{reactive teal_data})} - -\item{modules}{(\code{teal_modules} or \code{teal_module}) For \code{datanames} validation purpose} - -\item{validate_shiny_silent_error}{(\code{logical(1)})} -} -\value{ -\code{reactive} \code{teal_data} -} -\description{ -Module consumes \code{teal_data_module} elements and returns validated data: -\itemize{ -\item \code{srv/ui_teal_data_module}: executes a single \code{teal_data_module} -\item \code{srv/ui_teal_data_modules} executes multiple \code{teal_data_module} elements successively by passing -output of previous module to the next one. -} -} -\details{ -This is a low level module to handle data-loading or data-transformation as in both cases output -is a reactive and validated \code{teal_data}. Data loading can be considered as transformation module -of empty (initial) data object. - -Output \code{reactive} \code{teal_data} is validated by \code{\link{validate_reactive_teal_data}}. -Module makes sure that returned data doesn't break an app, so the \code{\link[=.fallback_on_failure]{.fallback_on_failure()}} is -implemented. -} -\keyword{internal} diff --git a/man/module_teal_module.Rd b/man/module_teal_module.Rd index c60f520fe2..240199607a 100644 --- a/man/module_teal_module.Rd +++ b/man/module_teal_module.Rd @@ -97,25 +97,16 @@ calling this function on it. \code{srv_teal_module} returns a reactive which returns the active module that corresponds to the selected tab. } \description{ -Create a UI of nested tabs of \code{teal_modules} -} -\section{\code{ui_teal_module}}{ - -Each \code{teal_modules} is translated to a \code{bslib::navset_tab} and each +On the UI side each \code{teal_modules} is translated to a \code{tabsetPanel} and each of its children is another tab-module called recursively. The UI of a \code{teal_module} is obtained by calling its UI function. - -The \code{datasets} argument is required to resolve the \code{teal} arguments in an -isolated context (with respect to reactivity). } - -\section{\code{srv_teal_module}}{ - -This module recursively calls all elements of \code{modules} and returns currently active one. +\details{ +On the server side module recursively calls all elements of \code{modules} and returns currently +active one. \itemize{ \item \code{teal_module} returns self as a active module. \item \code{teal_modules} also returns module active within self which is determined by the \code{input$active_tab}. } } - \keyword{internal} diff --git a/man/module_transform_data.Rd b/man/module_transform_data.Rd new file mode 100644 index 0000000000..49cd47daa4 --- /dev/null +++ b/man/module_transform_data.Rd @@ -0,0 +1,27 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/module_transform_data.R +\name{module_transform_data} +\alias{module_transform_data} +\alias{ui_transform_data} +\alias{srv_transform_data} +\title{Module to transform \code{reactive} \code{teal_data}} +\usage{ +ui_transform_data(id, transforms, class = "well") + +srv_transform_data(id, data, transforms, modules) +} +\arguments{ +\item{id}{(\code{character(1)}) Module id} + +\item{data}{(\verb{reactive teal_data})} + +\item{modules}{(\code{teal_modules} or \code{teal_module}) For \code{datanames} validation purpose} +} +\value{ +\code{reactive} \code{teal_data} +} +\description{ +Module calls multiple \link{module_teal_data} in sequence so that \code{reactive }teal_data` output +from one module is handed over to the following module's input. +} +\keyword{internal} diff --git a/man/tdata_deprecation.Rd b/man/tdata_deprecation.Rd index 5f0b006de2..0b925f9968 100644 --- a/man/tdata_deprecation.Rd +++ b/man/tdata_deprecation.Rd @@ -24,7 +24,10 @@ use this function to downgrade the \code{data} argument. } \examples{ td <- teal_data() -td <- within(td, iris <- iris) \%>\% within(mtcars <- mtcars) +td <- within( + within(td, iris <- iris), + mtcars <- mtcars +) td as_tdata(td) as_tdata(reactive(td)) diff --git a/man/teal_modules.Rd b/man/teal_modules.Rd index 6788c27be0..a89473e403 100644 --- a/man/teal_modules.Rd +++ b/man/teal_modules.Rd @@ -19,7 +19,7 @@ module( datanames = "all", server_args = NULL, ui_args = NULL, - transformers = NULL + transformers = list() ) modules(..., label = "root") @@ -73,7 +73,7 @@ a subset of datasets which are appended to the \code{data} argument in server fu \item{ui_args}{(named \code{list}) with additional arguments passed on to the UI function.} -\item{transformers}{(\code{list}) with \code{teal_data_module} that will be applied to transform the data. +\item{transformers}{(\code{list} of \code{teal_data_module}) that will be applied to transform the data. Each transform module UI will appear in the \code{teal} application, unless the \code{custom_ui} attribute is set on the list. If so, the module developer is responsible to display the UI in the module itself. diff --git a/man/teal_transform_module.Rd b/man/teal_transform_module.Rd new file mode 100644 index 0000000000..cc8f13ae52 --- /dev/null +++ b/man/teal_transform_module.Rd @@ -0,0 +1,61 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/teal_data_module.R +\name{teal_transform_module} +\alias{teal_transform_module} +\title{Data module for \code{teal} transformers.} +\usage{ +teal_transform_module(ui, server, label = "transform module") +} +\arguments{ +\item{ui}{(\verb{function(id)}) +\code{shiny} module UI function; must only take \code{id} argument} + +\item{server}{(\verb{function(id, data)}) +\code{shiny} module server function; that takes \code{id} and \code{data} argument, +where the \code{id} is the module id and \code{data} is the reactive \code{teal_data} input. +The server function must return reactive expression containing \code{teal_data} object.} + +\item{label}{(\code{character(1)}) Label of the module.} +} +\description{ +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} + +Create a \code{teal_data_module} object for custom transformation of data for pre-processing +before passing the data into the module. +} +\details{ +\code{teal_transform_module} creates a \code{teal_data_module} object to transform data in a \code{teal} +application. This transformation happens after the data has passed through the filtering activity +in teal. The transformed data is then sent to the server of the \code{\link[=teal_module]{teal_module()}}. + +See vignette \code{vignette("data-transform-as-shiny-module", package = "teal")} for more details. +} +\examples{ +my_transformers <- list( + teal_transform_module( + label = "Custom transform for iris", + ui = function(id) { + ns <- NS(id) + tags$div( + numericInput(ns("n_rows"), "Subset n rows", value = 6, min = 1, max = 150, step = 1) + ) + }, + server = function(id, data) { + moduleServer(id, function(input, output, session) { + reactive({ + within(data(), + { + iris <- head(iris, num_rows) + }, + num_rows = input$n_rows + ) + }) + }) + } + ) +) + +} +\seealso{ +\code{\link{teal_data_module}}, \code{\link[=teal_data_module]{teal_data_module()}} +} diff --git a/man/validate_reactive_teal_data.Rd b/man/validate_reactive_teal_data.Rd deleted file mode 100644 index 11b0596ea3..0000000000 --- a/man/validate_reactive_teal_data.Rd +++ /dev/null @@ -1,58 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/1.0_validate_reactive_teal_data.R -\name{validate_reactive_teal_data} -\alias{validate_reactive_teal_data} -\alias{ui_validate_reactive_teal_data} -\alias{srv_validate_reactive_teal_data} -\title{Validate reactive \code{teal_data}} -\usage{ -ui_validate_reactive_teal_data(id) - -srv_validate_reactive_teal_data( - id, - data, - modules = NULL, - validate_shiny_silent_error = FALSE -) -} -\arguments{ -\item{id}{(\code{character}) optional -string specifying the \code{shiny} module id in cases it is used as a \code{shiny} module -rather than a standalone \code{shiny} app. This is a legacy feature.} - -\item{data}{(\code{teal_data}, \code{teal_data_module} or \code{reactive} returning \code{teal_data})} - -\item{modules}{(\code{list} or \code{teal_modules} or \code{teal_module}) -nested list of \code{teal_modules} or \code{teal_module} objects or a single -\code{teal_modules} or \code{teal_module} object. These are the specific output modules which -will be displayed in the \code{teal} application. See \code{\link[=modules]{modules()}} and \code{\link[=module]{module()}} for -more details.} - -\item{validate_shiny_silent_error}{(\code{logical}) If \code{TRUE}, then \code{shiny.silent.error} is validated and -error message is displayed. -Default is \code{FALSE} to handle empty reactive cycle on \code{init}.} -} -\value{ -(\code{reactive} returning \code{teal_data}) -} -\description{ -Validate reactive \code{teal_data} -} -\section{data validation}{ - -\code{data} is invalid if: -\itemize{ -\item \code{\link[=teal_data_module]{teal_data_module()}} is invalid if server doesn't return \code{reactive}. -\item \code{reactive} throws a \code{shiny.error} - happens when module creating \code{\link[=teal_data]{teal_data()}} fails. -\item \code{reactive} returns \code{qenv.error} - happens when \code{\link[=teal_data]{teal_data()}} evaluates a failing code. -\item \code{reactive} object doesn't return \code{\link[=teal_data]{teal_data()}}. -\item \code{\link[=teal_data]{teal_data()}} object lacks any \code{datanames} specified in the \code{modules} argument. -} - -Any errors or warnings are displayed in the app pointing out to the reason of failure. -In all above, reactive cycle is halted and \code{teal} doesn't continue sending data further. On \code{init}, -halting reactive cycle stops an app load, while on subsequent reactive cycles, data just remains -unchanged and user is able to continue using the app. -} - -\keyword{internal} diff --git a/tests/testthat/test-module_teal.R b/tests/testthat/test-module_teal.R index ea181ab08a..fd01a8e824 100644 --- a/tests/testthat/test-module_teal.R +++ b/tests/testthat/test-module_teal.R @@ -19,7 +19,7 @@ is_slices_equivalent <<- function(x, y, with_attrs = TRUE) { } transform_list <<- list( - fail = teal_data_module( + fail = teal_transform_module( ui = function(id) NULL, server = function(id, data) { moduleServer(id, function(input, output, session) { @@ -29,7 +29,7 @@ transform_list <<- list( }) } ), - iris = teal_data_module( + iris = teal_transform_module( ui = function(id) NULL, server = function(id, data) { moduleServer(id, function(input, output, session) { @@ -39,7 +39,7 @@ transform_list <<- list( }) } ), - mtcars = teal_data_module( + mtcars = teal_transform_module( ui = function(id) NULL, server = function(id, data) { moduleServer(id, function(input, output, session) { @@ -49,7 +49,7 @@ transform_list <<- list( }) } ), - add_dataset = teal_data_module( + add_dataset = teal_transform_module( ui = function(id) NULL, server = function(id, data) { moduleServer(id, function(input, output, session) { @@ -577,7 +577,7 @@ testthat::describe("srv_teal teal_modules", { label = "module_1", server = function(id, data) data, transformers = list( - teal_data_module( + teal_transform_module( label = "Dummy", ui = function(id) div("(does nothing)"), server = function(id, data) { @@ -616,7 +616,7 @@ testthat::describe("srv_teal teal_modules", { label = "module_1", server = function(id, data) data, transformers = list( - teal_data_module( + teal_transform_module( label = "Dummy", ui = function(id) div("(does nothing)"), server = function(id, data) { @@ -653,7 +653,7 @@ testthat::describe("srv_teal teal_modules", { label = "module_1", server = function(id, data) data, transformers = list( - teal_data_module( + teal_transform_module( label = "Dummy", ui = function(id) div("(does nothing)"), server = function(id, data) { @@ -685,7 +685,7 @@ testthat::describe("srv_teal teal_modules", { label = "module_1", server = function(id, data) data, transformers = list( - teal_data_module( + teal_transform_module( label = "Dummy", ui = function(id) div("(does nothing)"), server = function(id, data) { @@ -1477,7 +1477,7 @@ testthat::describe("srv_teal teal_module(s) transformer", { module( server = function(id, data) data, transformers = list( - teal_data_module( + teal_transform_module( ui = function(id) NULL, server = function(id, data) "whatever" ) @@ -1503,7 +1503,7 @@ testthat::describe("srv_teal teal_module(s) transformer", { label = "module_1", server = function(id, data) data, transformers = list( - teal_data_module( + teal_transform_module( ui = function(id) NULL, server = function(id, data) { reactive(validate(need(FALSE, "my error"))) @@ -1532,7 +1532,7 @@ testthat::describe("srv_teal teal_module(s) transformer", { label = "module_1", server = function(id, data) data, transformers = list( - teal_data_module( + teal_transform_module( ui = function(id) NULL, server = function(id, data) { reactive(validate(need(FALSE, "my error"))) diff --git a/tests/testthat/test-shinytest2-data_summary.R b/tests/testthat/test-shinytest2-data_summary.R index 7a0db384a3..9cff06298b 100644 --- a/tests/testthat/test-shinytest2-data_summary.R +++ b/tests/testthat/test-shinytest2-data_summary.R @@ -91,8 +91,9 @@ testthat::test_that( testthat::skip_if_not_installed("MultiAssayExperiment") skip_if_too_deep(5) - data <- teal.data::teal_data() %>% - within({ + data <- within( + teal.data::teal_data(), + { mtcars1 <- mtcars mtcars2 <- data.frame(am = c(0, 1), test = c("a", "b")) iris <- iris @@ -103,7 +104,8 @@ testthat::test_that( factors <- names(Filter(isTRUE, vapply(CO2, is.factor, logical(1L)))) CO2[factors] <- lapply(CO2[factors], as.character) # nolint end: object_name. - }) + } + ) teal.data::join_keys(data) <- teal.data::join_keys( teal.data::join_key("mtcars2", "mtcars1", keys = c("am")) diff --git a/tests/testthat/test-shinytest2-init.R b/tests/testthat/test-shinytest2-init.R index 9f57bfa904..7bb63d17b8 100644 --- a/tests/testthat/test-shinytest2-init.R +++ b/tests/testthat/test-shinytest2-init.R @@ -85,9 +85,10 @@ testthat::test_that("e2e: init creates UI containing specified title, favicon, h app_title ) testthat::expect_equal( - app$get_html_rvest("head > link[rel='icon']") %>% - rvest::html_elements("link") %>% - rvest::html_attr("href"), + rvest::html_attr( + rvest::html_elements(app$get_html_rvest("head > link[rel='icon']"), "link"), + "href" + ), app_favicon ) testthat::expect_match( diff --git a/tests/testthat/test-shinytest2-modules.R b/tests/testthat/test-shinytest2-modules.R index c51b79de01..b966a7f204 100644 --- a/tests/testthat/test-shinytest2-modules.R +++ b/tests/testthat/test-shinytest2-modules.R @@ -81,9 +81,10 @@ testthat::test_that("e2e: filter panel is not displayed when datanames is NULL", testthat::expect_true( is.na( - app$get_html_rvest(".teal_secondary_col") %>% - rvest::html_element("div") %>% - rvest::html_attr("style") + rvest::html_attr( + rvest::html_element(app$get_html_rvest(".teal_secondary_col"), "div"), + "style" + ) ) ) diff --git a/tests/testthat/test-shinytest2-reporter.R b/tests/testthat/test-shinytest2-reporter.R index e1fd4f76a6..b0f72a39bc 100644 --- a/tests/testthat/test-shinytest2-reporter.R +++ b/tests/testthat/test-shinytest2-reporter.R @@ -5,8 +5,7 @@ testthat::test_that("e2e: reporter tab is created when a module has reporter", { modules = report_module(label = "Module with Reporter") ) - teal_tabs <- app$get_html_rvest(selector = "#teal-teal_modules-active_tab") %>% - rvest::html_elements("a") + teal_tabs <- rvest::html_elements(app$get_html_rvest(selector = "#teal-teal_modules-active_tab"), "a") tab_names <- setNames( rvest::html_attr(teal_tabs, "data-value"), rvest::html_text(teal_tabs) @@ -25,8 +24,10 @@ testthat::test_that("e2e: reporter tab is not created when a module has no repor data = simple_teal_data(), modules = example_module(label = "Example Module") ) - teal_tabs <- app$get_html_rvest(selector = "#teal-teal_modules-active_tab") %>% - rvest::html_elements("a") + teal_tabs <- rvest::html_elements( + app$get_html_rvest(selector = "#teal-teal_modules-active_tab"), + "a" + ) tab_names <- setNames( rvest::html_attr(teal_tabs, "data-value"), rvest::html_text(teal_tabs) diff --git a/tests/testthat/test-shinytest2-show-rcode.R b/tests/testthat/test-shinytest2-show-rcode.R index 949e0ba825..fe9394f46f 100644 --- a/tests/testthat/test-shinytest2-show-rcode.R +++ b/tests/testthat/test-shinytest2-show-rcode.R @@ -30,20 +30,16 @@ testthat::test_that("e2e: teal app initializes with Show R Code modal", { ) # Check for Copy buttons. testthat::expect_equal( - app$active_module_element("rcode-copy_button1") %>% - app$get_text(), + app$get_text(app$active_module_element("rcode-copy_button1")), "Copy to Clipboard" ) testthat::expect_equal( - app$active_module_element("rcode-copy_button2") %>% - app$get_text(), + app$get_text(app$active_module_element("rcode-copy_button2")), "Copy to Clipboard" ) # Check R code output. - r_code <- - app$active_module_element("rcode-verbatim_content") %>% - app$get_text() + r_code <- app$get_text(app$active_module_element("rcode-verbatim_content")) testthat::expect_match(r_code, "iris <- iris", fixed = TRUE) testthat::expect_match(r_code, "iris_raw <- iris", fixed = TRUE) diff --git a/tests/testthat/test-shinytest2-utils.R b/tests/testthat/test-shinytest2-utils.R index a1ae80be5b..eb9709af88 100644 --- a/tests/testthat/test-shinytest2-utils.R +++ b/tests/testthat/test-shinytest2-utils.R @@ -6,8 +6,7 @@ testthat::test_that("e2e: show/hide hamburger works as expected", { ) get_class_attributes <- function(app, selector) { - element <- app$get_html_rvest(selector = selector) %>% - rvest::html_elements(selector) + element <- rvest::html_elements(app$get_html_rvest(selector = selector), selector) list( class = rvest::html_attr(element, "class"), style = rvest::html_attr(element, "style") diff --git a/vignettes/adding-support-for-reporting.Rmd b/vignettes/adding-support-for-reporting.Rmd index f5ebb24d16..91d3c2bc40 100644 --- a/vignettes/adding-support-for-reporting.Rmd +++ b/vignettes/adding-support-for-reporting.Rmd @@ -31,7 +31,7 @@ The entire life cycle of objects involved in creating the report and configuring Let us consider an example module, based on the example module from `teal`: ```{r, message=FALSE} library(teal) -example_module <- function(label = "example teal module") { +my_module <- function(label = "example teal module") { module( label = label, server = function(id, data) { @@ -62,7 +62,7 @@ Using `teal`, you can launch this example module with the following: ```{r, eval = FALSE} app <- init( data = teal_data(IRIS = iris, MTCARS = mtcars), - modules = example_module() + modules = my_module() ) if (interactive()) shinyApp(app$ui, app$server) @@ -77,7 +77,7 @@ This informs `teal` that the module requires `reporter`, and it will be included See below: ```{r} -example_module_with_reporting <- function(label = "example teal module") { +my_module_with_reporting <- function(label = "example teal module") { module( label = label, server = function(id, data, reporter) { @@ -105,7 +105,7 @@ With these modifications, the module is now ready to be launched with `teal`: ```{r} app <- init( data = teal_data(IRIS = iris, MTCARS = mtcars), - modules = example_module_with_reporting() + modules = my_module_with_reporting() ) if (interactive()) shinyApp(app$ui, app$server) @@ -117,10 +117,10 @@ That requires inserting UI and server elements of the `teal.reporter` module int ### Insert `teal.reporter` module -The UI and the server logic necessary for adding cards from `example_module_with_reporting` to the report are provided by `teal.reporter::simple_reporter_ui` and `teal.reporter::simple_reporter_srv`. +The UI and the server logic necessary for adding cards from `my_module_with_reporting` to the report are provided by `teal.reporter::simple_reporter_ui` and `teal.reporter::simple_reporter_srv`. ```{r} -example_module_with_reporting <- function(label = "example teal module") { +my_module_with_reporting <- function(label = "example teal module") { module( label = label, server = function(id, data, reporter) { @@ -156,7 +156,7 @@ This updated module is now ready to be launched: ```{r} app <- init( data = teal_data(IRIS = iris, MTCARS = mtcars), - modules = example_module_with_reporting() + modules = my_module_with_reporting() ) if (interactive()) shinyApp(app$ui, app$server) @@ -182,7 +182,7 @@ custom_function <- function(card = teal.reporter::ReportCard$new()) { card } -example_module_with_reporting <- function(label = "example teal module") { +my_module_with_reporting <- function(label = "example teal module") { module( label = label, server = function(id, data, reporter) { @@ -216,7 +216,7 @@ example_module_with_reporting <- function(label = "example teal module") { ```{r} app <- init( data = teal_data(IRIS = iris, MTCARS = mtcars), - modules = example_module_with_reporting() + modules = my_module_with_reporting() ) if (interactive()) shinyApp(app$ui, app$server) @@ -353,7 +353,7 @@ app <- init( data = teal_data(AIR = airquality, IRIS = iris), modules = list( example_reporter_module(label = "with Reporter"), - example_module(label = "without Reporter") + my_module(label = "without Reporter") ), filter = teal_slices(teal_slice(dataname = "AIR", varname = "Temp", selected = c(72, 85))), header = "Example teal app with reporter" diff --git a/vignettes/bootstrap-themes-in-teal.Rmd b/vignettes/bootstrap-themes-in-teal.Rmd index 937e90212c..8ea255e4c6 100644 --- a/vignettes/bootstrap-themes-in-teal.Rmd +++ b/vignettes/bootstrap-themes-in-teal.Rmd @@ -75,7 +75,10 @@ The most important HTML tags in `teal` have a specific id or class, so they can ``` library(magrittr) -options("teal.bs_theme" = bslib::bs_theme(version = "5") %>% bslib::bs_add_rules("Anything understood by sass::as_sass()")) +options("teal.bs_theme" = bslib::bs_add_rules( + bslib::bs_theme(version = "5"), + "Anything understood by sass::as_sass()" +)) ``` Other `bslib::bs_add_*` family functions could be used to specify low-level Bootstrap elements. diff --git a/vignettes/creating-custom-modules.Rmd b/vignettes/creating-custom-modules.Rmd index 6353512e57..0d1cea359c 100644 --- a/vignettes/creating-custom-modules.Rmd +++ b/vignettes/creating-custom-modules.Rmd @@ -56,7 +56,7 @@ Note that dataset choices are specified by the `datanames` property of the `teal ```{r, message=FALSE} library(teal) -example_module <- function(label = "example teal module") { +my_module <- function(label = "example teal module") { checkmate::assert_string(label) module( @@ -149,10 +149,8 @@ srv_histogram_example <- function(id, data) { }) # view code - output$code <- renderPrint({ - plot_code_q() %>% - get_code() %>% - cat() + output$code <- renderText({ + get_code(plot_code_q()) }) }) } diff --git a/vignettes/data-transform-as-shiny-module.Rmd b/vignettes/data-transform-as-shiny-module.Rmd new file mode 100644 index 0000000000..c2b8dd591f --- /dev/null +++ b/vignettes/data-transform-as-shiny-module.Rmd @@ -0,0 +1,165 @@ +--- +title: "Data Transform as shiny Module" +author: "NEST CoreDev" +output: + rmarkdown::html_vignette: + toc: true +vignette: > + %\VignetteIndexEntry{Data Transform as shiny Module} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +## Introduction + +When data transformations need to occur inside the teal app, they typically happen within the teal module development by the teal module developer. This results in the transformation code being tightly coupled with the teal module. Any changes to the data transformation logic require updating the teal module, which can impact the teal app development until the module is updated. +With the new `teal_transform_module()` function, it is possible to provide a more dynamic data transformation capability that can be customized by the teal app developer, not just the teal module developer. +The `teal_transform_module()` function is essentially a Shiny module that takes `ui` and `server` arguments and can be provided to the `transformer` argument in a `module()`. When provided, teal will execute the data transformation logic for the specified module when it is loaded. +In addition, this approach makes it easier for teal app developers to handle additional data transformations while using the teal modules R packages like `teal.modules.general` and `teal.modules.clinical`. +Let’s explore this new way to manage custom data transformations in teal apps. + +## Creating your first custom data transformation module + +We initialize a simple teal app where we pass `iris` and `mtcars` as the input datasets. + +```{r, message = FALSE, warning = FALSE} +library(teal) +``` + +```{r} +data <- within(teal_data(), { + iris <- iris + mtcars <- mtcars +}) + +app <- init( + data = data, + modules = teal::example_module() +) + +if (interactive()) { + shinyApp(app$ui, app$server) +} +``` + +Lets create a simple `teal_transform_module` that returns the first n number of rows of `iris` based on the user input. + +We do this by creating the `ui` with the `numericInput` for the user to input the number of rows to be displayed. +In the `server` function we take in the reactive `data` and perform this transformation and return the new reactive `data`. + +```{r} +library(teal) +data <- within(teal_data(), { + iris <- iris + mtcars <- mtcars +}) +datanames(data) <- c("iris", "mtcars") + +my_transformers <- list( + teal_transform_module( + label = "Custom transform for iris", + ui = function(id) { + ns <- NS(id) + tags$div( + numericInput(ns("n_rows"), "Number of rows to subset", value = 6, min = 1, max = 150, step = 1) + ) + }, + server = function(id, data) { + moduleServer(id, function(input, output, session) { + reactive({ + within(data(), + { + iris <- head(iris, num_rows) + }, + num_rows = input$n_rows + ) + }) + }) + } + ) +) + +app <- init( + data = data, + modules = teal::example_module(transformers = my_transformers) +) + +if (interactive()) { + shinyApp(app$ui, app$server) +} +``` + +Note that we can add multiple teal transforms by adding calling `teal_transform_module` in a list and passing the list of teal transforms to the `transformers` argument of the module call. + +Lets add another transformation to the `mtcars` dataset to create a column with the `rownames` of `mtcars`. +Also, note that this module does not have interactive UI elements. + +```{r} +library(teal) +data <- within(teal_data(), { + iris <- iris + mtcars <- mtcars +}) +datanames(data) <- c("iris", "mtcars") + +my_transformers <- list( + teal_transform_module( + label = "Custom transform for iris", + ui = function(id) { + ns <- NS(id) + tags$div( + numericInput(ns("n_rows"), "Number of rows to subset", value = 6, min = 1, max = 150, step = 1) + ) + }, + server = function(id, data) { + moduleServer(id, function(input, output, session) { + reactive({ + within(data(), + { + iris <- head(iris, num_rows) + }, + num_rows = input$n_rows + ) + }) + }) + } + ), + teal_transform_module( + label = "Custom transform for mtcars", + ui = function(id) { + ns <- NS(id) + tags$div( + "Adding rownames column to mtcars" + ) + }, + server = function(id, data) { + moduleServer(id, function(input, output, session) { + reactive({ + within(data(), { + mtcars$rownames <- rownames(mtcars) + rownames(mtcars) <- NULL + }) + }) + }) + } + ) +) + +app <- init( + data = data, + modules = teal::example_module(transformers = my_transformers) +) + +if (interactive()) { + shinyApp(app$ui, app$server) +} +``` + +## Custom placement of the transform UI + +When a custom transformation is used, the UI for the transformation is placed below the filter panel. +However, there is a way to customize the placement of the UI inside the module content. + +This can only be done when the module allocates a dedicated space for the transform UI. + +Let's create a custom module that has a dedicated space for the transform UI inside the module content. diff --git a/vignettes/including-data-in-teal-applications.Rmd b/vignettes/including-data-in-teal-applications.Rmd index 723210cc07..16f33f7e22 100644 --- a/vignettes/including-data-in-teal-applications.Rmd +++ b/vignettes/including-data-in-teal-applications.Rmd @@ -189,12 +189,11 @@ For convenience, an empty `datanames` property is considered to mean "all object ```{r} data_with_objects <- teal_data(iris = iris, cars = mtcars) -data_with_code <- teal_data() %>% - within({ - iris <- iris - cars <- mtcars - not_a_dataset <- "data source credits" - }) +data_with_code <- within(teal_data(), { + iris <- iris + cars <- mtcars + not_a_dataset <- "data source credits" +}) datanames(data_with_objects) datanames(data_with_code) datanames(data_with_code) <- c("iris", "cars")