From bf4039300ea0c85ae194c161f36e564aa1db393b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ka=C5=82=C4=99dkowski?= <6959016+gogonzo@users.noreply.github.com> Date: Thu, 5 Sep 2024 09:04:35 +0200 Subject: [PATCH 01/18] datanames slot in `teal_transform_module` (ignore `@datanames`) (#1334) ignore `datanames(data)` --- NAMESPACE | 1 + NEWS.md | 5 +- R/dummy_functions.R | 2 +- R/init.R | 10 +- R/module_data_summary.R | 2 +- R/module_filter_data.R | 13 +- R/module_init_data.R | 6 +- R/module_nested_tabs.R | 2 +- R/modules.R | 77 +++++++- R/reporter_previewer_module.R | 2 +- R/teal_data_module.R | 11 +- R/teal_data_utils.R | 16 +- R/utils.R | 15 +- man/dot-get_hashes_code.Rd | 2 +- man/example_module.Rd | 11 +- man/teal_data_to_filtered_data.Rd | 2 +- man/teal_data_utilities.Rd | 3 - man/teal_modules.Rd | 50 ++++- man/teal_transform_module.Rd | 13 +- tests/testthat/test-module_teal.R | 178 +++++------------- tests/testthat/test-modules.R | 61 ++++++ tests/testthat/test-utils.R | 19 -- .../including-data-in-teal-applications.Rmd | 32 ---- 23 files changed, 278 insertions(+), 255 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index edaaa6a136..a42856456d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -28,6 +28,7 @@ export(modules) export(new_tdata) export(report_card_template) export(reporter_previewer_module) +export(set_datanames) export(show_rcode_modal) export(srv_teal) export(srv_teal_with_splash) diff --git a/NEWS.md b/NEWS.md index 84314f235b..b99a176120 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,10 +6,13 @@ * Introduced `teal_transform_module` to provide a way to interactively modify data delivered to `teal_module`'s `server`. #1228 * Introduced a new argument `once = FALSE` in `teal_data_module` to possibly reload data during a run time. * Possibility to download lockfile to restore app session for reproducibility. #479 - +* Introduced a function `set_datanames()` to change a `datanames` of the `teal_module`. +* Datasets which name starts with `.` are ignored when `module`'s `datanames` is set as `"all"`. ### Breaking changes +* Setting `datanames()` on `data` passed to teal application no longer has effect. In order to change `teal_module`'s +`datanames` one should modify `module$datanames`. * The `landing_popup_module()` needs to be passed as the `landing_popup` argument of `init` instead of being passed as a module of the `modules` argument of `init`. * `teal` no longer re-export `%>%`. Please load `library(magrittr)` instead or use `|>` from `base`. diff --git a/R/dummy_functions.R b/R/dummy_functions.R index d4071c6fbc..7e6765dfbc 100644 --- a/R/dummy_functions.R +++ b/R/dummy_functions.R @@ -21,7 +21,7 @@ example_module <- function(label = "example teal module", datanames = "all", tra checkmate::assert_class(isolate(data()), "teal_data") moduleServer(id, function(input, output, session) { datanames_rv <- reactive({ - teal.data::datanames(req(data())) + .teal_data_ls(req(data())) }) observeEvent(datanames_rv(), { diff --git a/R/init.R b/R/init.R index 2cf058780d..805bf1ee2e 100644 --- a/R/init.R +++ b/R/init.R @@ -210,20 +210,16 @@ init <- function(data, ## `data` - `modules` if (inherits(data, "teal_data")) { - if (length(.teal_data_datanames(data)) == 0) { + if (length(.teal_data_ls(data)) == 0) { stop("The environment of `data` is empty.") } - if (!length(teal.data::datanames(data))) { - warning("`data` object has no datanames. Default datanames are set using `teal_data`'s environment.") - } - - is_modules_ok <- check_modules_datanames(modules, .teal_data_datanames(data)) + is_modules_ok <- check_modules_datanames(modules, .teal_data_ls(data)) if (!isTRUE(is_modules_ok) && length(unlist(extract_transformers(modules))) == 0) { lapply(is_modules_ok$string, warning, call. = FALSE) } - is_filter_ok <- check_filter_datanames(filter, .teal_data_datanames(data)) + is_filter_ok <- check_filter_datanames(filter, .teal_data_ls(data)) if (!isTRUE(is_filter_ok)) { warning(is_filter_ok) # we allow app to continue if applied filters are outside diff --git a/R/module_data_summary.R b/R/module_data_summary.R index 5152292341..407edc0aba 100644 --- a/R/module_data_summary.R +++ b/R/module_data_summary.R @@ -63,7 +63,7 @@ srv_data_summary <- function(id, teal_data) { summary_table <- reactive({ req(inherits(teal_data(), "teal_data")) - if (length(teal.data::datanames(teal_data())) == 0) { + if (!length(.teal_data_ls(teal_data()))) { return(NULL) } diff --git a/R/module_filter_data.R b/R/module_filter_data.R index bc210bc40f..20bf73dd6a 100644 --- a/R/module_filter_data.R +++ b/R/module_filter_data.R @@ -27,24 +27,25 @@ ui_filter_data <- function(id) { srv_filter_data <- function(id, datasets, active_datanames, data_rv, is_active) { assert_reactive(datasets) moduleServer(id, function(input, output, session) { + active_corrected <- reactive(intersect(active_datanames(), datasets()$datanames())) + output$panel <- renderUI({ req(inherits(datasets(), "FilteredData")) isolate({ # render will be triggered only when FilteredData object changes (not when filters change) # technically it means that teal_data_module needs to be refreshed logger::log_debug("srv_filter_panel rendering filter panel.") - if (length(active_datanames())) { - datasets()$srv_active("filters", active_datanames = active_datanames) - # todo: make sure to bump the `teal.slice` version. Please use the branch `669_insertUI@main` in `teal.slice`. - datasets()$ui_active(session$ns("filters"), active_datanames = active_datanames) + if (length(active_corrected())) { + datasets()$srv_active("filters", active_datanames = active_corrected) + datasets()$ui_active(session$ns("filters"), active_datanames = active_corrected) } }) }) - trigger_data <- .observe_active_filter_changed(datasets, is_active, active_datanames, data_rv) + trigger_data <- .observe_active_filter_changed(datasets, is_active, active_corrected, data_rv) eventReactive(trigger_data(), { - .make_filtered_teal_data(modules, data = data_rv(), datasets = datasets(), datanames = active_datanames()) + .make_filtered_teal_data(modules, data = data_rv(), datasets = datasets(), datanames = active_corrected()) }) }) } diff --git a/R/module_init_data.R b/R/module_init_data.R index 6511b3f5e2..7990a2bb0a 100644 --- a/R/module_init_data.R +++ b/R/module_init_data.R @@ -107,7 +107,7 @@ srv_init_data <- function(id, data, modules, filter = teal_slices()) { ) } - is_filter_ok <- check_filter_datanames(filter, .teal_data_datanames(data_validated())) + is_filter_ok <- check_filter_datanames(filter, .teal_data_ls(data_validated())) if (!isTRUE(is_filter_ok)) { showNotification( "Some filters were not applied because of incompatibility with data. Contact app developer.", @@ -160,7 +160,6 @@ srv_init_data <- function(id, data, modules, filter = teal_slices()) { ) tdata@verified <- data@verified - teal.data::datanames(tdata) <- teal.data::datanames(data) tdata } @@ -172,8 +171,7 @@ srv_init_data <- function(id, data, modules, filter = teal_slices()) { #' @return A character vector with the code lines. #' @keywords internal #' -.get_hashes_code <- function(data, datanames = .teal_data_datanames(data)) { - # todo: this should be based on data_rv object not on datasets +.get_hashes_code <- function(data, datanames = .teal_data_ls(data)) { vapply( datanames, function(dataname, datasets) { diff --git a/R/module_nested_tabs.R b/R/module_nested_tabs.R index 6a838c0d70..7bc3a72fbf 100644 --- a/R/module_nested_tabs.R +++ b/R/module_nested_tabs.R @@ -301,7 +301,7 @@ srv_teal_module.teal_module <- function(id, .resolve_module_datanames <- function(data, modules) { stopifnot("data_rv must be teal_data object." = inherits(data, "teal_data")) if (is.null(modules$datanames) || identical(modules$datanames, "all")) { - .teal_data_datanames(data) + .teal_data_ls(data) } else { intersect( include_parent_datanames(modules$datanames, teal.data::join_keys(data)), diff --git a/R/modules.R b/R/modules.R index 941e8baa6f..7e39e44b60 100644 --- a/R/modules.R +++ b/R/modules.R @@ -1,3 +1,6 @@ +setOldClass("teal_module") +setOldClass("teal_modules") + #' Create `teal_module` and `teal_modules` objects #' #' @description @@ -39,18 +42,17 @@ #' - `...` (optional) When provided, `ui_args` elements will be passed to the module named argument #' or to the `...`. #' @param filters (`character`) Deprecated. Use `datanames` instead. -#' @param datanames (`character`) A vector with `datanames` that are relevant for the item. The -#' filter panel will automatically update the shown filters to include only -#' filters in the listed datasets. `NULL` will hide the filter panel, -#' and the keyword `"all"` will show filters of all datasets. `datanames` also determines -#' a subset of datasets which are appended to the `data` argument in server function. +#' @param datanames (`character`) Names of the datasets that are relevant for the item. +#' The keyword `"all"` provides all datasets available in `data` passed to `teal` application. +#' `NULL` will hide the filter panel. #' @param server_args (named `list`) with additional arguments passed on to the server function. #' @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` 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. +#' If so, the module developer is responsible to display the UI in the module itself. `datanames` of the `transformers` +#' will be added to the `datanames`. #' #' When the transformation does not have sufficient input data, the resulting data will fallback #' to the last successful transform or, in case there are none, to the filtered data. @@ -58,6 +60,22 @@ #' - For `modules()`: (`teal_module` or `teal_modules`) Objects to wrap into a tab. #' - For `format()` and `print()`: Arguments passed to other methods. #' +#' @section `datanames`: +#' The module's `datanames` argument determines a subset of datasets from the `data` object, as specified in the +#' server function argument, to be presented in the module. Datasets displayed in the filter panel will be limited +#' to this subset. +#' When `datanames` is set to `"all"`, all available datasets in the `data` object are considered relevant for the +#' module. However, setting `datanames` argument to `"all"` might include datasets that are irrelevant for the module, +#' for example: +#' - Proxy variables used for modifying columns. +#' - Modified copies of datasets used to create a final dataset. +#' - Connection objects. +#' To prevent these irrelevant datasets from appearing in the module, use the [set_datanames()] function on the +#' [module] or [modules()] to change the `datanames` from `"all"` to specific dataset names. Attempting to change +#' `datanames` values that was not set to `"all"` using [set_datanames()] will be ignored with a warning. +#' +#' Additionally, datasets with names starting with `.` are ignored when `datanames` is set to `"all"`. +#' #' @return #' `module()` returns an object of class `teal_module`. #' @@ -127,7 +145,7 @@ #' @export #' module <- function(label = "module", - server = function(id, ...) moduleServer(id, function(input, output, session) NULL), + server = function(id, data, ...) moduleServer(id, function(input, output, session) NULL), ui = function(id, ...) tags$p(paste0("This module has no UI (id: ", id, " )")), filters, datanames = "all", @@ -241,14 +259,23 @@ module <- function(label = "module", } ## `transformers` - checkmate::assert_list(transformers, types = "teal_data_module") + if (inherits(transformers, "teal_transform_module")) { + transformers <- list(transformers) + } + checkmate::assert_list(transformers, types = "teal_transform_module") + transformer_datanames <- unlist(lapply(transformers, attr, "datanames")) + combined_datanames <- if (identical(datanames, "all") || identical(transformer_datanames, "all")) { + "all" + } else { + union(datanames, transformer_datanames) + } structure( list( label = label, server = server, ui = ui, - datanames = unique(datanames), + datanames = combined_datanames, server_args = server_args, ui_args = ui_args, transformers = transformers @@ -313,6 +340,38 @@ format.teal_modules <- function(x, indent = 0, ...) { ) } +#' @param modules (`teal_module` or `teal_modules`) +#' @rdname teal_modules +#' @examples +#' # change the module's datanames +#' set_datanames(module(datanames = "all"), "a") +#' +#' # change modules' datanames +#' set_datanames( +#' modules( +#' module(datanames = "all"), +#' module(datanames = "a") +#' ), +#' "b" +#' ) +#' @export +set_datanames <- function(modules, datanames) { + checkmate::assert_multi_class(modules, c("teal_modules", "teal_module")) + if (inherits(modules, "teal_modules")) { + modules$children <- lapply(modules$children, set_datanames, datanames) + } else { + if (identical(modules$datanames, "all")) { + modules$datanames <- datanames + } else { + warning( + "Not possible to modify datanames of the module ", modules$label, + ". set_datanames() can only change datanames if it was set to \"all\".", + call. = FALSE + ) + } + } + modules +} #' @rdname teal_modules #' @export diff --git a/R/reporter_previewer_module.R b/R/reporter_previewer_module.R index ba84f3173f..19b707b34f 100644 --- a/R/reporter_previewer_module.R +++ b/R/reporter_previewer_module.R @@ -41,7 +41,7 @@ reporter_previewer_module <- function(label = "Report previewer", server_args = ) # Module is created with a placeholder label and the label is changed later. # This is to prevent another module being labeled "Report previewer". - class(module) <- c("teal_module_previewer", class(module)) + class(module) <- c(class(module), "teal_module_previewer") module$label <- label attr(module, "teal_bookmarkable") <- TRUE module diff --git a/R/teal_data_module.R b/R/teal_data_module.R index 535516524d..8051b604b4 100644 --- a/R/teal_data_module.R +++ b/R/teal_data_module.R @@ -104,10 +104,15 @@ teal_data_module <- function(ui, server, label = "data module", once = TRUE) { #' `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. +#' @param datanames (`character`) +#' Names of the datasets that are relevant for the module. The +#' filter panel will only display filters for specified `datanames`. The keyword `"all"` will show +#' filters of all datasets. `datanames` will be automatically appended to the [modules()] `datanames`. #' @examples #' my_transformers <- list( #' teal_transform_module( #' label = "Custom transform for iris", +#' datanames = "iris", #' ui = function(id) { #' ns <- NS(id) #' tags$div( @@ -132,7 +137,10 @@ teal_data_module <- function(ui, server, label = "data module", once = TRUE) { #' @name teal_transform_module #' #' @export -teal_transform_module <- function(ui, server, label = "transform module") { +teal_transform_module <- function(ui = function(id) NULL, + server = function(id, data) data, + label = "transform module", + datanames = "all") { checkmate::assert_function(ui, args = "id", nargs = 1) checkmate::assert_function(server, args = c("id", "data"), nargs = 2) checkmate::assert_string(label) @@ -149,6 +157,7 @@ teal_transform_module <- function(ui, server, label = "transform module") { } ), label = label, + datanames = datanames, class = c("teal_transform_module", "teal_data_module") ) } diff --git a/R/teal_data_utils.R b/R/teal_data_utils.R index 31b624faf5..bb265e0fcd 100644 --- a/R/teal_data_utils.R +++ b/R/teal_data_utils.R @@ -65,22 +65,12 @@ NULL ) ) new_data@verified <- data@verified - teal.data::datanames(new_data) <- datanames_corrected + teal.data::datanames(new_data) <- datanames new_data } -#' @rdname teal_data_utilities -.teal_data_datanames <- function(data) { - checkmate::assert_class(data, "teal_data") - datanames <- teal.data::datanames(data) - if (length(datanames)) { - datanames - } else { - .teal_data_ls(data) - } -} - #' @rdname teal_data_utilities .teal_data_ls <- function(data) { - grep("._raw_", ls(teal.code::get_env(data), all.names = TRUE), value = TRUE, invert = TRUE) + checkmate::assert_class(data, "teal_data") + grep("._raw_", ls(teal.code::get_env(data), all.names = FALSE), value = TRUE, invert = TRUE) } diff --git a/R/utils.R b/R/utils.R index b674bce076..36cd81f564 100644 --- a/R/utils.R +++ b/R/utils.R @@ -45,16 +45,17 @@ get_teal_bs_theme <- function() { #' @noRd #' @keywords internal include_parent_datanames <- function(dataname, join_keys) { - parents <- character(0) + ordered_datanames <- dataname for (i in dataname) { + parents <- character(0) while (length(i) > 0) { parent_i <- teal.data::parent(join_keys, i) parents <- c(parent_i, parents) i <- parent_i } + ordered_datanames <- c(parents, dataname, ordered_datanames) } - - unique(c(parents, dataname)) + unique(ordered_datanames) } #' Create a `FilteredData` @@ -65,13 +66,15 @@ include_parent_datanames <- function(dataname, join_keys) { #' @param datanames (`character`) vector of data set names to include; must be subset of `datanames(x)` #' @return A `FilteredData` object. #' @keywords internal -teal_data_to_filtered_data <- function(x, datanames = .teal_data_datanames(x)) { +teal_data_to_filtered_data <- function(x, datanames = .teal_data_ls(x)) { checkmate::assert_class(x, "teal_data") checkmate::assert_character(datanames, min.chars = 1L, any.missing = FALSE) - # Otherwise, FilteredData will be created in the modules' scope later teal.slice::init_filtered_data( - x = sapply(datanames, function(dn) x[[dn]], simplify = FALSE), + x = Filter( + length, + sapply(datanames, function(dn) x[[dn]], simplify = FALSE) + ), join_keys = teal.data::join_keys(x) ) } diff --git a/man/dot-get_hashes_code.Rd b/man/dot-get_hashes_code.Rd index 6283d2dca9..07280ef587 100644 --- a/man/dot-get_hashes_code.Rd +++ b/man/dot-get_hashes_code.Rd @@ -4,7 +4,7 @@ \alias{.get_hashes_code} \title{Get code that tests the integrity of the reproducible data} \usage{ -.get_hashes_code(data, datanames = .teal_data_datanames(data)) +.get_hashes_code(data, datanames = .teal_data_ls(data)) } \arguments{ \item{data}{(\code{teal_data}) object holding the data} diff --git a/man/example_module.Rd b/man/example_module.Rd index 8227e44e45..26558f8650 100644 --- a/man/example_module.Rd +++ b/man/example_module.Rd @@ -14,15 +14,14 @@ example_module( \item{label}{(\code{character(1)}) Label shown in the navigation item for the module or module group. For \code{modules()} defaults to \code{"root"}. See \code{Details}.} -\item{datanames}{(\code{character}) A vector with \code{datanames} that are relevant for the item. The -filter panel will automatically update the shown filters to include only -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{datanames}{(\code{character}) Names of the datasets that are relevant for the item. +The keyword \code{"all"} provides all datasets available in \code{data} passed to \code{teal} application. +\code{NULL} will hide the filter panel.} \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. +If so, the module developer is responsible to display the UI in the module itself. \code{datanames} of the \code{transformers} +will be added to the \code{datanames}. When the transformation does not have sufficient input data, the resulting data will fallback to the last successful transform or, in case there are none, to the filtered data.} diff --git a/man/teal_data_to_filtered_data.Rd b/man/teal_data_to_filtered_data.Rd index d02e74ddfa..4d7d53e38e 100644 --- a/man/teal_data_to_filtered_data.Rd +++ b/man/teal_data_to_filtered_data.Rd @@ -4,7 +4,7 @@ \alias{teal_data_to_filtered_data} \title{Create a \code{FilteredData}} \usage{ -teal_data_to_filtered_data(x, datanames = .teal_data_datanames(x)) +teal_data_to_filtered_data(x, datanames = .teal_data_ls(x)) } \arguments{ \item{x}{(\code{teal_data}) object} diff --git a/man/teal_data_utilities.Rd b/man/teal_data_utilities.Rd index 4d306aad64..a9fc37eb9a 100644 --- a/man/teal_data_utilities.Rd +++ b/man/teal_data_utilities.Rd @@ -5,7 +5,6 @@ \alias{.append_evaluated_code} \alias{.append_modified_data} \alias{.subset_teal_data} -\alias{.teal_data_datanames} \alias{.teal_data_ls} \title{\code{teal_data} utils} \usage{ @@ -15,8 +14,6 @@ .subset_teal_data(data, datanames) -.teal_data_datanames(data) - .teal_data_ls(data) } \arguments{ diff --git a/man/teal_modules.Rd b/man/teal_modules.Rd index 8c45a74b6a..33865d976c 100644 --- a/man/teal_modules.Rd +++ b/man/teal_modules.Rd @@ -8,12 +8,14 @@ \alias{format.teal_module} \alias{print.teal_module} \alias{format.teal_modules} +\alias{set_datanames} \alias{print.teal_modules} \title{Create \code{teal_module} and \code{teal_modules} objects} \usage{ module( label = "module", - server = function(id, ...) moduleServer(id, function(input, output, session) NULL), + server = function(id, data, ...) moduleServer(id, function(input, output, session) + NULL), ui = function(id, ...) tags$p(paste0("This module has no UI (id: ", id, " )")), filters, datanames = "all", @@ -30,6 +32,8 @@ modules(..., label = "root") \method{format}{teal_modules}(x, indent = 0, ...) +set_datanames(modules, datanames) + \method{print}{teal_modules}(x, ...) } \arguments{ @@ -63,11 +67,9 @@ or to the \code{...}. \item{filters}{(\code{character}) Deprecated. Use \code{datanames} instead.} -\item{datanames}{(\code{character}) A vector with \code{datanames} that are relevant for the item. The -filter panel will automatically update the shown filters to include only -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{datanames}{(\code{character}) Names of the datasets that are relevant for the item. +The keyword \code{"all"} provides all datasets available in \code{data} passed to \code{teal} application. +\code{NULL} will hide the filter panel.} \item{server_args}{(named \code{list}) with additional arguments passed on to the server function.} @@ -75,7 +77,8 @@ a subset of datasets which are appended to the \code{data} argument in server fu \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. +If so, the module developer is responsible to display the UI in the module itself. \code{datanames} of the \code{transformers} +will be added to the \code{datanames}. When the transformation does not have sufficient input data, the resulting data will fallback to the last successful transform or, in case there are none, to the filtered data.} @@ -88,6 +91,8 @@ to the last successful transform or, in case there are none, to the filtered dat \item{x}{(\code{teal_module} or \code{teal_modules}) Object to format/print.} \item{indent}{(\code{integer(1)}) Indention level; each nested element is indented one level more.} + +\item{modules}{(\code{teal_module} or \code{teal_modules})} } \value{ \code{module()} returns an object of class \code{teal_module}. @@ -116,6 +121,26 @@ The labels \code{"global_filters"} and \code{"Report previewer"} are reserved because they are used by the \code{mapping} argument of \code{\link[=teal_slices]{teal_slices()}} and the report previewer module \code{\link[=reporter_previewer_module]{reporter_previewer_module()}}, respectively. } +\section{\code{datanames}}{ + +The module's \code{datanames} argument determines a subset of datasets from the \code{data} object, as specified in the +server function argument, to be presented in the module. Datasets displayed in the filter panel will be limited +to this subset. +When \code{datanames} is set to \code{"all"}, all available datasets in the \code{data} object are considered relevant for the +module. However, setting \code{datanames} argument to \code{"all"} might include datasets that are irrelevant for the module, +for example: +\itemize{ +\item Proxy variables used for modifying columns. +\item Modified copies of datasets used to create a final dataset. +\item Connection objects. +To prevent these irrelevant datasets from appearing in the module, use the \code{\link[=set_datanames]{set_datanames()}} function on the +\link{module} or \code{\link[=modules]{modules()}} to change the \code{datanames} from \code{"all"} to specific dataset names. Attempting to change +\code{datanames} values that was not set to \code{"all"} using \code{\link[=set_datanames]{set_datanames()}} will be ignored with a warning. +} + +Additionally, datasets with names starting with \code{.} are ignored when \code{datanames} is set to \code{"all"}. +} + \examples{ library(shiny) @@ -170,4 +195,15 @@ app <- init( if (interactive()) { shinyApp(app$ui, app$server) } +# change the module's datanames +set_datanames(module(datanames = "all"), "a") + +# change modules' datanames +set_datanames( + modules( + module(datanames = "all"), + module(datanames = "a") + ), + "b" +) } diff --git a/man/teal_transform_module.Rd b/man/teal_transform_module.Rd index 0a4ed27a34..0f424b329f 100644 --- a/man/teal_transform_module.Rd +++ b/man/teal_transform_module.Rd @@ -4,7 +4,12 @@ \alias{teal_transform_module} \title{Data module for \code{teal} transformers.} \usage{ -teal_transform_module(ui, server, label = "transform module") +teal_transform_module( + ui = function(id) NULL, + server = function(id, data) data, + label = "transform module", + datanames = "all" +) } \arguments{ \item{ui}{(\verb{function(id)}) @@ -16,6 +21,11 @@ where the \code{id} is the module id and \code{data} is the reactive \code{teal_ The server function must return reactive expression containing \code{teal_data} object.} \item{label}{(\code{character(1)}) Label of the module.} + +\item{datanames}{(\code{character}) +Names of the datasets that are relevant for the module. The +filter panel will only display filters for specified \code{datanames}. The keyword \code{"all"} will show +filters of all datasets. \code{datanames} will be automatically appended to the \code{\link[=modules]{modules()}} \code{datanames}.} } \description{ \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} @@ -34,6 +44,7 @@ See vignette \code{vignette("data-transform-as-shiny-module", package = "teal")} my_transformers <- list( teal_transform_module( label = "Custom transform for iris", + datanames = "iris", ui = function(id) { ns <- NS(id) tags$div( diff --git a/tests/testthat/test-module_teal.R b/tests/testthat/test-module_teal.R index 6232cad730..b1162c9924 100644 --- a/tests/testthat/test-module_teal.R +++ b/tests/testthat/test-module_teal.R @@ -62,20 +62,6 @@ transform_list <<- list( }) }) } - ), - add_dataset = teal_transform_module( - ui = function(id) NULL, - server = function(id, data) { - moduleServer(id, function(input, output, session) { - reactive({ - new_data <- within(data(), { - new_dataset <- data.frame(a = 1:3, b = 4:6) - }) - teal.data::datanames(new_data) <- c(teal.data::datanames(new_data), "new_dataset") - new_data - }) - }) - } ) ) @@ -522,8 +508,6 @@ testthat::describe("srv_teal teal_modules", { testthat::it("is called and receives data even if datanames in `teal_data` are not sufficient", { data <- teal_data(iris = iris) - teal.data::datanames(data) <- "iris" - shiny::testServer( app = srv_teal, args = list( @@ -540,14 +524,13 @@ testthat::describe("srv_teal teal_modules", { ) }) - testthat::it("receives all objects from @env except `DATA._raw_` when `DATA` is present in the @env and module$datanames = \"all\" and @datanames is empty", { # nolint: line_length. + testthat::it("receives all objects from @env excluding ._raw_ when module$datanames = \"all\"", { shiny::testServer( app = srv_teal, args = list( id = "test", data = reactive({ td <- teal_data(iris = iris, mtcars = mtcars, swiss = swiss, iris_raw = iris) - teal.data::datanames(td) <- character(0) td }), modules = modules( @@ -564,27 +547,6 @@ testthat::describe("srv_teal teal_modules", { ) }) - testthat::it("receives @datanames when module$datanames = \"all\"", { - shiny::testServer( - app = srv_teal, - args = list( - id = "test", - data = reactive({ - td <- teal_data(iris = iris, mtcars = mtcars, swiss = swiss) - teal.data::datanames(td) <- c("iris", "mtcars") - td - }), - modules = modules( - module("module_1", server = function(id, data) data, datanames = "all") - ) - ), - expr = { - session$setInputs(`teal_modules-active_tab` = "module_1") - testthat::expect_identical(teal.data::datanames(modules_output$module_1()()), c("iris", "mtcars")) - } - ) - }) - testthat::it("receives parent data when module$datanames limited to a child data but join keys are provided", { parent <- data.frame(id = 1:3, test = letters[1:3]) child <- data.frame(id = 1:9, parent_id = rep(1:3, each = 3), test2 = letters[1:9]) @@ -610,12 +572,18 @@ testthat::describe("srv_teal teal_modules", { ) }) - testthat::it("receives extra datanames added in a transform if specified in module$datanames", { + testthat::it("receives all transform datasets if module$datanames == 'all'", { shiny::testServer( app = srv_teal, args = list( id = "test", - data = reactive(teal_data(iris = iris, mtcars = mtcars)), + data = reactive({ + td <- within(teal_data(), { + iris <- iris + mtcars <- mtcars + }) + td + }), modules = modules( module( label = "module_1", @@ -623,7 +591,6 @@ testthat::describe("srv_teal teal_modules", { transformers = list( teal_transform_module( label = "Dummy", - ui = function(id) div("(does nothing)"), server = function(id, data) { moduleServer(id, function(input, output, session) { reactive(within(data(), swiss <- swiss)) @@ -631,18 +598,18 @@ testthat::describe("srv_teal teal_modules", { } ) ), - datanames = c("mtcars", "iris", "swiss") + datanames = "all" ) ) ), expr = { session$setInputs(`teal_modules-active_tab` = "module_1") - testthat::expect_identical(teal.data::datanames(modules_output$module_1()()), c("mtcars", "iris", "swiss")) + testthat::expect_identical(teal.data::datanames(modules_output$module_1()()), c("iris", "mtcars", "swiss")) } ) }) - testthat::it("doesn't receive extra transform datasets not set in @datanames if module$datanames == 'all'", { + testthat::it("receives all datasets if transform$datanames == 'all'", { shiny::testServer( app = srv_teal, args = list( @@ -652,7 +619,6 @@ testthat::describe("srv_teal teal_modules", { iris <- iris mtcars <- mtcars }) - teal.data::datanames(td) <- c("mtcars", "iris") td }), modules = modules( @@ -662,7 +628,6 @@ testthat::describe("srv_teal teal_modules", { transformers = list( teal_transform_module( label = "Dummy", - ui = function(id) div("(does nothing)"), server = function(id, data) { moduleServer(id, function(input, output, session) { reactive(within(data(), swiss <- swiss)) @@ -676,22 +641,17 @@ testthat::describe("srv_teal teal_modules", { ), expr = { session$setInputs(`teal_modules-active_tab` = "module_1") - testthat::expect_identical(teal.data::datanames(modules_output$module_1()()), c("mtcars", "iris")) + testthat::expect_identical(teal.data::datanames(modules_output$module_1()()), c("iris", "mtcars", "swiss")) } ) }) - testthat::it("receives extra transform datasets if module$datanames == 'all' and @datanames empty", { + testthat::it("combines datanames from transform/module $datanames", { shiny::testServer( app = srv_teal, args = list( id = "test", - data = reactive({ - within(teal_data(), { - iris <- iris - mtcars <- mtcars - }) - }), + data = reactive(teal_data(iris = iris, mtcars = mtcars, not_included = data.frame())), modules = modules( module( label = "module_1", @@ -704,10 +664,11 @@ testthat::describe("srv_teal teal_modules", { moduleServer(id, function(input, output, session) { reactive(within(data(), swiss <- swiss)) }) - } + }, + datanames = "swiss" ) ), - datanames = "all" + datanames = c("iris", "mtcars") ) ) ), @@ -718,12 +679,18 @@ testthat::describe("srv_teal teal_modules", { ) }) - testthat::it("doesn't receive extra datanames in a transform if not specified in module$datanames", { + testthat::it("does not receive transform datasets not specified in transform$datanames nor modue$datanames", { shiny::testServer( app = srv_teal, args = list( id = "test", - data = reactive(teal_data(iris = iris, mtcars = mtcars)), + data = reactive({ + td <- within(teal_data(), { + iris <- iris + mtcars <- mtcars + }) + td + }), modules = modules( module( label = "module_1", @@ -731,12 +698,12 @@ testthat::describe("srv_teal teal_modules", { transformers = list( teal_transform_module( label = "Dummy", - ui = function(id) div("(does nothing)"), server = function(id, data) { moduleServer(id, function(input, output, session) { reactive(within(data(), swiss <- swiss)) }) - } + }, + datanames = character(0) ) ), datanames = c("iris", "mtcars") @@ -849,64 +816,6 @@ testthat::describe("srv_teal teal_modules", { } ) }) - - testthat::it("reveives code of datasets used in transform even if not specified explicitly", { - testthat::it("receives all possible objects while those not specified in module$datanames are unfiltered", { - shiny::testServer( - app = srv_teal, - args = list( - id = "test", - data = reactive(within(teal.data::teal_data(), { - iris <- iris - mtcars <- mtcars - })), - filter = teal_slices( - teal_slice(dataname = "mtcars", varname = "cyl", selected = "4"), - teal_slice(dataname = "iris", varname = "Species", selected = "versicolor") - ), - modules = modules( - module( - label = "module_1", - server = function(id, data) data, - datanames = c("new_list"), - transformers = list( - teal_transform_module( - ui = function(id) NULL, - server = function(id, data) { - moduleServer(id, function(input, output, session) { - reactive({ - within(data(), new_list <- list(iris = iris, mtcars = mtcars)) - }) - }) - } - ) - ) - ) - ) - ), - expr = { - session$setInputs(`teal_modules-active_tab` = "module_1") - session$flushReact() - testthat::expect_identical(teal.data::datanames(modules_output$module_1()()), "new_list") - testthat::expect_identical(modules_output$module_1()()[["new_list"]]$mtcars, mtcars) - testthat::expect_identical(modules_output$module_1()()[["new_list"]]$iris, iris) - testthat::expect_identical( - teal.code::get_code(modules_output$module_1()()), - paste( - c( - "iris <- iris", - "mtcars <- mtcars", - 'stopifnot(rlang::hash(iris) == "34844aba7bde36f5a34f6d8e39803508")', - 'stopifnot(rlang::hash(mtcars) == "d0487363db4e6cc64fdb740cb6617fc0")', - "new_list <- list(iris = iris, mtcars = mtcars)" - ), - collapse = "\n" - ) - ) - } - ) - }) - }) }) testthat::describe("srv_teal filters", { @@ -1605,7 +1514,8 @@ testthat::describe("srv_teal teal_module(s) transformer", { within(data(), data_from_transform <- list(iris = iris, mtcars = mtcars)) }) }) - } + }, + datanames = character(0) ) ) ) @@ -1805,7 +1715,6 @@ testthat::describe("srv_teal summary table", { teal.data::join_keys(data) <- teal.data::join_keys( teal.data::join_key("a", "b", keys = "id") ) - teal.data::datanames(data) <- c("a", "b") shiny::testServer( app = srv_teal, @@ -1839,7 +1748,6 @@ testthat::describe("srv_teal summary table", { teal.data::join_key("a", keys = "id"), teal.data::join_key("b", keys = c("id", "id2")) ) - teal.data::datanames(data) <- c("a", "b") shiny::testServer( app = srv_teal, @@ -1874,7 +1782,6 @@ testthat::describe("srv_teal summary table", { teal.data::join_key("b", keys = c("id", "id2")), teal.data::join_key("a", "b", keys = "id") ) - teal.data::datanames(data) <- c("a", "b") shiny::testServer( app = srv_teal, @@ -1909,7 +1816,6 @@ testthat::describe("srv_teal summary table", { teal.data::join_key("b", keys = c("id", "id2")), teal.data::join_key("a", "b", keys = "id") ) - teal.data::datanames(data) <- c("a", "b") shiny::testServer( app = srv_teal, @@ -1945,7 +1851,6 @@ testthat::describe("srv_teal summary table", { teal.data::join_key("b", keys = c("id", "id2")), teal.data::join_key("a", "b", keys = "id") ) - teal.data::datanames(data) <- c("a", "b") shiny::testServer( app = srv_teal, @@ -1973,7 +1878,7 @@ testthat::describe("srv_teal summary table", { ) }) - testthat::it("reflects transform adding new dataset", { + testthat::it("reflects transform adding new dataset if specified in module", { shiny::testServer( app = srv_teal, args = list( @@ -1983,7 +1888,16 @@ testthat::describe("srv_teal summary table", { module( "module_1", server = function(id, data) data, - transformers = transform_list["add_dataset"], + transformers = teal_transform_module( + datanames = character(0), + server = function(id, data) { + moduleServer(id, function(input, output, session) { + reactive({ + within(data(), new_dataset <- data.frame(x = 1:3)) + }) + }) + } + ), datanames = c("iris", "new_dataset") ) ) @@ -2036,8 +1950,6 @@ testthat::describe("srv_teal summary table", { testthat::it("displays only module$datanames", { data <- teal.data::teal_data(iris = iris, mtcars = mtcars) - teal.data::datanames(data) <- c("iris", "mtcars") - shiny::testServer( app = srv_teal, args = list( @@ -2061,10 +1973,10 @@ testthat::describe("srv_teal summary table", { }) testthat::it("displays parent before child when join_keys are provided", { - data <- teal.data::teal_data(mtcars1 = mtcars, mtcars2 = data.frame(am = c(0, 1), test = c("a", "b"))) + data <- teal.data::teal_data(parent = mtcars, child = data.frame(am = c(0, 1), test = c("a", "b"))) teal.data::join_keys(data) <- teal.data::join_keys( - teal.data::join_key("mtcars2", "mtcars1", keys = c("am")) + teal.data::join_key("parent", "child", keys = c("am")) ) shiny::testServer( @@ -2079,7 +1991,7 @@ testthat::describe("srv_teal summary table", { session$flushReact() testthat::expect_identical( module_output_table(output, "module_1")[["Data Name"]], - c("mtcars2", "mtcars1") + c("parent", "child") ) } ) @@ -2087,8 +1999,6 @@ testthat::describe("srv_teal summary table", { testthat::it("displays subset of module$datanames if not sufficient", { data <- teal.data::teal_data(iris = iris, mtcars = mtcars) - teal.data::datanames(data) <- c("iris", "mtcars") - shiny::testServer( app = srv_teal, args = list( diff --git a/tests/testthat/test-modules.R b/tests/testthat/test-modules.R index a4a6a8651f..abd9c8f144 100644 --- a/tests/testthat/test-modules.R +++ b/tests/testthat/test-modules.R @@ -512,3 +512,64 @@ testthat::test_that("format.teal_modules returns proper structure", { "+ c\n + a\n + c\n + c\n" ) }) + + +testthat::test_that("module datanames is appended by its transformers datanames", { + transformer_w_datanames <- teal_transform_module( + ui = function(id) NULL, + server = function(id, data) { + moduleServer(id, function(input, output, session) { + reactive({ + new_data <- within(data(), { + new_dataset <- data.frame(a = 1:3, b = 4:6) + }) + new_data + }) + }) + }, + datanames = c("a", "b") + ) + + out <- module(datanames = "c", transformers = list(transformer_w_datanames)) + testthat::expect_identical(out$datanames, c("c", "a", "b")) +}) + +testthat::test_that("module datanames is set to 'all' if transformer $datanames is 'all'", { + transformer_w_datanames <- teal_transform_module( + ui = function(id) NULL, + server = function(id, data) { + moduleServer(id, function(input, output, session) { + reactive({ + new_data <- within(data(), { + new_dataset <- data.frame(a = 1:3, b = 4:6) + }) + new_data + }) + }) + }, + datanames = "all" + ) + + out <- module(datanames = "c", transformers = list(transformer_w_datanames)) + testthat::expect_identical(out$datanames, "all") +}) + +testthat::test_that("module datanames stays 'all' regardless of transformers", { + transformer_w_datanames <- teal_transform_module( + ui = function(id) NULL, + server = function(id, data) { + moduleServer(id, function(input, output, session) { + reactive({ + new_data <- within(data(), { + new_dataset <- data.frame(a = 1:3, b = 4:6) + }) + new_data + }) + }) + }, + datanames = c("a", "b") + ) + + out <- module(datanames = "all", transformers = list(transformer_w_datanames)) + testthat::expect_identical(out$datanames, "all") +}) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index dc171b5bf3..2905288579 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -45,25 +45,6 @@ test_that("teal_data_to_filtered_data return FilteredData class", { testthat::expect_s3_class(teal_data_to_filtered_data(teal_data), "FilteredData") }) -test_that("teal_data_datanames returns names of the @env's objects when datanames not set", { - teal_data <- teal.data::teal_data() - teal_data <- within(teal_data, { - iris <- head(iris) - mtcars <- head(mtcars) - }) - testthat::expect_setequal(.teal_data_datanames(teal_data), c("mtcars", "iris")) -}) - -test_that("teal_data_datanames returns datanames which are set by teal.data::datanames", { - teal_data <- teal.data::teal_data() - teal_data <- within(teal_data, { - iris <- head(iris) - mtcars <- head(mtcars) - }) - datanames(teal_data) <- "iris" - testthat::expect_equal(.teal_data_datanames(teal_data), "iris") -}) - test_that("validate_app_title_tag works on validating the title tag", { valid_title <- tags$head( tags$title("title"), diff --git a/vignettes/including-data-in-teal-applications.Rmd b/vignettes/including-data-in-teal-applications.Rmd index 16f33f7e22..aedf948d3b 100644 --- a/vignettes/including-data-in-teal-applications.Rmd +++ b/vignettes/including-data-in-teal-applications.Rmd @@ -179,38 +179,6 @@ The filter panel supports MAEs out of the box. ## `teal_data` properties -##### `datanames` - -The `datanames` property lists the objects stored in the `teal_data` environment that constitute datasets of interest. -Objects passed to `teal_data` become automatically listed in the `datanames` property of the resulting object. -Objects created in `teal_data` by evaluating code need not be data objects of interest and as such they are not automatically added to `datanames`. -For convenience, an empty `datanames` property is considered to mean "all objects in the container". -`datanames` can be read or modified with the `datanames` function. - -```{r} -data_with_objects <- teal_data(iris = iris, cars = mtcars) -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") -datanames(data_with_code) -``` - -The `datanames` property serves as a communication bridge between the data container and modules in a `teal` application. -In `teal` all modules are called with a `datanames` argument that determines which of the variables in the `teal_data` object they are to access. -Only variables enumerated in the `datanames` property are eligible for use in modules. - -Note that specifying `datanames` in `teal_data` is optional; if the property is empty, all objects are considered eligible. -Likewise, the `datanames` argument in the module call defaults to `"all"`, which means that module will attempt to access all eligible variables in the `teal_data` object. - -For a detailed explanation of `datanames`, see [this `teal.data` vignette](https://insightsengineering.github.io/teal.data/latest-tag/articles/teal-data.html). - -[(back to General Data)](#general-data) - ##### `join_keys` Using relational data requires specifying joining keys for each pair of datasets. From fc481ee88bf8e40c60a498048c3a1759817d66ef Mon Sep 17 00:00:00 2001 From: gogonzo Date: Thu, 5 Sep 2024 07:05:27 +0000 Subject: [PATCH 02/18] [skip actions] Bump version to 0.15.2.9060 --- DESCRIPTION | 4 ++-- NEWS.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 477ef2ec06..1b83515d09 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Type: Package Package: teal Title: Exploratory Web Apps for Analyzing Clinical Trials Data -Version: 0.15.2.9059 -Date: 2024-08-28 +Version: 0.15.2.9060 +Date: 2024-09-05 Authors@R: c( person("Dawid", "Kaledkowski", , "dawid.kaledkowski@roche.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0001-9533-457X")), diff --git a/NEWS.md b/NEWS.md index b99a176120..e7052c1b17 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -# teal 0.15.2.9059 +# teal 0.15.2.9060 ### New features From 12ba5e3623ef9148d140d67f3de709b52d21e251 Mon Sep 17 00:00:00 2001 From: Pawel Rucki <12943682+pawelru@users.noreply.github.com> Date: Tue, 10 Sep 2024 18:37:01 +0200 Subject: [PATCH 03/18] vbump teal.logger (#1343) teal.logger had been already released so there is no point of depending on the old development version Signed-off-by: Pawel Rucki <12943682+pawelru@users.noreply.github.com> --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 1b83515d09..c8c1bb2b4d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -52,7 +52,7 @@ Imports: shinyjs, stats, teal.code (>= 0.5.0), - teal.logger (>= 0.1.3.9013), + teal.logger (>= 0.2.0), teal.reporter (>= 0.3.1.9004), teal.widgets (>= 0.4.0), utils From 0f58ca6e5dd31be4a615f85468d49bba39682b0e Mon Sep 17 00:00:00 2001 From: pawelru Date: Tue, 10 Sep 2024 16:38:00 +0000 Subject: [PATCH 04/18] [skip actions] Bump version to 0.15.2.9061 --- DESCRIPTION | 4 ++-- NEWS.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index c8c1bb2b4d..1e000a1c0c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Type: Package Package: teal Title: Exploratory Web Apps for Analyzing Clinical Trials Data -Version: 0.15.2.9060 -Date: 2024-09-05 +Version: 0.15.2.9061 +Date: 2024-09-10 Authors@R: c( person("Dawid", "Kaledkowski", , "dawid.kaledkowski@roche.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0001-9533-457X")), diff --git a/NEWS.md b/NEWS.md index e7052c1b17..49057be1eb 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -# teal 0.15.2.9060 +# teal 0.15.2.9061 ### New features From 9a0733b61236fbd8ab5a556bf42585f119072974 Mon Sep 17 00:00:00 2001 From: walkowif <59475134+walkowif@users.noreply.github.com> Date: Mon, 16 Sep 2024 16:29:01 +0200 Subject: [PATCH 05/18] Workflow propagations (#1332) --- .github/workflows/check.yaml | 3 +++ .github/workflows/release.yaml | 3 +++ .github/workflows/scheduled.yaml | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 90e8823f08..e5060049d9 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -68,6 +68,9 @@ jobs: uses: insightsengineering/r.pkg.template/.github/workflows/test-coverage.yaml@main secrets: REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} + with: + additional-env-vars: | + NOT_CRAN=true linter: if: github.event_name != 'push' name: SuperLinter πŸ¦Έβ€β™€οΈ diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e56418bb83..aa3e7bb457 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -52,6 +52,9 @@ jobs: uses: insightsengineering/r.pkg.template/.github/workflows/test-coverage.yaml@main secrets: REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} + with: + additional-env-vars: | + NOT_CRAN=true wasm: name: Build WASM packages πŸ§‘β€πŸ­ needs: release diff --git a/.github/workflows/scheduled.yaml b/.github/workflows/scheduled.yaml index 4af24cb40a..c4233673e5 100644 --- a/.github/workflows/scheduled.yaml +++ b/.github/workflows/scheduled.yaml @@ -5,9 +5,26 @@ on: schedule: - cron: '45 3 * * 0' workflow_dispatch: + inputs: + chosen-workflow: + description: | + Select which workflow you'd like to run + required: true + type: choice + default: rhub + options: + - rhub + - dependency-test + - branch-cleanup + - revdepcheck jobs: dependency-test: + if: > + github.event_name == 'schedule' || ( + github.event_name == 'workflow_dispatch' && + inputs.chosen-workflow == 'dependency-test' + ) strategy: fail-fast: false matrix: @@ -22,14 +39,29 @@ jobs: additional-env-vars: | PKG_SYSREQS_DRY_RUN=true branch-cleanup: + if: > + github.event_name == 'schedule' || ( + github.event_name == 'workflow_dispatch' && + inputs.chosen-workflow == 'branch-cleanup' + ) name: Branch Cleanup 🧹 uses: insightsengineering/r.pkg.template/.github/workflows/branch-cleanup.yaml@main secrets: REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} revdepcheck: + if: > + github.event_name == 'schedule' || ( + github.event_name == 'workflow_dispatch' && + inputs.chosen-workflow == 'revdepcheck' + ) name: revdepcheck ↩️ uses: insightsengineering/r.pkg.template/.github/workflows/revdepcheck.yaml@main rhub: + if: > + github.event_name == 'schedule' || ( + github.event_name == 'workflow_dispatch' && + inputs.chosen-workflow == 'rhub' + ) name: R-hub 🌐 uses: insightsengineering/r.pkg.template/.github/workflows/rhub.yaml@main with: From e4bb083ab222f4a0c8b2b7e78b83785e19fa6b60 Mon Sep 17 00:00:00 2001 From: walkowif Date: Mon, 16 Sep 2024 14:29:57 +0000 Subject: [PATCH 06/18] [skip actions] Bump version to 0.15.2.9062 --- .pre-commit-config.yaml | 2 +- DESCRIPTION | 4 ++-- NEWS.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b6eb50d62..8b944da297 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ default_language_version: python: python3 repos: - repo: https://github.com/lorenzwalthert/precommit - rev: v0.4.3 + rev: v0.4.3.9001 hooks: - id: style-files name: Style code with `styler` diff --git a/DESCRIPTION b/DESCRIPTION index 1e000a1c0c..446c5a6cbe 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Type: Package Package: teal Title: Exploratory Web Apps for Analyzing Clinical Trials Data -Version: 0.15.2.9061 -Date: 2024-09-10 +Version: 0.15.2.9062 +Date: 2024-09-16 Authors@R: c( person("Dawid", "Kaledkowski", , "dawid.kaledkowski@roche.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0001-9533-457X")), diff --git a/NEWS.md b/NEWS.md index 49057be1eb..8190d796b4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -# teal 0.15.2.9061 +# teal 0.15.2.9062 ### New features From ec16ae92abb2f9beb545198962f85a5709242d24 Mon Sep 17 00:00:00 2001 From: Marcin <133694481+m7pr@users.noreply.github.com> Date: Tue, 17 Sep 2024 15:17:02 +0200 Subject: [PATCH 07/18] `module()$datanames` : unify `combined_datanames` no matter what's the length of `transformers` (#1344) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `module()$datanames` are affected by `datanames` parameter and `datanames` coming out of `transformers`. So far this is not unified, and the outcome `$datanames` depend on the length of input of the transformers. They are different if the transformer has length 1. ```r # 0 transformers example_module("mod-1", transformers = list( ), datanames = c("ADSL", "ADTTE"))$datanames # > [1] "ADSL" "ADTTE" "all" # 1 transformer example_module("mod-1", transformers = list( teal_transform_module(ui = function(id) NULL, server = function(id, data) NULL) ), datanames = c("ADSL", "ADTTE"))$datanames [1] "all" # 2 transformers example_module("mod-1", transformers = list( teal_transform_module(ui = function(id) NULL, server = function(id, data) NULL), teal_transform_module(ui = function(id) NULL, server = function(id, data) NULL) ), datanames = c("ADSL", "ADTTE"))$datanames # [1] "ADSL" "ADTTE" "all" ``` The solution is up to discussion, but for now I plan to unify to return `union(datanames, transformers$datanames)` as a result. So if - `modules(datanames = "all"` and `transfomers(datanames = "all"` we get `"all"` - `modules(datanames = "custom"` and `transfomers(datanames = "all"` we get `c("custom", "all")` - `modules(datanames = "all"` and `transfomers(datanames = "custom"` we get `"all"` - `modules(datanames = "custom"` and `transfomers(datanames = "custom2"` we get `c("custom", "custom2")` The main reason for this change is to unify and to have custom datanames returned in `module()$datanames` if any custom names are passed via `module(datanames = ` parameter. --------- Signed-off-by: Marcin <133694481+m7pr@users.noreply.github.com> Co-authored-by: Dawid KaΕ‚Δ™dkowski <6959016+gogonzo@users.noreply.github.com> --- R/modules.R | 2 +- tests/testthat/test-modules.R | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/modules.R b/R/modules.R index 7e39e44b60..76dea53d68 100644 --- a/R/modules.R +++ b/R/modules.R @@ -264,7 +264,7 @@ module <- function(label = "module", } checkmate::assert_list(transformers, types = "teal_transform_module") transformer_datanames <- unlist(lapply(transformers, attr, "datanames")) - combined_datanames <- if (identical(datanames, "all") || identical(transformer_datanames, "all")) { + combined_datanames <- if (identical(datanames, "all") || any(sapply(transformer_datanames, identical, "all"))) { "all" } else { union(datanames, transformer_datanames) diff --git a/tests/testthat/test-modules.R b/tests/testthat/test-modules.R index abd9c8f144..594e801d45 100644 --- a/tests/testthat/test-modules.R +++ b/tests/testthat/test-modules.R @@ -534,7 +534,7 @@ testthat::test_that("module datanames is appended by its transformers datanames" testthat::expect_identical(out$datanames, c("c", "a", "b")) }) -testthat::test_that("module datanames is set to 'all' if transformer $datanames is 'all'", { +testthat::test_that("module datanames is set to 'all' if any transformer $datanames is 'all'", { transformer_w_datanames <- teal_transform_module( ui = function(id) NULL, server = function(id, data) { From 19cda800137c50cbed347f19a5241618aed38624 Mon Sep 17 00:00:00 2001 From: m7pr Date: Tue, 17 Sep 2024 13:17:59 +0000 Subject: [PATCH 08/18] [skip actions] Bump version to 0.15.2.9063 --- DESCRIPTION | 4 ++-- NEWS.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 446c5a6cbe..f668bd7eec 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Type: Package Package: teal Title: Exploratory Web Apps for Analyzing Clinical Trials Data -Version: 0.15.2.9062 -Date: 2024-09-16 +Version: 0.15.2.9063 +Date: 2024-09-17 Authors@R: c( person("Dawid", "Kaledkowski", , "dawid.kaledkowski@roche.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0001-9533-457X")), diff --git a/NEWS.md b/NEWS.md index 8190d796b4..e4ff529659 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -# teal 0.15.2.9062 +# teal 0.15.2.9063 ### New features From 1b4bb508d3333086b2da80d074686ec896cc99de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ka=C5=82=C4=99dkowski?= <6959016+gogonzo@users.noreply.github.com> Date: Mon, 23 Sep 2024 06:47:04 +0200 Subject: [PATCH 09/18] 479 mirai lockfile@main (#1263) https://github.com/insightsengineering/teal/issues/1276 @pawelru @m7pr This is a `mirai` alternative. Package seems to address the biggest issues reported during a research: - `mirai` has a native support of `ExtendedTask` - `mirai` is not being killed when `runApp` is executed (like `callr` does) - `mirai` by default opens a deamon in parallel R session without a need to handle the `future::plan`. - `mirai` has only one dependency in the whole dependency tree. #### Disadvantages so far: - ~~we need to pass and set `options`, system vars, working directory and `.libPaths` https://github.com/shikokuchuo/mirai/issues/122~~ #### How does it work: - lockfile creation is invoked in `init` before application starts. This prevents to start the process each time when a new shiny session starts. Process is invoked as a promise and eventually `teal_app.lock` will be created - When shiny session starts `download lockfile` button is hidden by default. If promise is eventually resolved and lockfile is created then download button is shown. - alternatively, app developer can pre-compute lockfile and provide its path in `teal.renv.lockfile` option. In such case `renv::snapshot` will be skipped and user lockfile will be used in an app. #### Logs and notifications Logs are printed for app developer while notifications are presented to the app user: 1. When app uses precomputed file: - log in init: `Lockfile set using option "teal.renv.lockfile" - skipping automatic creation.` - no notification to the app user. 2. When app automatically determines snapshot: - log in init: `Lockfile creation started based on { getwd() }.` - log If lockfile created: `Lockfile {path} containing { n-pkgs } packages created{ with errors or warnings }.` - notification if lockfile created: `Lockfile available to download` - log if lockfile not created: `Lockfile creation failed.` - notification if lockfile not created: `Lockfile creation failed.` --------- Signed-off-by: Marcin <133694481+m7pr@users.noreply.github.com> Signed-off-by: Pawel Rucki <12943682+pawelru@users.noreply.github.com> Co-authored-by: m7pr Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Marcin <133694481+m7pr@users.noreply.github.com> Co-authored-by: Pawel Rucki <12943682+pawelru@users.noreply.github.com> Co-authored-by: 27856297+dependabot-preview[bot]@users.noreply.github.com <27856297+dependabot-preview[bot]@users.noreply.github.com> Co-authored-by: Aleksander Chlebowski <114988527+chlebowa@users.noreply.github.com> --- .pre-commit-config.yaml | 3 +- DESCRIPTION | 12 +- R/init.R | 3 - R/module_teal.R | 6 +- R/module_teal_lockfile.R | 201 ++++++++++++++++++++++++++++++ R/teal_lockfile.R | 114 ----------------- R/zzz.R | 9 +- man/module_teal_lockfile.Rd | 57 +++++++++ man/teal_lockfile.Rd | 45 ------- tests/testthat/test-module_teal.R | 50 ++++++++ tests/testthat/test-utils.R | 28 ----- vignettes/teal-options.Rmd | 14 ++- 12 files changed, 336 insertions(+), 206 deletions(-) create mode 100644 R/module_teal_lockfile.R delete mode 100644 R/teal_lockfile.R create mode 100644 man/module_teal_lockfile.Rd delete mode 100644 man/teal_lockfile.Rd diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b944da297..0952df50b1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,14 +17,13 @@ repos: additional_dependencies: - davidgohel/flextable # Error: package 'flextable' is not available - davidgohel/gdtools # for flextable + - mirai - checkmate - - future - jsonlite - lifecycle - logger - magrittr - methods - - promises - renv - rlang - shiny diff --git a/DESCRIPTION b/DESCRIPTION index f668bd7eec..0e19f7866b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -41,13 +41,10 @@ Depends: teal.slice (>= 0.5.1.9009) Imports: checkmate (>= 2.1.0), - future (>= 1.33.2), jsonlite, lifecycle (>= 0.2.0), logger (>= 0.2.0), methods, - promises (>= 1.3.0), - renv (>= 1.0.7), rlang (>= 1.0.0), shinyjs, stats, @@ -59,8 +56,10 @@ Imports: Suggests: bslib, knitr (>= 1.42), + mirai (>= 1.1.1), MultiAssayExperiment, R6, + renv (>= 1.0.7), rmarkdown (>= 2.23), rvest, shinytest2, @@ -74,8 +73,9 @@ RdMacros: lifecycle Config/Needs/verdepcheck: rstudio/shiny, insightsengineering/teal.data, insightsengineering/teal.slice, mllg/checkmate, - HenrikBengtsson/future, jeroen/jsonlite, r-lib/lifecycle, - daroczig/logger, rstudio/promises, rstudio/renv, r-lib/rlang, + jeroen/jsonlite, r-lib/lifecycle, + daroczig/logger, shikokuchuo/mirai, shikokuchuo/nanonext, + rstudio/renv, r-lib/rlang, daattali/shinyjs, insightsengineering/teal.code, insightsengineering/teal.logger, insightsengineering/teal.reporter, insightsengineering/teal.widgets, rstudio/bslib, yihui/knitr, @@ -106,6 +106,7 @@ Collate: 'module_snapshot_manager.R' 'module_teal.R' 'module_teal_data.R' + 'module_teal_lockfile.R' 'module_teal_with_splash.R' 'module_transform_data.R' 'reporter_previewer_module.R' @@ -116,7 +117,6 @@ Collate: 'teal_data_module-eval_code.R' 'teal_data_module-within.R' 'teal_data_utils.R' - 'teal_lockfile.R' 'teal_reporter.R' 'teal_slices-store.R' 'teal_slices.R' diff --git a/R/init.R b/R/init.R index 805bf1ee2e..ca5820e0a0 100644 --- a/R/init.R +++ b/R/init.R @@ -153,9 +153,6 @@ init <- function(data, # log teal.logger::log_system_info() - # invoke lockfile creation - teal_lockfile() - # argument transformations ## `modules` - landing module landing <- extract_module(modules, "teal_module_landing") diff --git a/R/module_teal.R b/R/module_teal.R index 391782cb9d..796a306966 100644 --- a/R/module_teal.R +++ b/R/module_teal.R @@ -138,7 +138,7 @@ ui_teal <- function(id, footer, teal.widgets::verbatim_popup_ui(ns("sessionInfo"), "Session Info", type = "link"), br(), - downloadLink(ns("lockFile"), "Download .lock file"), + ui_teal_lockfile(ns("lockfile")), textOutput(ns("identifier")) ) ) @@ -156,6 +156,8 @@ srv_teal <- function(id, data, modules, filter = teal_slices()) { moduleServer(id, function(input, output, session) { logger::log_debug("srv_teal initializing.") + srv_teal_lockfile("lockfile") + output$identifier <- renderText( paste0("Pid:", Sys.getpid(), " Token:", substr(session$token, 25, 32)) ) @@ -166,8 +168,6 @@ srv_teal <- function(id, data, modules, filter = teal_slices()) { title = "SessionInfo" ) - output$lockFile <- teal_lockfile_downloadhandler() - # `JavaScript` code run_js_files(files = "init.js") diff --git a/R/module_teal_lockfile.R b/R/module_teal_lockfile.R new file mode 100644 index 0000000000..5dd058d1f0 --- /dev/null +++ b/R/module_teal_lockfile.R @@ -0,0 +1,201 @@ +#' Generate lockfile for application's environment reproducibility +#' +#' @param lockfile_path (`character`) path to the lockfile. +#' +#' @section Different ways of creating lockfile: +#' `teal` leverages [renv::snapshot()], which offers multiple methods for lockfile creation. +#' +#' - **Working directory lockfile**: `teal`, by default, will create an `implicit` type lockfile that uses +#' `renv::dependencies()` to detect all R packages in the current project's working directory. +#' - **`DESCRIPTION`-based lockfile**: To generate a lockfile based on a `DESCRIPTION` file in your working +#' directory, set `renv::settings$snapshot.type("explicit")`. The naming convention for `type` follows +#' `renv::snapshot()`. For the `"explicit"` type, refer to `renv::settings$package.dependency.fields()` for the +#' `DESCRIPTION` fields included in the lockfile. +#' - **Custom files-based lockfile**: To specify custom files as the basis for the lockfile, set +#' `renv::settings$snapshot.type("custom")` and configure the `renv.snapshot.filter` option. +#' +#' @section lockfile usage: +#' After creating the lockfile, you can restore the application's environment using `renv::restore()`. +#' +#' @seealso [renv::snapshot()], [renv::restore()]. +#' +#' @return `NULL` +#' +#' @name module_teal_lockfile +#' @rdname module_teal_lockfile +#' +#' @keywords internal +NULL + +#' @rdname module_teal_lockfile +ui_teal_lockfile <- function(id) { + ns <- NS(id) + shiny::tagList( + tags$span("", id = ns("lockFileStatus")), + shinyjs::disabled(downloadLink(ns("lockFileLink"), "Download lockfile")) + ) +} + +#' @rdname module_teal_lockfile +srv_teal_lockfile <- function(id) { + moduleServer(id, function(input, output, session) { + logger::log_debug("Initialize srv_teal_lockfile.") + enable_lockfile_download <- function() { + shinyjs::html("lockFileStatus", "Application lockfile ready.") + shinyjs::hide("lockFileStatus", anim = TRUE) + shinyjs::enable("lockFileLink") + output$lockFileLink <- shiny::downloadHandler( + filename = function() { + "renv.lock" + }, + content = function(file) { + file.copy(lockfile_path, file) + file + }, + contentType = "application/json" + ) + } + disable_lockfile_download <- function() { + warning("Lockfile creation failed.", call. = FALSE) + shinyjs::html("lockFileStatus", "Lockfile creation failed.") + shinyjs::hide("lockFileLink") + } + + shiny::onStop(function() { + if (file.exists(lockfile_path) && !shiny::isRunning()) { + logger::log_debug("Removing lockfile after shutting down the app") + file.remove(lockfile_path) + } + }) + + lockfile_path <- "teal_app.lock" + mode <- getOption("teal.lockfile.mode", default = "") + + if (!(mode %in% c("auto", "enabled", "disabled"))) { + stop("'teal.lockfile.mode' option can only be one of \"auto\", \"disabled\" or \"disabled\". ") + } + + if (mode == "disabled") { + logger::log_debug("'teal.lockfile.mode' option is set to 'disabled'. Hiding lockfile download button.") + shinyjs::hide("lockFileLink") + return(NULL) + } + + if (file.exists(lockfile_path)) { + logger::log_debug("Lockfile has already been created for this app - skipping automatic creation.") + enable_lockfile_download() + return(NULL) + } + + if (mode == "auto" && .is_disabled_lockfile_scenario()) { + logger::log_debug( + "Automatic lockfile creation disabled. Execution scenario satisfies teal:::.is_disabled_lockfile_scenario()." + ) + shinyjs::hide("lockFileLink") + return(NULL) + } + + if (!.is_lockfile_deps_installed()) { + warning("Automatic lockfile creation disabled. `mirai` and `renv` packages must be installed.") + shinyjs::hide("lockFileLink") + return(NULL) + } + + # - Will be run only if the lockfile doesn't exist (see the if-s above) + # - We render to the tempfile because the process might last after session is closed and we don't + # want to make a "teal_app.renv" then. This is why we copy only during active session. + process <- .teal_lockfile_process_invoke(lockfile_path) + observeEvent(process$status(), { + if (process$status() %in% c("initial", "running")) { + shinyjs::html("lockFileStatus", "Creating lockfile...") + } else if (process$status() == "success") { + result <- process$result() + if (any(grepl("Lockfile written to", result$out))) { + logger::log_debug("Lockfile containing { length(result$res$Packages) } packages created.") + if (any(grepl("(WARNING|ERROR):", result$out))) { + warning("Lockfile created with warning(s) or error(s):", call. = FALSE) + for (i in result$out) { + warning(i, call. = FALSE) + } + } + enable_lockfile_download() + } else { + disable_lockfile_download() + } + } else if (process$status() == "error") { + disable_lockfile_download() + } + }) + + NULL + }) +} + +utils::globalVariables(c("opts", "sysenv", "libpaths", "wd", "lockfilepath", "run")) # needed for mirai call +#' @rdname module_teal_lockfile +.teal_lockfile_process_invoke <- function(lockfile_path) { + mirai_obj <- NULL + process <- shiny::ExtendedTask$new(function() { + m <- mirai::mirai( + { + options(opts) + do.call(Sys.setenv, sysenv) + .libPaths(libpaths) + setwd(wd) + run(lockfile_path = lockfile_path) + }, + run = .renv_snapshot, + lockfile_path = lockfile_path, + opts = options(), + libpaths = .libPaths(), + sysenv = as.list(Sys.getenv()), + wd = getwd() + ) + mirai_obj <<- m + m + }) + + shiny::onStop(function() { + if (mirai::unresolved(mirai_obj)) { + logger::log_debug("Terminating a running lockfile process...") + mirai::stop_mirai(mirai_obj) # this doesn't stop running - renv will be created even if session is closed + } + }) + + suppressWarnings({ # 'package:stats' may not be available when loading + process$invoke() + }) + + logger::log_debug("Lockfile creation started based on { getwd() }.") + + process +} + +#' @rdname module_teal_lockfile +.renv_snapshot <- function(lockfile_path) { + out <- utils::capture.output( + res <- renv::snapshot( + lockfile = lockfile_path, + prompt = FALSE, + force = TRUE, + type = renv::settings$snapshot.type() # see the section "Different ways of creating lockfile" above here + ) + ) + + list(out = out, res = res) +} + +#' @rdname module_teal_lockfile +.is_lockfile_deps_installed <- function() { + requireNamespace("mirai", quietly = TRUE) && requireNamespace("renv", quietly = TRUE) +} + +#' @rdname module_teal_lockfile +.is_disabled_lockfile_scenario <- function() { + identical(Sys.getenv("CALLR_IS_RUNNING"), "true") || # inside callr process + identical(Sys.getenv("TESTTHAT"), "true") || # inside devtools::test + !identical(Sys.getenv("QUARTO_PROJECT_ROOT"), "") || # inside Quarto process + ( + ("CheckExEnv" %in% search()) || any(c("_R_CHECK_TIMINGS_", "_R_CHECK_LICENSE_") %in% names(Sys.getenv())) + ) # inside R CMD CHECK +} diff --git a/R/teal_lockfile.R b/R/teal_lockfile.R deleted file mode 100644 index 5d253f8e9c..0000000000 --- a/R/teal_lockfile.R +++ /dev/null @@ -1,114 +0,0 @@ -#' Generate lockfile for application reproducibility -#' -#' This function is invoked during [teal::init] to create `renv`-compatible lockfile for use within the application. -#' -#' The function leverages [renv::snapshot()], which offers multiple methods for lockfile creation. -#' -#' - User-specified: -#' - **Pre-computed lockfile**: Users can provide their own pre-computed lockfile by specifying the path via -#' `teal.renv.lockfile` option. Automatic lockfile computation is skipped in such case. -#' - Automatically computed: -#' - **Working directory lockfile**: If `teal.renv.lockfile` is not set, `teal` will, by default, create an -#' `implicit` type lockfile that uses `renv::dependencies()` to detect all R packages in the current project's -#' working directory. -#' - **`DESCRIPTION`-based lockfile**: To generate a lockfile based on a `DESCRIPTION` file in your working -#' directory, set `renv::settings$snapshot.type("explicit")`. The naming convention for `type` follows -#' `renv::snapshot()`. For the `"explicit"` type, refer to `renv::settings$package.dependency.fields()` for the -#' `DESCRIPTION` fields included in the lockfile. -#' - **Custom files-based lockfile**: To specify custom files as the basis for the lockfile, set -#' `renv::settings$snapshot.type("custom")` and configure the `renv.snapshot.filter` option. -#' -#' @section lockfile usage: -#' After creating the lockfile, you can restore the application environment using `renv::restore()`. -#' -#' @seealso [renv::snapshot()], [renv::restore()]. -#' -#' @return Nothing. This function is executed for its side effect of creating a lockfile used in the `teal` application. -#' -#' @keywords internal -teal_lockfile <- function() { - lockfile_path <- "teal_app.lock" - # If user has setup the file, there is no need to compute a new one. - user_lockfile <- getOption("teal.renv.lockfile", "") - if (!identical(user_lockfile, "")) { - if (file.exists(user_lockfile)) { - file.copy(user_lockfile, lockfile_path) - return(invisible(NULL)) - } else { - stop("lockfile provided through options('teal.renv.lockfile') does not exist.") - } - } - - if (!(is_in_test() || is_r_cmd_check())) { - old_plan <- future::plan() - # If there is already a parallel (non-sequential) backend, reuse it. - if (inherits(old_plan, "sequential")) { - future::plan(future::multisession, workers = 2) - } - - lockfile_task <- ExtendedTask$new(create_renv_lockfile) - lockfile_task$invoke(close = inherits(old_plan, "sequential"), lockfile_path) - logger::log_debug("lockfile creation invoked.") - } -} - -create_renv_lockfile <- function(close = FALSE, lockfile_path = NULL) { - checkmate::assert_flag(close) - checkmate::assert_string(lockfile_path, na.ok = TRUE) - promise <- promises::future_promise({ - # Below we can not use a file created in tempdir() directory. - # If a file is created in tempdir(), it gets deleted on 'then(onFulfilled' part. - shiny::onStop(function() file.remove(lockfile_path)) - - renv_logs <- utils::capture.output( - renv::snapshot( - lockfile = lockfile_path, - prompt = FALSE, - force = TRUE - # type = is taken from renv::settings$snapshot.type() - ) - ) - if (any(grepl("Lockfile written", renv_logs))) { - logger::log_debug("lockfile created successfully.") - } else { - logger::log_debug("lockfile created with issues.") - } - - lockfile_path - }) - if (close) { - # If the initial backend was only sequential, bring it back. - promises::then(promise, onFulfilled = function() { - future::plan(future::sequential) - }) - } - promise -} - -teal_lockfile_downloadhandler <- function() { - downloadHandler( - filename = function() { - "renv.lock" - }, - content = function(file) { - teal_lockfile <- "teal_app.lock" - iter <- 1 - while (!file.exists(teal_lockfile) && iter <= 100) { - logger::log_debug("lockfile not created yet, retrying...") - Sys.sleep(0.25) - iter <- iter + 1 # max wait time is 25 seconds - } - file.copy(teal_lockfile, file) - file - }, - contentType = "application/json" - ) -} - -is_r_cmd_check <- function() { - ("CheckExEnv" %in% search()) || any(c("_R_CHECK_TIMINGS_", "_R_CHECK_LICENSE_") %in% names(Sys.getenv())) -} - -is_in_test <- function() { - identical(Sys.getenv("TESTTHAT"), "true") -} diff --git a/R/zzz.R b/R/zzz.R index 817f9bae4b..a991d041f2 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,13 +1,16 @@ .onLoad <- function(libname, pkgname) { # adapted from https://github.com/r-lib/devtools/blob/master/R/zzz.R - teal_default_options <- list(teal.show_js_log = FALSE) + + teal_default_options <- list( + teal.show_js_log = FALSE, + teal.lockfile.mode = "auto", + shiny.sanitize.errors = FALSE + ) op <- options() toset <- !(names(teal_default_options) %in% names(op)) if (any(toset)) options(teal_default_options[toset]) - options("shiny.sanitize.errors" = FALSE) - # Set up the teal logger instance teal.logger::register_logger("teal") teal.logger::register_handlers("teal") diff --git a/man/module_teal_lockfile.Rd b/man/module_teal_lockfile.Rd new file mode 100644 index 0000000000..c4f7170dfd --- /dev/null +++ b/man/module_teal_lockfile.Rd @@ -0,0 +1,57 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/module_teal_lockfile.R +\name{module_teal_lockfile} +\alias{module_teal_lockfile} +\alias{ui_teal_lockfile} +\alias{srv_teal_lockfile} +\alias{.teal_lockfile_process_invoke} +\alias{.renv_snapshot} +\alias{.is_lockfile_deps_installed} +\alias{.is_disabled_lockfile_scenario} +\title{Generate lockfile for application's environment reproducibility} +\usage{ +ui_teal_lockfile(id) + +srv_teal_lockfile(id) + +.teal_lockfile_process_invoke(lockfile_path) + +.renv_snapshot(lockfile_path) + +.is_lockfile_deps_installed() + +.is_disabled_lockfile_scenario() +} +\arguments{ +\item{lockfile_path}{(\code{character}) path to the lockfile.} +} +\value{ +\code{NULL} +} +\description{ +Generate lockfile for application's environment reproducibility +} +\section{Different ways of creating lockfile}{ + +\code{teal} leverages \code{\link[renv:snapshot]{renv::snapshot()}}, which offers multiple methods for lockfile creation. +\itemize{ +\item \strong{Working directory lockfile}: \code{teal}, by default, will create an \code{implicit} type lockfile that uses +\code{renv::dependencies()} to detect all R packages in the current project's working directory. +\item \strong{\code{DESCRIPTION}-based lockfile}: To generate a lockfile based on a \code{DESCRIPTION} file in your working +directory, set \code{renv::settings$snapshot.type("explicit")}. The naming convention for \code{type} follows +\code{renv::snapshot()}. For the \code{"explicit"} type, refer to \code{renv::settings$package.dependency.fields()} for the +\code{DESCRIPTION} fields included in the lockfile. +\item \strong{Custom files-based lockfile}: To specify custom files as the basis for the lockfile, set +\code{renv::settings$snapshot.type("custom")} and configure the \code{renv.snapshot.filter} option. +} +} + +\section{lockfile usage}{ + +After creating the lockfile, you can restore the application's environment using \code{renv::restore()}. +} + +\seealso{ +\code{\link[renv:snapshot]{renv::snapshot()}}, \code{\link[renv:restore]{renv::restore()}}. +} +\keyword{internal} diff --git a/man/teal_lockfile.Rd b/man/teal_lockfile.Rd deleted file mode 100644 index dc83582c6f..0000000000 --- a/man/teal_lockfile.Rd +++ /dev/null @@ -1,45 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/teal_lockfile.R -\name{teal_lockfile} -\alias{teal_lockfile} -\title{Generate lockfile for application reproducibility} -\usage{ -teal_lockfile() -} -\value{ -Nothing. This function is executed for its side effect of creating a lockfile used in the \code{teal} application. -} -\description{ -This function is invoked during \link{init} to create \code{renv}-compatible lockfile for use within the application. -} -\details{ -The function leverages \code{\link[renv:snapshot]{renv::snapshot()}}, which offers multiple methods for lockfile creation. -\itemize{ -\item User-specified: -\itemize{ -\item \strong{Pre-computed lockfile}: Users can provide their own pre-computed lockfile by specifying the path via -\code{teal.renv.lockfile} option. Automatic lockfile computation is skipped in such case. -} -\item Automatically computed: -\itemize{ -\item \strong{Working directory lockfile}: If \code{teal.renv.lockfile} is not set, \code{teal} will, by default, create an -\code{implicit} type lockfile that uses \code{renv::dependencies()} to detect all R packages in the current project's -working directory. -\item \strong{\code{DESCRIPTION}-based lockfile}: To generate a lockfile based on a \code{DESCRIPTION} file in your working -directory, set \code{renv::settings$snapshot.type("explicit")}. The naming convention for \code{type} follows -\code{renv::snapshot()}. For the \code{"explicit"} type, refer to \code{renv::settings$package.dependency.fields()} for the -\code{DESCRIPTION} fields included in the lockfile. -\item \strong{Custom files-based lockfile}: To specify custom files as the basis for the lockfile, set -\code{renv::settings$snapshot.type("custom")} and configure the \code{renv.snapshot.filter} option. -} -} -} -\section{lockfile usage}{ - -After creating the lockfile, you can restore the application environment using \code{renv::restore()}. -} - -\seealso{ -\code{\link[renv:snapshot]{renv::snapshot()}}, \code{\link[renv:restore]{renv::restore()}}. -} -\keyword{internal} diff --git a/tests/testthat/test-module_teal.R b/tests/testthat/test-module_teal.R index b1162c9924..41f057c2e0 100644 --- a/tests/testthat/test-module_teal.R +++ b/tests/testthat/test-module_teal.R @@ -65,6 +65,56 @@ transform_list <<- list( ) ) +testthat::describe("srv_teal lockfile", { + testthat::it(paste0( + "creation process is invoked for teal.lockfile.mode = \"enabled\" ", + "and snapshot is copied to teal_app.lock and removed after session ended" + ), { + withr::with_options( + list(teal.lockfile.mode = "enabled"), + { + renv_filename <- "teal_app.lock" + shiny::testServer( + app = srv_teal, + args = list( + id = "test", + data = teal.data::teal_data(iris = iris), + modules = modules(example_module()) + ), + expr = { + iter <- 1 + while (!file.exists(renv_filename) && iter <= 100) { + Sys.sleep(0.25) + iter <- iter + 1 # max wait time is 25 seconds + } + testthat::expect_true(file.exists(renv_filename)) + } + ) + testthat::expect_false(file.exists(renv_filename)) + } + ) + }) + testthat::it("creation process is not invoked for teal.lockfile.mode = \"disabled\"", { + withr::with_options( + list(teal.lockfile.mode = "disabled"), + { + renv_filename <- "teal_app.lock" + shiny::testServer( + app = srv_teal, + args = list( + id = "test", + data = teal.data::teal_data(iris = iris), + modules = modules(example_module()) + ), + expr = { + testthat::expect_false(file.exists(renv_filename)) + } + ) + } + ) + }) +}) + testthat::describe("srv_teal arguments", { testthat::it("accepts data to be teal_data", { testthat::expect_no_error( diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 2905288579..4904c46461 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -176,31 +176,3 @@ testthat::test_that("defunction recursively goes down a list", { y ) }) - -testthat::test_that("create_renv_lockfile creates a lock file during the execution", { - old_plan <- future::plan(future::sequential) - withr::defer(future::plan(old_plan)) - - renv_file_name <- "teal_app.lock" - withr::defer(file.remove(renv_file_name)) - promise <- create_renv_lockfile(TRUE, renv_file_name) - - testthat::expect_true(file.exists(renv_file_name)) -}) - -testthat::test_that("check_modules_datanames message is the same in html tags and in string", { - testthat::skip_if_not_installed("rvest") - modules <- module(datanames = c("iris", "mtcars"), ui = function(id) NULL, server = function(id, data) NULL) - - message <- check_modules_datanames(modules, "missing") - - # Compares 2 strings (removes quotations and empty space surrounding tags) - testthat::expect_identical( - gsub("\"", "", message$string), - trimws( - rvest::html_text2( - rvest::read_html(as.character(message$html(with_module_name = TRUE))) - ) - ) - ) -}) diff --git a/vignettes/teal-options.Rmd b/vignettes/teal-options.Rmd index 99accfa5a6..3ab894456e 100644 --- a/vignettes/teal-options.Rmd +++ b/vignettes/teal-options.Rmd @@ -84,9 +84,19 @@ This indicates whether to print the `JavaScript` console logs to the `R` console Default: `FALSE`. -### `teal.renv.lockfile` (`character`) +### `teal.lockfile.mode` (`character`) -The path to the pre-computed `renv` lockfile that will be shared through teal app. To read more about lockfile usage creation check `?teal::teal_lockfile`. +This enables to compute `renv` lockfile and shows a button to `"download lockfile"` in the footer. + +Values: + +* `"auto"` - auto detect whether to compute `lockfile` +* `"enabled"` - compute `lockfile` and show `"download lockfile"` in the footer +* `"disabled"` - do not compute `lockfile` and do not show `"download lockfile"` in the footer + +Default: `"auto"`. + +To read more about lockfile usage creation check `?teal::module_teal_lockfile`. # Deprecated options From 1736d8ffb002ac858f5af9f38b6879e97ea1cedf Mon Sep 17 00:00:00 2001 From: gogonzo Date: Mon, 23 Sep 2024 04:48:02 +0000 Subject: [PATCH 10/18] [skip actions] Bump version to 0.15.2.9064 --- DESCRIPTION | 23 +++++++++++------------ NEWS.md | 2 +- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 0e19f7866b..38e03d113c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Type: Package Package: teal Title: Exploratory Web Apps for Analyzing Clinical Trials Data -Version: 0.15.2.9063 -Date: 2024-09-17 +Version: 0.15.2.9064 +Date: 2024-09-23 Authors@R: c( person("Dawid", "Kaledkowski", , "dawid.kaledkowski@roche.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0001-9533-457X")), @@ -72,16 +72,15 @@ VignetteBuilder: RdMacros: lifecycle Config/Needs/verdepcheck: rstudio/shiny, insightsengineering/teal.data, - insightsengineering/teal.slice, mllg/checkmate, - jeroen/jsonlite, r-lib/lifecycle, - daroczig/logger, shikokuchuo/mirai, shikokuchuo/nanonext, - rstudio/renv, r-lib/rlang, - daattali/shinyjs, insightsengineering/teal.code, - insightsengineering/teal.logger, insightsengineering/teal.reporter, - insightsengineering/teal.widgets, rstudio/bslib, yihui/knitr, - bioc::MultiAssayExperiment, r-lib/R6, rstudio/rmarkdown, - tidyverse/rvest, rstudio/shinytest2, rstudio/shinyvalidate, - r-lib/testthat, r-lib/withr, yaml=vubiostat/r-yaml + insightsengineering/teal.slice, mllg/checkmate, jeroen/jsonlite, + r-lib/lifecycle, daroczig/logger, shikokuchuo/mirai, + shikokuchuo/nanonext, rstudio/renv, r-lib/rlang, daattali/shinyjs, + insightsengineering/teal.code, insightsengineering/teal.logger, + insightsengineering/teal.reporter, insightsengineering/teal.widgets, + rstudio/bslib, yihui/knitr, bioc::MultiAssayExperiment, r-lib/R6, + rstudio/rmarkdown, tidyverse/rvest, rstudio/shinytest2, + rstudio/shinyvalidate, r-lib/testthat, r-lib/withr, + yaml=vubiostat/r-yaml Config/Needs/website: insightsengineering/nesttemplate Encoding: UTF-8 Language: en-US diff --git a/NEWS.md b/NEWS.md index e4ff529659..0d82369174 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -# teal 0.15.2.9063 +# teal 0.15.2.9064 ### New features From 6e7841124e542993ce25e49960b4d03a0072bebf Mon Sep 17 00:00:00 2001 From: gogonzo Date: Mon, 23 Sep 2024 04:48:54 +0000 Subject: [PATCH 11/18] [skip ci] Update WORDLIST --- inst/WORDLIST | 1 - 1 file changed, 1 deletion(-) diff --git a/inst/WORDLIST b/inst/WORDLIST index e1af54046a..cf9153ad3f 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -4,7 +4,6 @@ Forkers Hoffmann MAEs ORCID -Pre Reproducibility TLG UI From 1ecce137b26169c85d6c4abe88425bc819ea6a28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ka=C5=82=C4=99dkowski?= <6959016+gogonzo@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:27:33 +0200 Subject: [PATCH 12/18] Change name of the raw (unfiltered object) (#1342) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From `._raw_` to `._raw`. This means that `teal.data::datanames()` is not really needed. `ls(data@env)` returns all object names from environment except prefixed by `.` (`all.names = FALSE` is a default). This adds clarity to the handling of a datanames in teal application: - "all" means, all object from the environment except those prefixed by `.` - One can exclude datanames globally, for example by changing `con -> .con` and `ADSL_temp -> .ADSL_temp` --------- Signed-off-by: Dawid KaΕ‚Δ™dkowski <6959016+gogonzo@users.noreply.github.com> Co-authored-by: 27856297+dependabot-preview[bot]@users.noreply.github.com <27856297+dependabot-preview[bot]@users.noreply.github.com> Co-authored-by: Marcin <133694481+m7pr@users.noreply.github.com> --- NEWS.md | 1 + R/dummy_functions.R | 5 +- R/init.R | 6 +- R/module_data_summary.R | 13 +-- R/module_filter_data.R | 10 ++- R/module_init_data.R | 9 +- R/module_nested_tabs.R | 6 +- R/module_teal_data.R | 5 +- R/teal_data_utils.R | 26 ++---- R/utils.R | 19 ++-- man/dot-get_hashes_code.Rd | 2 +- man/teal_data_to_filtered_data.Rd | 2 +- man/teal_data_utilities.Rd | 3 - tests/testthat/test-module_teal.R | 99 +++++++++++---------- tests/testthat/test-shinytest2-show-rcode.R | 17 ++-- 15 files changed, 116 insertions(+), 107 deletions(-) diff --git a/NEWS.md b/NEWS.md index 0d82369174..0e677cc2f4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -22,6 +22,7 @@ * Easier way of to call `javascript` events by setting `$(document).ready(function() { ... })`. #1114 * Provided progress bar for modules loading and data filtering during teal app startup. * Filter mapping display has a separate icon in the tab. +* Environment of the `data` passed to the `teal_module`'s server consists unfiltered datasets contained in `.raw_data`. # teal 0.15.2 diff --git a/R/dummy_functions.R b/R/dummy_functions.R index 7e6765dfbc..fd3f067699 100644 --- a/R/dummy_functions.R +++ b/R/dummy_functions.R @@ -20,10 +20,7 @@ example_module <- function(label = "example teal module", datanames = "all", tra server = function(id, data) { checkmate::assert_class(isolate(data()), "teal_data") moduleServer(id, function(input, output, session) { - datanames_rv <- reactive({ - .teal_data_ls(req(data())) - }) - + datanames_rv <- reactive(ls(teal.code::get_env((req(data()))))) observeEvent(datanames_rv(), { selected <- input$dataname if (identical(selected, "")) { diff --git a/R/init.R b/R/init.R index ca5820e0a0..e6cb66953b 100644 --- a/R/init.R +++ b/R/init.R @@ -207,16 +207,16 @@ init <- function(data, ## `data` - `modules` if (inherits(data, "teal_data")) { - if (length(.teal_data_ls(data)) == 0) { + if (length(ls(teal.code::get_env(data))) == 0) { stop("The environment of `data` is empty.") } - is_modules_ok <- check_modules_datanames(modules, .teal_data_ls(data)) + is_modules_ok <- check_modules_datanames(modules, ls(teal.code::get_env(data))) if (!isTRUE(is_modules_ok) && length(unlist(extract_transformers(modules))) == 0) { lapply(is_modules_ok$string, warning, call. = FALSE) } - is_filter_ok <- check_filter_datanames(filter, .teal_data_ls(data)) + is_filter_ok <- check_filter_datanames(filter, ls(teal.code::get_env(data))) if (!isTRUE(is_filter_ok)) { warning(is_filter_ok) # we allow app to continue if applied filters are outside diff --git a/R/module_data_summary.R b/R/module_data_summary.R index 407edc0aba..e793c53f70 100644 --- a/R/module_data_summary.R +++ b/R/module_data_summary.R @@ -62,8 +62,7 @@ srv_data_summary <- function(id, teal_data) { summary_table <- reactive({ req(inherits(teal_data(), "teal_data")) - - if (!length(.teal_data_ls(teal_data()))) { + if (!length(ls(teal.code::get_env(teal_data())))) { return(NULL) } @@ -141,22 +140,18 @@ srv_data_summary <- function(id, teal_data) { get_filter_overview <- function(teal_data) { datanames <- teal.data::datanames(teal_data()) joinkeys <- teal.data::join_keys(teal_data()) + filtered_data_objs <- sapply( datanames, - function(name) teal.code::get_env(teal_data())[[name]], - simplify = FALSE - ) - unfiltered_data_objs <- sapply( - datanames, - function(name) teal.code::get_env(teal_data())[[paste0(name, "._raw_")]], + function(name) teal.code::get_var(teal_data(), name), simplify = FALSE ) + unfiltered_data_objs <- teal.code::get_var(teal_data(), ".raw_data") rows <- lapply( datanames, function(dataname) { parent <- teal.data::parent(joinkeys, dataname) - # todo: what should we display for a parent dataset? # - Obs and Subjects # - Obs only diff --git a/R/module_filter_data.R b/R/module_filter_data.R index 20bf73dd6a..c9ea56ddae 100644 --- a/R/module_filter_data.R +++ b/R/module_filter_data.R @@ -52,7 +52,15 @@ srv_filter_data <- function(id, datasets, active_datanames, data_rv, is_active) #' @rdname module_filter_data .make_filtered_teal_data <- function(modules, data, datasets = NULL, datanames) { - data <- eval_code(data, sprintf("%1$s._raw_ <- %1$s", datanames)) + data <- eval_code( + data, + paste0( + ".raw_data <- list2env(list(", + toString(sprintf("%1$s = %1$s", datanames)), + "))\n", + "lockEnvironment(.raw_data) #@linksto .raw_data" # this is environment and it is shared by qenvs. CAN'T MODIFY! + ) + ) filtered_code <- teal.slice::get_filter_expr(datasets = datasets, datanames = datanames) filtered_teal_data <- .append_evaluated_code(data, filtered_code) filtered_datasets <- sapply(datanames, function(x) datasets$get_data(x, filtered = TRUE), simplify = FALSE) diff --git a/R/module_init_data.R b/R/module_init_data.R index 7990a2bb0a..e79909a1f9 100644 --- a/R/module_init_data.R +++ b/R/module_init_data.R @@ -107,7 +107,7 @@ srv_init_data <- function(id, data, modules, filter = teal_slices()) { ) } - is_filter_ok <- check_filter_datanames(filter, .teal_data_ls(data_validated())) + is_filter_ok <- check_filter_datanames(filter, ls(teal.code::get_env(data_validated()))) if (!isTRUE(is_filter_ok)) { showNotification( "Some filters were not applied because of incompatibility with data. Contact app developer.", @@ -134,7 +134,10 @@ srv_init_data <- function(id, data, modules, filter = teal_slices()) { }) # Adds signature protection to the datanames in the data - reactive(.add_signature_to_data(data_validated())) + reactive({ + req(data_validated()) + .add_signature_to_data(data_validated()) + }) }) } @@ -171,7 +174,7 @@ srv_init_data <- function(id, data, modules, filter = teal_slices()) { #' @return A character vector with the code lines. #' @keywords internal #' -.get_hashes_code <- function(data, datanames = .teal_data_ls(data)) { +.get_hashes_code <- function(data, datanames = ls(teal.code::get_env(data))) { vapply( datanames, function(dataname, datasets) { diff --git a/R/module_nested_tabs.R b/R/module_nested_tabs.R index 7bc3a72fbf..76ead65b33 100644 --- a/R/module_nested_tabs.R +++ b/R/module_nested_tabs.R @@ -301,11 +301,11 @@ srv_teal_module.teal_module <- function(id, .resolve_module_datanames <- function(data, modules) { stopifnot("data_rv must be teal_data object." = inherits(data, "teal_data")) if (is.null(modules$datanames) || identical(modules$datanames, "all")) { - .teal_data_ls(data) + .topologically_sort_datanames(ls(teal.code::get_env(data)), teal.data::join_keys(data)) } else { intersect( - include_parent_datanames(modules$datanames, teal.data::join_keys(data)), - .teal_data_ls(data) + .include_parent_datanames(modules$datanames, teal.data::join_keys(data)), + ls(teal.code::get_env(data)) ) } } diff --git a/R/module_teal_data.R b/R/module_teal_data.R index f1c6087575..943f635136 100644 --- a/R/module_teal_data.R +++ b/R/module_teal_data.R @@ -161,7 +161,10 @@ srv_validate_reactive_teal_data <- function(id, # nolint: object_length output$shiny_warnings <- renderUI({ if (inherits(data_out_r(), "teal_data")) { - is_modules_ok <- check_modules_datanames(modules = modules, datanames = .teal_data_ls(data_validated())) + is_modules_ok <- check_modules_datanames( + modules = modules, + datanames = ls(teal.code::get_env(data_validated())) + ) if (!isTRUE(is_modules_ok)) { tags$div( is_modules_ok$html( diff --git a/R/teal_data_utils.R b/R/teal_data_utils.R index bb265e0fcd..caedf21e21 100644 --- a/R/teal_data_utils.R +++ b/R/teal_data_utils.R @@ -33,7 +33,7 @@ NULL checkmate::assert_class(data, "teal_data") checkmate::assert_class(objects, "list") new_env <- list2env(objects, parent = .GlobalEnv) - rlang::env_coalesce(new_env, data@env) + rlang::env_coalesce(new_env, teal.code::get_env(data)) data@env <- new_env data } @@ -42,35 +42,23 @@ NULL .subset_teal_data <- function(data, datanames) { checkmate::assert_class(data, "teal_data") checkmate::assert_class(datanames, "character") - datanames_corrected <- intersect(datanames, ls(data@env)) - dataname_corrected_with_raw <- intersect(c(datanames, sprintf("%s._raw_", datanames)), ls(data@env)) - - if (!length(datanames)) { + datanames_corrected <- intersect(datanames, ls(teal.code::get_env(data))) + datanames_corrected_with_raw <- c(datanames_corrected, ".raw_data") + if (!length(datanames_corrected)) { return(teal_data()) } new_data <- do.call( teal.data::teal_data, args = c( - mget(x = dataname_corrected_with_raw, envir = data@env), + mget(x = datanames_corrected_with_raw, envir = teal.code::get_env(data)), list( - code = gsub( - "warning('Code was not verified for reproducibility.')\n", - "", - teal.data::get_code(data, datanames = dataname_corrected_with_raw), - fixed = TRUE - ), + code = teal.data::get_code(data, datanames = datanames_corrected_with_raw), join_keys = teal.data::join_keys(data)[datanames_corrected] ) ) ) new_data@verified <- data@verified - teal.data::datanames(new_data) <- datanames + teal.data::datanames(new_data) <- datanames_corrected new_data } - -#' @rdname teal_data_utilities -.teal_data_ls <- function(data) { - checkmate::assert_class(data, "teal_data") - grep("._raw_", ls(teal.code::get_env(data), all.names = FALSE), value = TRUE, invert = TRUE) -} diff --git a/R/utils.R b/R/utils.R index 36cd81f564..d2bedef4e3 100644 --- a/R/utils.R +++ b/R/utils.R @@ -44,20 +44,28 @@ get_teal_bs_theme <- function() { #' Return parentnames along with datanames. #' @noRd #' @keywords internal -include_parent_datanames <- function(dataname, join_keys) { - ordered_datanames <- dataname - for (i in dataname) { +.include_parent_datanames <- function(datanames, join_keys) { + ordered_datanames <- datanames + for (i in datanames) { parents <- character(0) while (length(i) > 0) { parent_i <- teal.data::parent(join_keys, i) parents <- c(parent_i, parents) i <- parent_i } - ordered_datanames <- c(parents, dataname, ordered_datanames) + ordered_datanames <- c(parents, ordered_datanames) } unique(ordered_datanames) } +#' Return topologicaly sorted datanames +#' @noRd +#' @keywords internal +.topologically_sort_datanames <- function(datanames, join_keys) { + datanames_with_parents <- .include_parent_datanames(datanames, join_keys) + intersect(datanames, datanames_with_parents) +} + #' Create a `FilteredData` #' #' Create a `FilteredData` object from a `teal_data` object. @@ -66,7 +74,7 @@ include_parent_datanames <- function(dataname, join_keys) { #' @param datanames (`character`) vector of data set names to include; must be subset of `datanames(x)` #' @return A `FilteredData` object. #' @keywords internal -teal_data_to_filtered_data <- function(x, datanames = .teal_data_ls(x)) { +teal_data_to_filtered_data <- function(x, datanames = ls(teal.code::get_env(x))) { checkmate::assert_class(x, "teal_data") checkmate::assert_character(datanames, min.chars = 1L, any.missing = FALSE) # Otherwise, FilteredData will be created in the modules' scope later @@ -79,6 +87,7 @@ teal_data_to_filtered_data <- function(x, datanames = .teal_data_ls(x)) { ) } + #' Template function for `TealReportCard` creation and customization #' #' This function generates a report card with a title, diff --git a/man/dot-get_hashes_code.Rd b/man/dot-get_hashes_code.Rd index 07280ef587..2b6d519312 100644 --- a/man/dot-get_hashes_code.Rd +++ b/man/dot-get_hashes_code.Rd @@ -4,7 +4,7 @@ \alias{.get_hashes_code} \title{Get code that tests the integrity of the reproducible data} \usage{ -.get_hashes_code(data, datanames = .teal_data_ls(data)) +.get_hashes_code(data, datanames = ls(teal.code::get_env(data))) } \arguments{ \item{data}{(\code{teal_data}) object holding the data} diff --git a/man/teal_data_to_filtered_data.Rd b/man/teal_data_to_filtered_data.Rd index 4d7d53e38e..d6eecd90cd 100644 --- a/man/teal_data_to_filtered_data.Rd +++ b/man/teal_data_to_filtered_data.Rd @@ -4,7 +4,7 @@ \alias{teal_data_to_filtered_data} \title{Create a \code{FilteredData}} \usage{ -teal_data_to_filtered_data(x, datanames = .teal_data_ls(x)) +teal_data_to_filtered_data(x, datanames = ls(teal.code::get_env(x))) } \arguments{ \item{x}{(\code{teal_data}) object} diff --git a/man/teal_data_utilities.Rd b/man/teal_data_utilities.Rd index a9fc37eb9a..07f850f124 100644 --- a/man/teal_data_utilities.Rd +++ b/man/teal_data_utilities.Rd @@ -5,7 +5,6 @@ \alias{.append_evaluated_code} \alias{.append_modified_data} \alias{.subset_teal_data} -\alias{.teal_data_ls} \title{\code{teal_data} utils} \usage{ .append_evaluated_code(data, code) @@ -13,8 +12,6 @@ .append_modified_data(data, objects) .subset_teal_data(data, datanames) - -.teal_data_ls(data) } \arguments{ \item{data}{(\code{teal_data})} diff --git a/tests/testthat/test-module_teal.R b/tests/testthat/test-module_teal.R index 41f057c2e0..766e49abb5 100644 --- a/tests/testthat/test-module_teal.R +++ b/tests/testthat/test-module_teal.R @@ -1,6 +1,6 @@ # comment: srv_teal is exported so the tests here are extensive and cover srv_data as well. # testing of srv_data is not needed. -module_output_table <<- function(output, id) { +module_summary_table <<- function(output, id) { testthat::skip_if_not_installed("rvest") table_id <- sprintf("teal_modules-%s-data_summary-table", id) html <- output[[table_id]]$html @@ -453,22 +453,10 @@ testthat::describe("srv_teal teal_modules", { ), expr = { testthat::expect_null(modules_output$module_1()) - session$setInputs(`data-teal_data_module-data-dataset` = "iris", `teal_modules-active_tab` = "module_1") - testthat::expect_identical( - ls(teal.code::get_env(modules_output$module_1()())), - c("iris", "iris._raw_") - ) - - # comment: can't trigger reactivity in testServer - the change in a reactive input data - # is not propagated to the teal_module(data). Instead we test if the modified data - # is sent to another teal_module + testthat::expect_setequal(ls(teal.code::get_env(modules_output$module_1()())), "iris") session$setInputs(`data-teal_data_module-data-dataset` = "mtcars", `teal_modules-active_tab` = "module_2") - session$flushReact() - testthat::expect_identical( - ls(teal.code::get_env(modules_output$module_2()())), - c("mtcars", "mtcars._raw_") - ) + testthat::expect_setequal(ls(teal.code::get_env(modules_output$module_2()())), "mtcars") } ) }) @@ -574,7 +562,7 @@ testthat::describe("srv_teal teal_modules", { ) }) - testthat::it("receives all objects from @env excluding ._raw_ when module$datanames = \"all\"", { + testthat::it("receives all objects from @env when module$datanames = \"all\"", { shiny::testServer( app = srv_teal, args = list( @@ -696,6 +684,34 @@ testthat::describe("srv_teal teal_modules", { ) }) + testthat::it("receives all raw datasets based on module$datanames", { + shiny::testServer( + app = srv_teal, + args = list( + id = "test", + data = reactive({ + td <- within(teal_data(), { + iris <- iris + mtcars <- mtcars + swiss <- swiss + }) + td + }), + modules = modules( + module( + label = "module_1", + server = function(id, data) data, + datanames = c("iris", "swiss") + ) + ) + ), + expr = { + session$setInputs(`teal_modules-active_tab` = "module_1") + testthat::expect_setequal(ls(modules_output$module_1()()[[".raw_data"]]), c("iris", "swiss")) + } + ) + }) + testthat::it("combines datanames from transform/module $datanames", { shiny::testServer( app = srv_teal, @@ -1278,20 +1294,17 @@ testthat::describe("srv_teal filters", { session$flushReact() # iris is not active testthat::expect_identical(modules_output$module_1()()[["iris"]], iris) - testthat::expect_identical(modules_output$module_1()()[["iris._raw_"]], iris) # mtcars has been modified expected_mtcars <- subset(mtcars, cyl == 4) testthat::expect_identical(modules_output$module_1()()[["mtcars"]], expected_mtcars) - testthat::expect_identical(modules_output$module_1()()[["mtcars._raw_"]], mtcars) - expected_code <- paste0( c( "iris <- iris", "mtcars <- mtcars", sprintf('stopifnot(rlang::hash(iris) == "%s")', rlang::hash(iris)), sprintf('stopifnot(rlang::hash(mtcars) == "%s")', rlang::hash(mtcars)), - "iris._raw_ <- iris", - "mtcars._raw_ <- mtcars", + ".raw_data <- list2env(list(iris = iris, mtcars = mtcars))", + "lockEnvironment(.raw_data)", "mtcars <- dplyr::filter(mtcars, cyl == 4)" ), collapse = "\n" @@ -1413,7 +1426,7 @@ testthat::describe("srv_teal data reload", { }) testthat::describe("srv_teal teal_module(s) transformer", { - testthat::it("evaluates custom qenv call and pass update teal_data to the module", { + testthat::it("evaluates custom qenv call and pass updated teal_data to the module", { shiny::testServer( app = srv_teal, args = list( @@ -1430,9 +1443,7 @@ testthat::describe("srv_teal teal_module(s) transformer", { expr = { session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_identical(modules_output$module_1()()[["iris"]], head(iris)) - testthat::expect_identical(modules_output$module_1()()[["iris._raw_"]], iris) testthat::expect_identical(modules_output$module_1()()[["mtcars"]], head(mtcars)) - testthat::expect_identical(modules_output$module_1()()[["mtcars._raw_"]], mtcars) } ) }) @@ -1460,22 +1471,18 @@ testthat::describe("srv_teal teal_module(s) transformer", { ), expr = { session$setInputs(`teal_modules-active_tab` = "module_1") - expected_iris <- subset(iris, Species == "versicolor") rownames(expected_iris) <- NULL expected_iris <- head(expected_iris) testthat::expect_identical(modules_output$module_1()()[["iris"]], expected_iris) - testthat::expect_identical(modules_output$module_1()()[["iris._raw_"]], iris) testthat::expect_identical(modules_output$module_1()()[["mtcars"]], head(subset(mtcars, cyl == 6))) - testthat::expect_identical(modules_output$module_1()()[["mtcars._raw_"]], mtcars) - expected_code <- paste(collapse = "\n", c( "iris <- iris", "mtcars <- mtcars", sprintf('stopifnot(rlang::hash(iris) == "%s")', rlang::hash(iris)), sprintf('stopifnot(rlang::hash(mtcars) == "%s")', rlang::hash(mtcars)), - "iris._raw_ <- iris", - "mtcars._raw_ <- mtcars", + ".raw_data <- list2env(list(iris = iris, mtcars = mtcars))", + "lockEnvironment(.raw_data)", 'iris <- dplyr::filter(iris, Species == "versicolor")', "mtcars <- dplyr::filter(mtcars, cyl == 6)", "iris <- head(iris, n = 6)", @@ -1514,17 +1521,14 @@ testthat::describe("srv_teal teal_module(s) transformer", { session$flushReact() testthat::expect_identical(modules_output$module_1()()[["iris"]], head(iris)) - testthat::expect_identical(modules_output$module_1()()[["iris._raw_"]], iris) testthat::expect_identical(modules_output$module_1()()[["mtcars"]], head(subset(mtcars, cyl == 4))) - testthat::expect_identical(modules_output$module_1()()[["mtcars._raw_"]], mtcars) - expected_code <- paste(collapse = "\n", c( "iris <- iris", "mtcars <- mtcars", sprintf('stopifnot(rlang::hash(iris) == "%s")', rlang::hash(iris)), sprintf('stopifnot(rlang::hash(mtcars) == "%s")', rlang::hash(mtcars)), - "iris._raw_ <- iris", - "mtcars._raw_ <- mtcars", + ".raw_data <- list2env(list(iris = iris, mtcars = mtcars))", + "lockEnvironment(.raw_data)", "mtcars <- dplyr::filter(mtcars, cyl == 4)", "iris <- head(iris, n = 6)", "mtcars <- head(mtcars, n = 6)" @@ -1632,7 +1636,6 @@ testthat::describe("srv_teal teal_module(s) transformer", { expr = { session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_identical(modules_output$module_1()()[["iris"]], iris) - testthat::expect_identical(modules_output$module_1()()[["iris._raw_"]], iris) } ) }) @@ -1661,7 +1664,6 @@ testthat::describe("srv_teal teal_module(s) transformer", { expr = { session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_identical(modules_output$module_1()()[["iris"]], iris) - testthat::expect_identical(modules_output$module_1()()[["iris._raw_"]], iris) } ) }) @@ -1746,7 +1748,7 @@ testthat::describe("srv_teal summary table", { session$setInputs("teal_modules-active_tab" = "module_1") session$flushReact() testthat::expect_identical( - module_output_table(output, "module_1"), + module_summary_table(output, "module_1"), data.frame( "Data Name" = c("iris", "mtcars"), Obs = c("150/150", "32/32"), @@ -1777,7 +1779,7 @@ testthat::describe("srv_teal summary table", { session$setInputs("teal_modules-active_tab" = "module_1") session$flushReact() testthat::expect_identical( - module_output_table(output, "module_1"), + module_summary_table(output, "module_1"), data.frame( "Data Name" = c("a", "b"), Obs = c("3/3", "6/6"), @@ -1810,7 +1812,7 @@ testthat::describe("srv_teal summary table", { session$setInputs("teal_modules-active_tab" = "module_1") session$flushReact() testthat::expect_identical( - module_output_table(output, "module_1"), + module_summary_table(output, "module_1"), data.frame( "Data Name" = c("a", "b"), Obs = c("3/3", "6/6"), @@ -1844,7 +1846,7 @@ testthat::describe("srv_teal summary table", { session$setInputs("teal_modules-active_tab" = "module_1") session$flushReact() testthat::expect_identical( - module_output_table(output, "module_1"), + module_summary_table(output, "module_1"), data.frame( "Data Name" = c("a", "b"), Obs = c("3/3", "6/6"), @@ -1879,7 +1881,7 @@ testthat::describe("srv_teal summary table", { session$setInputs("teal_modules-active_tab" = "module_1") session$flushReact() testthat::expect_identical( - module_output_table(output, "module_1"), + module_summary_table(output, "module_1"), data.frame( "Data Name" = c("a", "b"), Obs = c("1/3", "2/6"), @@ -1916,7 +1918,7 @@ testthat::describe("srv_teal summary table", { ) session$flushReact() testthat::expect_identical( - module_output_table(output, "module_1"), + module_summary_table(output, "module_1"), data.frame( "Data Name" = c("a", "b"), Obs = c("1/3", "2/6"), @@ -1956,7 +1958,7 @@ testthat::describe("srv_teal summary table", { session$setInputs("teal_modules-active_tab" = "module_1") session$flushReact() testthat::expect_identical( - module_output_table(output, "module_1"), + module_summary_table(output, "module_1"), data.frame( "Data Name" = c("iris", "new_dataset"), Obs = c("150/150", "3"), @@ -1986,7 +1988,7 @@ testthat::describe("srv_teal summary table", { session$setInputs("teal_modules-active_tab" = "module_1") session$flushReact() testthat::expect_identical( - module_output_table(output, "module_1"), + module_summary_table(output, "module_1"), data.frame( "Data Name" = c("iris"), Obs = c("6/150"), @@ -2011,7 +2013,7 @@ testthat::describe("srv_teal summary table", { session$setInputs("teal_modules-active_tab" = "module_1") session$flushReact() testthat::expect_identical( - module_output_table(output, "module_1"), + module_summary_table(output, "module_1"), data.frame( "Data Name" = c("iris"), Obs = c("150/150"), @@ -2028,7 +2030,6 @@ testthat::describe("srv_teal summary table", { teal.data::join_keys(data) <- teal.data::join_keys( teal.data::join_key("parent", "child", keys = c("am")) ) - shiny::testServer( app = srv_teal, args = list( @@ -2040,7 +2041,7 @@ testthat::describe("srv_teal summary table", { session$setInputs("teal_modules-active_tab" = "module_1") session$flushReact() testthat::expect_identical( - module_output_table(output, "module_1")[["Data Name"]], + module_summary_table(output, "module_1")[["Data Name"]], c("parent", "child") ) } @@ -2060,7 +2061,7 @@ testthat::describe("srv_teal summary table", { session$setInputs("teal_modules-active_tab" = "module_1") session$flushReact() testthat::expect_identical( - module_output_table(output, "module_1"), + module_summary_table(output, "module_1"), data.frame( "Data Name" = c("iris"), Obs = c("150/150"), diff --git a/tests/testthat/test-shinytest2-show-rcode.R b/tests/testthat/test-shinytest2-show-rcode.R index 0e6be7c128..20901cd270 100644 --- a/tests/testthat/test-shinytest2-show-rcode.R +++ b/tests/testthat/test-shinytest2-show-rcode.R @@ -42,11 +42,18 @@ testthat::test_that("e2e: teal app initializes with Show R Code modal", { ) # Check R code output. - 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) - testthat::expect_match(r_code, "stopifnot(rlang::hash(", fixed = TRUE) + testthat::expect_identical( + app$get_text(app$active_module_element("rcode-verbatim_content")), + paste( + "iris <- iris", + "mtcars <- mtcars", + sprintf('stopifnot(rlang::hash(iris) == "%s")', rlang::hash(iris)), + sprintf('stopifnot(rlang::hash(mtcars) == "%s")', rlang::hash(mtcars)), + ".raw_data <- list2env(list(iris = iris, mtcars = mtcars))", + "lockEnvironment(.raw_data)", + sep = "\n" + ) + ) app$stop() }) From 5f6bc656001815deaf16906b21b6aa35bd424bf8 Mon Sep 17 00:00:00 2001 From: gogonzo Date: Thu, 26 Sep 2024 12:28:28 +0000 Subject: [PATCH 13/18] [skip actions] Bump version to 0.15.2.9065 --- DESCRIPTION | 4 ++-- NEWS.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 38e03d113c..0b307d3b26 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Type: Package Package: teal Title: Exploratory Web Apps for Analyzing Clinical Trials Data -Version: 0.15.2.9064 -Date: 2024-09-23 +Version: 0.15.2.9065 +Date: 2024-09-26 Authors@R: c( person("Dawid", "Kaledkowski", , "dawid.kaledkowski@roche.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0001-9533-457X")), diff --git a/NEWS.md b/NEWS.md index 0e677cc2f4..2150fde333 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -# teal 0.15.2.9064 +# teal 0.15.2.9065 ### New features From 04135a9fdbea0a3bf2855d3e598079d0da2d0c4a Mon Sep 17 00:00:00 2001 From: Dony Unardi Date: Fri, 27 Sep 2024 07:03:11 -0700 Subject: [PATCH 14/18] increase timeout for ci (#1355) Closes https://github.com/insightsengineering/coredev-tasks/issues/581 --- tests/testthat/test-module_teal.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/testthat/test-module_teal.R b/tests/testthat/test-module_teal.R index 766e49abb5..9b0be9b378 100644 --- a/tests/testthat/test-module_teal.R +++ b/tests/testthat/test-module_teal.R @@ -83,9 +83,9 @@ testthat::describe("srv_teal lockfile", { ), expr = { iter <- 1 - while (!file.exists(renv_filename) && iter <= 100) { - Sys.sleep(0.25) - iter <- iter + 1 # max wait time is 25 seconds + while (!file.exists(renv_filename) && iter <= 1000) { + Sys.sleep(0.5) + iter <- iter + 1 # max wait time is 500 seconds } testthat::expect_true(file.exists(renv_filename)) } From 2a7d3f98222489983b72fc27227214c8a16dd6eb Mon Sep 17 00:00:00 2001 From: donyunardi Date: Fri, 27 Sep 2024 14:04:12 +0000 Subject: [PATCH 15/18] [skip actions] Bump version to 0.15.2.9066 --- DESCRIPTION | 4 ++-- NEWS.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 0b307d3b26..6cd1e69ac1 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Type: Package Package: teal Title: Exploratory Web Apps for Analyzing Clinical Trials Data -Version: 0.15.2.9065 -Date: 2024-09-26 +Version: 0.15.2.9066 +Date: 2024-09-27 Authors@R: c( person("Dawid", "Kaledkowski", , "dawid.kaledkowski@roche.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0001-9533-457X")), diff --git a/NEWS.md b/NEWS.md index 2150fde333..48072cbcd7 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -# teal 0.15.2.9065 +# teal 0.15.2.9066 ### New features From 7683b6fb491ca7db04090d51d9f3963db904f26a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Ka=C5=82=C4=99dkowski?= Date: Thu, 3 Oct 2024 09:28:48 +0200 Subject: [PATCH 16/18] 1304 handover error@main (#1341) Alternative solution to #1330. Closes #1304, #1307, and #1308 1. When teal_data_module fails, then teal-module-tabs are disabled. When teal_data_module returns teal_data again teal-module-tabs are enabled 2. When reactive data passed directly to srv_teal fails, then the whole tab-panel is hidden and error message is shown. Warning messages are displayed over tab-panel. 3. when teal_transform_module fails then following teal_transform_module(s) show generic message that something was wrong. Reason for this is the same as (3). 4. when teal_transform_module fails then teal-module output is disabled and generic failure message is shown in the main panel. We decided to show a generic failure message as "real failure message" should be only shown in the place where error occurred to no cause confusion. 5. failing teal_data_module/teal_transform_module fallbacks to previous valid data (see exaplanation below) The most important part of the implementation is that when teal_data_module fails then it return the previous valid data (i.e. it return unchanged data). This means that failure doesn't trigger downstream reactivity and we don't need to deal with `data` input as error. In other words, this implementation halts reactivity when something goes wrong. When something goes wrong, teal-module-output is hidden and instead error message is displayed. Also, I've moved `data` completely away from `ui` and now if there is `teal_data_module` then data-tab is added dynamically.
app w/ teal_data_module ```r options( teal.log_level = "TRACE", teal.show_js_log = TRUE, # teal.bs_theme = bslib::bs_theme(version = 5), shiny.bookmarkStore = "server" ) # pkgload::load_all("teal.data") pkgload::load_all("teal") make_data <- function(datanames = c("ADSL", "ADTTE")) { data_obj <- teal.data::teal_data() if ("ADSL" %in% datanames) { data_obj <- within(data_obj, ADSL <- teal.data::rADSL) } if ("ADTTE" %in% datanames) { data_obj <- within(data_obj, ADTTE <- teal.data::rADTTE) } join_keys(data_obj) <- default_cdisc_join_keys[datanames] teal.data::datanames(data_obj) <- datanames data_obj } trans <- list( teal_transform_module( ui = function(id) { ns <- NS(id) tagList( selectizeInput( ns("errortype"), label = "Error Type", choices = c( "ok", "insufficient datasets", "no data", "qenv.error", "error in reactive", "validate error", "silent.shiny.error", "not a reactive" ) ) ) }, server = function(id, data) { moduleServer(id, function(input, output, session) { logger::log_trace("example_module_transform2 initializing.") reactive({ switch(input$errortype, ok = data(), `insufficient datasets` = teal:::.subset_teal_data(data(), "ADSL"), `no data` = teal_data(), qenv.error = within(teal_data(), stop("\nthis is qenv.error in teal_transform_module\n")), `error in reactive` = stop("\nerror in a reactive in teal_transform_module\n"), `validate error` = validate(need(FALSE, "\nvalidate error in teal_transform_module\n")), `silent.shiny.error` = req(FALSE) ) }) }) } ) ) data <- teal_data_module( once = FALSE, ui = function(id) { ns <- NS(id) tagList( selectizeInput( ns("errortype"), label = "Error Type", choices = c( "ok", "insufficient datasets", "no data", "qenv.error", "error in reactive", "validate error", "silent.shiny.error", "not a reactive" ) ), actionButton(ns("submit"), "Go!") ) }, server = function(id, ...) { moduleServer(id, function(input, output, session) { logger::log_trace("example_module_transform2 initializing.") eventReactive(input$submit, { switch(input$errortype, ok = make_data(), `insufficient datasets` = make_data(datanames = "ADSL"), `no data` = teal_data(), qenv.error = within(data(), stop("\nthis is qenv.error in teal_data_module\n")), `error in reactive` = stop("\nerror in a reactive in teal_data_module\n"), `validate error` = validate(need(FALSE, "\nvalidate error in teal_data_module\n")), `silent.shiny.error` = req(FALSE) ) }) }) } ) app <- teal::init( data = data, modules = list( example_module("mod-1", transformers = c(trans, trans, trans), datanames = c("ADSL", "ADTTE")), example_module("mod-2", transformers = trans, datanames = c("ADSL", "ADTTE")), module( label = "I was made to annoy you", ui = function(id) NULL, server = function(id, data) { moduleServer(id, function(input, output, session) { observe({ teal.data::datanames(data()) ADSL <- data()[["ADSL"]] ADSL$AGE }) observeEvent(data(), { print(data()[["ADSL"]]$SEX) }) }) }, datanames = "ADSL" ) ), filter = teal_slices( teal_slice("ADSL", "SEX"), teal_slice("ADSL", "AGE", selected = c(18L, 65L)), teal_slice("ADTTE", "PARAMCD", selected = "CRSD"), include_varnames = list( ADSL = c("SEX", "AGE") ) ) ) runApp(app) ```
app wrapped ```r options( teal.log_level = "TRACE", teal.show_js_log = TRUE, # teal.bs_theme = bslib::bs_theme(version = 5), shiny.bookmarkStore = "server" ) library(scda) pkgload::load_all("teal") make_data <- function(datanames = c("ADSL", "ADTTE")) { data_obj <- teal.data::teal_data() if ("ADSL" %in% datanames) { data_obj <- within(data_obj, ADSL <- teal.data::rADSL) } if ("ADTTE" %in% datanames) { data_obj <- within(data_obj, ADTTE <- teal.data::rADTTE) } join_keys(data_obj) <- default_cdisc_join_keys[datanames] teal.data::datanames(data_obj) <- datanames data_obj } ui_data <- function(id) { ns <- NS(id) tagList( selectizeInput( ns("errortype"), label = "Error Type", choices = c( "ok", "insufficient datasets", "no data", "qenv.error", "error in reactive", "validate error", "silent.shiny.error", "not a reactive" ) ), actionButton(ns("submit"), "Go!") ) } srv_data <- function(id, ...) { moduleServer(id, function(input, output, session) { logger::log_trace("example_module_transform2 initializing.") eventReactive(input$submit, { switch(input$errortype, ok = make_data(), `insufficient datasets` = make_data(datanames = "ADSL"), `no data` = teal_data(), qenv.error = within(data(), stop("\nthis is qenv.error in teal_data_module\n")), `error in reactive` = stop("\nerror in a reactive in teal_data_module\n"), `validate error` = validate(need(FALSE, "\nvalidate error in teal_data_module\n")), `silent.shiny.error` = req(FALSE) ) }) }) } modules <- modules( teal.modules.general::tm_data_table("Data Table"), example_module("Example Module", datanames = "ADTTE"), module( ui = function(id) { ns <- NS(id) tagList( tableOutput(ns("filter_summary")) ) }, server = function(id, datasets) { moduleServer(id, function(input, output, session) { output$filter_summary <- renderTable({ datasets$get_filter_overview(datanames = datasets$datanames()) }) }) } ) ) shinyApp( ui = function(request) { fluidPage( ui_data("data"), ui_teal(id = "teal", modules = modules) ) }, server = function(input, output, session) { data_rv <- srv_data("data", data = data, modules = modules) srv_teal(id = "teal", data = data_rv, modules = modules) } ) ```
--------- Signed-off-by: Vedha Viyash <49812166+vedhav@users.noreply.github.com> Signed-off-by: Marcin <133694481+m7pr@users.noreply.github.com> Co-authored-by: vedhav Co-authored-by: Vedha Viyash <49812166+vedhav@users.noreply.github.com> Co-authored-by: m7pr Co-authored-by: Marcin <133694481+m7pr@users.noreply.github.com> Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: 27856297+dependabot-preview[bot]@users.noreply.github.com <27856297+dependabot-preview[bot]@users.noreply.github.com> --- NAMESPACE | 1 - R/init.R | 1 - R/module_filter_data.R | 13 +- R/module_init_data.R | 110 ++------ R/module_nested_tabs.R | 145 +++++++--- R/module_teal.R | 101 +++++-- R/module_teal_data.R | 252 ++++++++++-------- R/module_teal_with_splash.R | 2 +- R/module_transform_data.R | 13 +- R/utils.R | 3 + man/dot-fallback_on_failure.Rd | 25 -- man/module_filter_data.Rd | 3 + man/module_init_data.Rd | 24 +- man/module_teal.Rd | 18 +- man/module_teal_data.Rd | 30 ++- man/module_teal_module.Rd | 16 +- man/module_teal_with_splash.Rd | 5 +- man/module_transform_data.Rd | 12 +- tests/testthat/test-module_teal.R | 111 ++++---- tests/testthat/test-shinytest2-filter_panel.R | 2 +- .../test-shinytest2-teal_data_module.R | 20 +- 21 files changed, 495 insertions(+), 412 deletions(-) delete mode 100644 man/dot-fallback_on_failure.Rd diff --git a/NAMESPACE b/NAMESPACE index a42856456d..e4c3a538d9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,7 +10,6 @@ S3method(srv_teal_module,default) S3method(srv_teal_module,teal_module) S3method(srv_teal_module,teal_modules) S3method(ui_teal_module,default) -S3method(ui_teal_module,shiny.tag) S3method(ui_teal_module,teal_module) S3method(ui_teal_module,teal_modules) S3method(within,teal_data_module) diff --git a/R/init.R b/R/init.R index e6cb66953b..a62034eaf8 100644 --- a/R/init.R +++ b/R/init.R @@ -238,7 +238,6 @@ init <- function(data, ui = function(request) { ui_teal( id = ns("teal"), - data = if (inherits(data, "teal_data_module")) data, modules = modules, title = title, header = header, diff --git a/R/module_filter_data.R b/R/module_filter_data.R index c9ea56ddae..959fd867ee 100644 --- a/R/module_filter_data.R +++ b/R/module_filter_data.R @@ -61,7 +61,7 @@ srv_filter_data <- function(id, datasets, active_datanames, data_rv, is_active) "lockEnvironment(.raw_data) #@linksto .raw_data" # this is environment and it is shared by qenvs. CAN'T MODIFY! ) ) - filtered_code <- teal.slice::get_filter_expr(datasets = datasets, datanames = datanames) + filtered_code <- .get_filter_expr(datasets = datasets, datanames = datanames) filtered_teal_data <- .append_evaluated_code(data, filtered_code) filtered_datasets <- sapply(datanames, function(x) datasets$get_data(x, filtered = TRUE), simplify = FALSE) filtered_teal_data <- .append_modified_data(filtered_teal_data, filtered_datasets) @@ -75,7 +75,7 @@ srv_filter_data <- function(id, datasets, active_datanames, data_rv, is_active) req(inherits(datasets(), "FilteredData")) new_signature <- c( teal.data::get_code(data_rv()), - teal.slice::get_filter_expr(datasets = datasets(), datanames = active_datanames()) + .get_filter_expr(datasets = datasets(), datanames = active_datanames()) ) if (!identical(previous_signature(), new_signature)) { previous_signature(new_signature) @@ -100,3 +100,12 @@ srv_filter_data <- function(id, datasets, active_datanames, data_rv, is_active) trigger_data } + +#' @rdname module_filter_data +.get_filter_expr <- function(datasets, datanames) { + if (length(datanames)) { + teal.slice::get_filter_expr(datasets = datasets, datanames = datanames) + } else { + NULL + } +} diff --git a/R/module_init_data.R b/R/module_init_data.R index e79909a1f9..8c09936a75 100644 --- a/R/module_init_data.R +++ b/R/module_init_data.R @@ -25,14 +25,11 @@ #' @inheritParams init #' #' @param data (`teal_data`, `teal_data_module`, or `reactive` returning `teal_data`) -#' The `ui` component of this module does not require `data` if `teal_data_module` is not provided. -#' The `data` argument in the `ui` is included solely for the `$ui` function of the -#' `teal_data_module`. Otherwise, it can be disregarded, ensuring that `ui_teal` does not depend on -#' the reactive data of the enclosing application. +#' The data which application will depend on. #' #' @return A `reactive` object that returns: -#' - `teal_data` when the object is validated -#' - `shiny.silent.error` when not validated. +#' Output of the `data`. If `data` fails then returned error is handled (after [tryCatch()]) so that +#' rest of the application can respond to this respectively. #' #' @rdname module_init_data #' @name module_init_data @@ -40,104 +37,55 @@ NULL #' @rdname module_init_data -ui_init_data <- function(id, data) { +ui_init_data <- function(id) { ns <- shiny::NS(id) shiny::div( id = ns("content"), - style = "display: inline-block;", - if (inherits(data, "teal_data_module")) { - ui_teal_data(ns("teal_data_module"), data_module = data) - } else { - NULL - } + style = "display: inline-block; width: 100%;", + uiOutput(ns("data")) ) } #' @rdname module_init_data -srv_init_data <- function(id, data, modules, filter = teal_slices()) { +srv_init_data <- function(id, data) { 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") - checkmate::assert_class(filter, "teal_slices") + checkmate::assert_multi_class(data, c("teal_data", "teal_data_module", "reactive")) moduleServer(id, function(input, output, session) { logger::log_debug("srv_data initializing.") - - if (getOption("teal.show_js_log", default = FALSE)) { - shinyjs::showLog() - } - # 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( - "teal_data_module", - data = reactive(req(FALSE)), # to .fallback_on_failure to shiny.silent.error - data_module = data, - modules = modules, - validate_shiny_silent_error = FALSE - ) + data_out <- if (inherits(data, "teal_data_module")) { + output$data <- renderUI(data$ui(id = session$ns("teal_data_module"))) + data$server("teal_data_module") } else if (inherits(data, "teal_data")) { reactiveVal(data) } else if (test_reactive(data)) { - .fallback_on_failure(this = data, that = reactive(req(FALSE)), label = "Reactive data") - } - - if (inherits(data, "teal_data_module")) { - shinyjs::disable(selector = sprintf(".teal-body:has('#%s') .nav li a", session$ns("content"))) + data } - observeEvent(data_validated(), { - showNotification("Data loaded successfully.", duration = 5) - shinyjs::enable(selector = sprintf(".teal-body:has('#%s') .nav li a", session$ns("content"))) - if (isTRUE(attr(data, "once"))) { - # Hiding the data module tab. - shinyjs::hide( - selector = sprintf( - ".teal-body:has('#%s') a[data-value='teal_data_module']", - session$ns("content") - ) - ) - # Clicking the second tab, which is the first module. - shinyjs::runjs( - sprintf( - "document.querySelector('.teal-body:has(#%s) .nav li:nth-child(2) a').click();", - session$ns("content") - ) - ) - } + data_handled <- reactive({ + tryCatch(data_out(), error = function(e) e) + }) - is_filter_ok <- check_filter_datanames(filter, ls(teal.code::get_env(data_validated()))) - if (!isTRUE(is_filter_ok)) { - showNotification( - "Some filters were not applied because of incompatibility with data. Contact app developer.", - type = "warning", - duration = 10 + # We want to exclude teal_data_module elements from bookmarking as they might have some secrets + observeEvent(data_handled(), { + if (inherits(data_handled(), "teal_data")) { + app_session <- .subset2(shiny::getDefaultReactiveDomain(), "parent") + setBookmarkExclude( + session$ns( + grep( + pattern = "teal_data_module-", + x = names(reactiveValuesToList(input)), + value = TRUE + ) + ), + session = app_session ) - warning(is_filter_ok) } }) - observeEvent(data_validated(), once = TRUE, { - # Excluding the ids from teal_data_module using full namespace and global shiny app session. - app_session <- .subset2(shiny::getDefaultReactiveDomain(), "parent") - setBookmarkExclude( - session$ns( - grep( - pattern = "teal_data_module-", - x = names(reactiveValuesToList(input)), - value = TRUE - ) - ), - session = app_session - ) - }) - - # Adds signature protection to the datanames in the data - reactive({ - req(data_validated()) - .add_signature_to_data(data_validated()) - }) + data_handled }) } diff --git a/R/module_nested_tabs.R b/R/module_nested_tabs.R index 76ead65b33..cae7d9acca 100644 --- a/R/module_nested_tabs.R +++ b/R/module_nested_tabs.R @@ -20,6 +20,13 @@ #' When `datasets` is passed from the parent module (`srv_teal`) then `dataset` is a singleton #' which implies in filter-panel to be "global". When `NULL` then filter-panel is "module-specific". #' +#' @param data_load_status (`reactive` returning `character`) +#' Determines action dependent on a data loading status: +#' - `"ok"` when `teal_data` is returned from the data loading. +#' - `"teal_data_module failed"` when [teal_data_module()] didn't return `teal_data`. Disables tabs buttons. +#' - `"external failed"` when a `reactive` passed to `srv_teal(data)` didn't return `teal_data`. Hides the whole tab +#' panel. +#' #' @return #' output of currently active module. #' - `srv_teal_module.teal_module` returns `reactiveVal` containing output of the called module. @@ -45,54 +52,64 @@ ui_teal_module.default <- function(id, modules, depth = 0L) { #' @export ui_teal_module.teal_modules <- function(id, modules, depth = 0L) { ns <- NS(id) - do.call( - tabsetPanel, - c( - # by giving an id, we can reactively respond to tab changes - list( - id = ns("active_tab"), - type = if (modules$label == "root") "pills" else "tabs" - ), - lapply( - names(modules$children), - function(module_id) { - module_label <- modules$children[[module_id]]$label - if (is.null(module_label)) { - module_label <- icon("fas fa-database") - } - tabPanel( - title = module_label, - value = module_id, # when clicked this tab value changes input$ - ui_teal_module( - id = ns(module_id), - modules = modules$children[[module_id]], - depth = depth + 1L + tags$div( + id = ns("wrapper"), + do.call( + tabsetPanel, + c( + # by giving an id, we can reactively respond to tab changes + list( + id = ns("active_tab"), + type = if (modules$label == "root") "pills" else "tabs" + ), + lapply( + names(modules$children), + function(module_id) { + module_label <- modules$children[[module_id]]$label + if (is.null(module_label)) { + module_label <- icon("fas fa-database") + } + tabPanel( + title = module_label, + value = module_id, # when clicked this tab value changes input$ + ui_teal_module( + id = ns(module_id), + modules = modules$children[[module_id]], + depth = depth + 1L + ) ) - ) - } + } + ) ) ) ) } -#' @rdname module_teal_module -#' @export -ui_teal_module.shiny.tag <- function(id, modules, depth = 0L) { - modules -} - #' @rdname module_teal_module #' @export ui_teal_module.teal_module <- function(id, modules, depth = 0L) { ns <- NS(id) args <- c(list(id = ns("module")), modules$ui_args) - ui_teal <- div( + ui_teal <- tagList( div( - class = "teal_validated", + id = ns("validate_datanames"), ui_validate_reactive_teal_data(ns("validate_datanames")) ), - do.call(modules$ui, args) + shinyjs::hidden( + tags$div( + id = ns("transformer_failure_info"), + class = "teal_validated", + div( + class = "teal-output-warning", + "One of transformers failed. Please fix and continue." + ) + ) + ), + tags$div( + id = ns("teal_module_ui"), + do.call(modules$ui, args) + ) ) div( @@ -115,7 +132,13 @@ ui_teal_module.teal_module <- function(id, modules, depth = 0L) { ) ) } else { - ui_teal + div( + div( + class = "teal_validated", + uiOutput(ns("data_input_error")) + ), + ui_teal + ) } ) ) @@ -128,6 +151,7 @@ srv_teal_module <- function(id, datasets = NULL, slices_global, reporter = teal.reporter::Reporter$new(), + data_load_status = reactive("ok"), is_active = reactive(TRUE)) { checkmate::assert_string(id) assert_reactive(data_rv) @@ -135,6 +159,7 @@ srv_teal_module <- function(id, assert_reactive(datasets, null.ok = TRUE) checkmate::assert_class(slices_global, ".slicesGlobal") checkmate::assert_class(reporter, "Reporter") + assert_reactive(data_load_status) UseMethod("srv_teal_module", modules) } @@ -146,6 +171,7 @@ srv_teal_module.default <- function(id, datasets = NULL, slices_global, reporter = teal.reporter::Reporter$new(), + data_load_status = reactive("ok"), is_active = reactive(TRUE)) { stop("Modules class not supported: ", paste(class(modules), collapse = " ")) } @@ -158,10 +184,26 @@ srv_teal_module.teal_modules <- function(id, datasets = NULL, slices_global, reporter = teal.reporter::Reporter$new(), + data_load_status = reactive("ok"), is_active = reactive(TRUE)) { moduleServer(id = id, module = function(input, output, session) { logger::log_debug("srv_teal_module.teal_modules initializing the module { deparse1(modules$label) }.") + observeEvent(data_load_status(), { + tabs_selector <- sprintf("#%s li a", session$ns("active_tab")) + if (identical(data_load_status(), "ok")) { + logger::log_debug("srv_teal_module@1 enabling modules tabs.") + shinyjs::show("wrapper") + shinyjs::enable(selector = tabs_selector) + } else if (identical(data_load_status(), "teal_data_module failed")) { + logger::log_debug("srv_teal_module@1 disabling modules tabs.") + shinyjs::disable(selector = tabs_selector) + } else if (identical(data_load_status(), "external failed")) { + logger::log_debug("srv_teal_module@1 hiding modules tabs.") + shinyjs::hide("wrapper") + } + }) + modules_output <- sapply( names(modules$children), function(module_id) { @@ -190,17 +232,17 @@ srv_teal_module.teal_module <- function(id, datasets = NULL, slices_global, reporter = teal.reporter::Reporter$new(), + data_load_status = reactive("ok"), is_active = reactive(TRUE)) { logger::log_debug("srv_teal_module.teal_module initializing the module: { deparse1(modules$label) }.") moduleServer(id = id, module = function(input, output, session) { - active_datanames <- reactive(.resolve_module_datanames(data = data_rv(), modules = modules)) + active_datanames <- reactive({ + .resolve_module_datanames(data = data_rv(), modules = modules) + }) if (is.null(datasets)) { datasets <- eventReactive(data_rv(), { - if (!inherits(data_rv(), "teal_data")) { - stop("data_rv must be teal_data object.") - } + req(inherits(data_rv(), "teal_data")) logger::log_debug("srv_teal_module@1 initializing module-specific FilteredData") - teal_data_to_filtered_data(data_rv(), datanames = active_datanames()) }) } @@ -219,20 +261,37 @@ srv_teal_module.teal_module <- function(id, is_active = is_active ) + is_transformer_failed <- reactiveValues() transformed_teal_data <- srv_transform_data( "data_transform", data = filtered_teal_data, transforms = modules$transformers, - modules = modules + modules = modules, + is_transformer_failed = is_transformer_failed ) + any_transformer_failed <- reactive({ + any(unlist(reactiveValuesToList(is_transformer_failed))) + }) + observeEvent(any_transformer_failed(), { + if (isTRUE(any_transformer_failed())) { + shinyjs::hide("teal_module_ui") + shinyjs::hide("validate_datanames") + shinyjs::show("transformer_failure_info") + } else { + shinyjs::show("teal_module_ui") + shinyjs::show("validate_datanames") + shinyjs::hide("transformer_failure_info") + } + }) module_teal_data <- reactive({ + req(inherits(transformed_teal_data(), "teal_data")) all_teal_data <- transformed_teal_data() module_datanames <- .resolve_module_datanames(data = all_teal_data, modules = modules) .subset_teal_data(all_teal_data, module_datanames) }) - module_teal_data_validated <- srv_validate_reactive_teal_data( + srv_validate_reactive_teal_data( "validate_datanames", data = module_teal_data, modules = modules @@ -247,9 +306,9 @@ srv_teal_module.teal_module <- function(id, # wait for module_teal_data() to be not NULL but only once: ignoreNULL = TRUE, once = TRUE, - eventExpr = module_teal_data_validated(), + eventExpr = module_teal_data(), handlerExpr = { - module_out(.call_teal_module(modules, datasets, module_teal_data_validated, reporter)) + module_out(.call_teal_module(modules, datasets, module_teal_data, reporter)) } ) } else { diff --git a/R/module_teal.R b/R/module_teal.R index 796a306966..8624636dd2 100644 --- a/R/module_teal.R +++ b/R/module_teal.R @@ -22,6 +22,13 @@ #' - data filtering in [`module_filter_data`] #' - data transformation in [`module_transform_data`] #' +#' ## Fallback on failure +#' +#' `teal` is designed in such way that app will never crash if the error is introduced in any +#' custom `shiny` module provided by app developer (e.g. [teal_data_module()], [teal_transform_module()]). +#' If any module returns a failing object, the app will halt the evaluation and display a warning message. +#' App user should always have a chance to fix the improper input and continue without restarting the session. +#' #' @rdname module_teal #' @name module_teal #' @@ -35,12 +42,10 @@ NULL #' @export ui_teal <- function(id, modules, - data = NULL, title = build_app_title(), header = tags$p(), footer = tags$p()) { checkmate::assert_character(id, max.len = 1, any.missing = FALSE) - checkmate::assert_multi_class(data, "teal_data_module", null.ok = TRUE) checkmate::assert( .var.name = "title", checkmate::check_string(title), @@ -85,13 +90,6 @@ ui_teal <- function(id, ) ) - bookmark_panel_ui <- ui_bookmark_panel(ns("bookmark_manager"), modules) - data_elem <- ui_init_data(ns("data"), data = data) - if (!is.null(data)) { - modules$children <- c(list(teal_data_module = data_elem), modules$children) - } - tabs_elem <- ui_teal_module(id = ns("teal_modules"), modules = modules) - fluidPage( id = id, title = title, @@ -103,12 +101,12 @@ ui_teal <- function(id, tags$div( id = ns("tabpanel_wrapper"), class = "teal-body", - tabs_elem + ui_teal_module(id = ns("teal_modules"), modules = modules) ), tags$div( id = ns("options_buttons"), style = "position: absolute; right: 10px;", - bookmark_panel_ui, + ui_bookmark_panel(ns("bookmark_manager"), modules), tags$button( class = "btn action-button filter_hamburger", # see sidebar.css for style filter_hamburger href = "javascript:void(0)", @@ -149,13 +147,17 @@ ui_teal <- function(id, #' @export srv_teal <- 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_multi_class(data, c("teal_data", "teal_data_module", "reactive")) checkmate::assert_class(modules, "teal_modules") checkmate::assert_class(filter, "teal_slices") moduleServer(id, function(input, output, session) { logger::log_debug("srv_teal initializing.") + if (getOption("teal.show_js_log", default = FALSE)) { + shinyjs::showLog() + } + srv_teal_lockfile("lockfile") output$identifier <- renderText( @@ -184,17 +186,77 @@ srv_teal <- function(id, data, modules, filter = teal_slices()) { } ) - data_rv <- srv_init_data("data", data = data, modules = modules, filter = filter) + data_pulled <- srv_init_data("data", data = data) + data_validated <- srv_validate_reactive_teal_data( + "validate", + data = data_pulled, + modules = modules, + validate_shiny_silent_error = FALSE + ) + data_rv <- reactive({ + req(inherits(data_validated(), "teal_data")) + is_filter_ok <- check_filter_datanames(filter, ls(teal.code::get_env(data_validated()))) + if (!isTRUE(is_filter_ok)) { + showNotification( + "Some filters were not applied because of incompatibility with data. Contact app developer.", + type = "warning", + duration = 10 + ) + warning(is_filter_ok) + } + .add_signature_to_data(data_validated()) + }) + + data_load_status <- reactive({ + if (inherits(data_pulled(), "teal_data")) { + "ok" + } else if (inherits(data, "teal_data_module")) { + "teal_data_module failed" + } else { + "external failed" + } + }) + datasets_rv <- if (!isTRUE(attr(filter, "module_specific"))) { eventReactive(data_rv(), { - if (!inherits(data_rv(), "teal_data")) { - stop("data_rv must be teal_data object.") - } + req(inherits(data_rv(), "teal_data")) logger::log_debug("srv_teal@1 initializing FilteredData") teal_data_to_filtered_data(data_rv()) }) } + if (inherits(data, "teal_data_module")) { + setBookmarkExclude(c("teal_modules-active_tab")) + shiny::insertTab( + inputId = "teal_modules-active_tab", + position = "before", + select = TRUE, + tabPanel( + title = icon("fas fa-database"), + value = "teal_data_module", + tags$div( + ui_init_data(session$ns("data")), + ui_validate_reactive_teal_data(session$ns("validate")) + ) + ) + ) + + if (attr(data, "once")) { + observeEvent(data_rv(), once = TRUE, { + logger::log_debug("srv_teal@2 removing data tab.") + # when once = TRUE we pull data once and then remove data tab + removeTab("teal_modules-active_tab", target = "teal_data_module") + }) + } + } else { + # when no teal_data_module then we want to display messages above tabsetPanel (because there is no data-tab) + insertUI( + selector = sprintf("#%s", session$ns("tabpanel_wrapper")), + where = "beforeBegin", + ui = tags$div(ui_validate_reactive_teal_data(session$ns("validate")), tags$br()) + ) + } + module_labels <- unlist(module_labels(modules), use.names = FALSE) slices_global <- methods::new(".slicesGlobal", filter, module_labels) modules_output <- srv_teal_module( @@ -202,15 +264,12 @@ srv_teal <- function(id, data, modules, filter = teal_slices()) { data_rv = data_rv, datasets = datasets_rv, modules = modules, - slices_global = slices_global + slices_global = slices_global, + data_load_status = data_load_status ) mapping_table <- srv_filter_manager_panel("filter_manager_panel", slices_global = slices_global) snapshots <- srv_snapshot_manager_panel("snapshot_manager_panel", slices_global = slices_global) srv_bookmark_panel("bookmark_manager", modules) - - if (inherits(data, "teal_data_module")) { - setBookmarkExclude(c("teal_modules-active_tab")) - } }) invisible(NULL) diff --git a/R/module_teal_data.R b/R/module_teal_data.R index 943f635136..899ef14028 100644 --- a/R/module_teal_data.R +++ b/R/module_teal_data.R @@ -16,19 +16,18 @@ #' 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 -#' +#' returned. If error 2-4 occurs, relevant error message is displayed to the app user. Once the issue is +#' resolved, the app will continue to run. `teal` guarantees that errors in data don't crash the app +#' (except error 1). #' #' @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`. +#' @param is_transformer_failed (`reactiveValues`) contains `logical` flags named after each transformer. +#' Help to determine if any previous transformer failed, so that following transformers can be disabled +#' and display a generic failure message. #' #' @return `reactive` `teal_data` #' @@ -38,55 +37,81 @@ NULL #' @rdname module_teal_data -ui_teal_data <- function(id, data_module) { +ui_teal_data <- function(id, data_module = function(id) NULL) { checkmate::assert_string(id) - checkmate::assert_class(data_module, "teal_data_module") + checkmate::assert_function(data_module, args = "id") ns <- NS(id) + shiny::tagList( - data_module$ui(id = ns("data")), + tags$div(id = ns("wrapper"), data_module(id = ns("data"))), ui_validate_reactive_teal_data(ns("validate")) ) } #' @rdname module_teal_data srv_teal_data <- function(id, - data, - data_module, + data_module = function(id) NULL, modules = NULL, - validate_shiny_silent_error = TRUE) { + validate_shiny_silent_error = TRUE, + is_transformer_failed = reactiveValues()) { checkmate::assert_string(id) - checkmate::assert_class(data_module, "teal_data_module") + checkmate::assert_function(data_module, args = "id") checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"), null.ok = TRUE) + checkmate::assert_class(is_transformer_failed, "reactivevalues") moduleServer(id, function(input, output, session) { logger::log_debug("srv_teal_data initializing.") + is_transformer_failed[[id]] <- FALSE + data_out <- data_module(id = "data") + data_handled <- reactive(tryCatch(data_out(), error = function(e) e)) + observeEvent(data_handled(), { + if (!inherits(data_handled(), "teal_data")) { + is_transformer_failed[[id]] <- TRUE + } else { + is_transformer_failed[[id]] <- FALSE + } + }) - data_out <- if (is_arg_used(data_module$server, "data")) { - data_module$server(id = "data", data = data) - } else { - data_module$server(id = "data") - } + is_previous_failed <- reactive({ + idx_this <- which(names(is_transformer_failed) == id) + is_transformer_failed_list <- reactiveValuesToList(is_transformer_failed) + idx_failures <- which(unlist(is_transformer_failed_list)) + any(idx_failures < idx_this) + }) - data_validated <- srv_validate_reactive_teal_data( - id = "validate", - data = data_out, - modules = modules, - validate_shiny_silent_error = validate_shiny_silent_error - ) + observeEvent(is_previous_failed(), { + if (is_previous_failed()) { + shinyjs::disable("wrapper") + } else { + shinyjs::enable("wrapper") + } + }) - .fallback_on_failure( - this = data_validated, - that = data, - label = sprintf("Data element '%s' for module '%s'", id, modules$label) + srv_validate_reactive_teal_data( + "validate", + data = data_handled, + modules = modules, + validate_shiny_silent_error = validate_shiny_silent_error, + hide_validation_error = is_previous_failed ) }) } #' @rdname module_teal_data ui_validate_reactive_teal_data <- function(id) { + ns <- NS(id) tagList( - uiOutput(NS(id, "shiny_errors")), - uiOutput(NS(id, "shiny_warnings")) + div( + id = ns("validate_messages"), + class = "teal_validated", + ui_validate_error(ns("silent_error")), + ui_check_class_teal_data(ns("class_teal_data")), + ui_check_shiny_warnings(ns("shiny_warnings")) + ), + div( + class = "teal_validated", + uiOutput(ns("previous_failed")) + ) ) } @@ -94,118 +119,133 @@ ui_validate_reactive_teal_data <- function(id) { srv_validate_reactive_teal_data <- function(id, # nolint: object_length data, modules = NULL, - validate_shiny_silent_error = FALSE) { + validate_shiny_silent_error = FALSE, + hide_validation_error = reactive(FALSE)) { checkmate::assert_string(id) checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"), null.ok = TRUE) checkmate::assert_flag(validate_shiny_silent_error) moduleServer(id, function(input, output, session) { - data_out_r <- reactive(tryCatch(data(), error = function(e) e)) + # there is an empty reactive cycle on `init` and `data_rv` has `shiny.silent.error` class + srv_validate_error("silent_error", data, validate_shiny_silent_error) + srv_check_class_teal_data("class_teal_data", data) + srv_check_shiny_warnings("shiny_warnings", data, modules) + output$previous_failed <- renderUI({ + if (hide_validation_error()) { + shinyjs::hide("validate_messages") + tags$div("One of previous transformers failed. Please fix and continue.", class = "teal-output-warning") + } else { + shinyjs::show("validate_messages") + NULL + } + }) - data_validated <- reactive({ - # custom module can return error - data_out <- data_out_r() + .trigger_on_success(data) + }) +} - # 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( - "Shiny error when executing the `data` module", - "Check your inputs or contact app developer if error persists.", - collapse = "\n" - ) - ) - ) - } - } +#' @keywords internal +ui_validate_error <- function(id) { + ns <- NS(id) + uiOutput(ns("message")) +} - # to handle errors and qenv.error(s) - if (inherits(data_out, c("qenv.error", "error"))) { +#' @keywords internal +srv_validate_error <- function(id, data, validate_shiny_silent_error) { + checkmate::assert_string(id) + checkmate::assert_flag(validate_shiny_silent_error) + moduleServer(id, function(input, output, session) { + output$message <- renderUI({ + is_shiny_silent_error <- inherits(data(), "shiny.silent.error") && identical(data()$message, "") + if (inherits(data(), "qenv.error")) { validate( need( FALSE, - paste0( + paste( "Error when executing the `data` module:", - strip_style(paste(data_out$message, collapse = "\n")), - "Check your inputs or contact app developer if error persists.", + strip_style(paste(data()$message, collapse = "\n")), + "\nCheck your inputs or contact app developer if error persists.", collapse = "\n" ) ) ) + } else if (inherits(data(), "error")) { + if (is_shiny_silent_error && !validate_shiny_silent_error) { + return(NULL) + } + validate( + need( + FALSE, + sprintf( + "Shiny error when executing the `data` module.\n%s\n%s", + data()$message, + "Check your inputs or contact app developer if error persists." + ) + ) + ) } + }) + }) +} + +#' @keywords internal +ui_check_class_teal_data <- function(id) { + ns <- NS(id) + uiOutput(ns("message")) +} + +#' @keywords internal +srv_check_class_teal_data <- function(id, data) { + checkmate::assert_string(id) + moduleServer(id, function(input, output, session) { + output$message <- renderUI({ validate( need( - checkmate::test_class(data_out, "teal_data"), - paste0( - "Assertion on return value from the 'data' module failed:", - checkmate::test_class(data_out, "teal_data"), - "Check your inputs or contact app developer if error persists.", - collapse = "\n" - ) + inherits(data(), c("teal_data", "error")), + "Did not receive `teal_data` object. Cannot proceed further." ) ) - - data_out }) + }) +} - output$shiny_errors <- renderUI({ - data_validated() - NULL - }) +#' @keywords internal +ui_check_shiny_warnings <- function(id) { + ns <- NS(id) + uiOutput(NS(id, "message")) +} - output$shiny_warnings <- renderUI({ - if (inherits(data_out_r(), "teal_data")) { - is_modules_ok <- check_modules_datanames( - modules = modules, - datanames = ls(teal.code::get_env(data_validated())) - ) +#' @keywords internal +srv_check_shiny_warnings <- function(id, data, modules) { + checkmate::assert_string(id) + moduleServer(id, function(input, output, session) { + output$message <- renderUI({ + if (inherits(data(), "teal_data")) { + is_modules_ok <- check_modules_datanames(modules = modules, datanames = ls(teal.code::get_env(data()))) if (!isTRUE(is_modules_ok)) { tags$div( + class = "teal-output-warning", is_modules_ok$html( # Show modules prefix on message only in teal_data_module tab grepl(sprintf("data-teal_data_module-%s", id), session$ns(NULL), fixed = TRUE) - ), - 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) { - assert_reactive(this) - assert_reactive(that) - 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.") - that() +.trigger_on_success <- function(data) { + out <- reactiveVal(NULL) + observeEvent(data(), { + if (inherits(data(), "teal_data")) { + if (!identical(data(), out())) { + out(data()) + } } }) + + out } diff --git a/R/module_teal_with_splash.R b/R/module_teal_with_splash.R index c7404e7920..c4f29c0ae3 100644 --- a/R/module_teal_with_splash.R +++ b/R/module_teal_with_splash.R @@ -24,7 +24,7 @@ ui_teal_with_splash <- function(id, what = "ui_teal_with_splash()", details = "Deprecated, please use `ui_teal` instead" ) - ui_teal(id = id, data = data, title = title, header = header, footer = footer) + ui_teal(id = id, title = title, header = header, footer = footer) } #' @export diff --git a/R/module_transform_data.R b/R/module_transform_data.R index 30107ef88d..418c81d06d 100644 --- a/R/module_transform_data.R +++ b/R/module_transform_data.R @@ -44,7 +44,7 @@ ui_transform_data <- function(id, transforms, class = "well") { ), div( id = wrapper_id, - ui_teal_data(id = ns(name), data_module = transforms[[name]]) + ui_teal_data(id = ns(name), data_module = transforms[[name]]$ui) ) ) } @@ -52,29 +52,26 @@ ui_transform_data <- function(id, transforms, class = "well") { } #' @rdname module_transform_data -srv_transform_data <- function(id, data, transforms, modules) { +srv_transform_data <- function(id, data, transforms, modules, is_transformer_failed = reactiveValues()) { checkmate::assert_string(id) assert_reactive(data) 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 + data_module = function(id) transforms[[name]]$server(id, previous_result), + modules = modules, + is_transformer_failed = is_transformer_failed ) }, x = names(transforms), diff --git a/R/utils.R b/R/utils.R index d2bedef4e3..e5830bf0ca 100644 --- a/R/utils.R +++ b/R/utils.R @@ -139,6 +139,9 @@ check_modules_datanames <- function(modules, datanames) { if (inherits(modules, "teal_modules")) { result <- lapply(modules$children, function(module) recursive_check_datanames(module, datanames = datanames)) result <- result[vapply(result, Negate(is.null), logical(1L))] + if (length(result) == 0) { + return(NULL) + } list( string = do.call(c, as.list(unname(sapply(result, function(x) x$string)))), html = function(with_module_name = TRUE) { diff --git a/man/dot-fallback_on_failure.Rd b/man/dot-fallback_on_failure.Rd deleted file mode 100644 index 5d8f168e2d..0000000000 --- a/man/dot-fallback_on_failure.Rd +++ /dev/null @@ -1,25 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/module_teal_data.R -\name{.fallback_on_failure} -\alias{.fallback_on_failure} -\title{Fallback on failure} -\usage{ -.fallback_on_failure(this, that, label) -} -\arguments{ -\item{this}{(\code{reactive}) Current reactive.} - -\item{that}{(\code{reactive}) Previous reactive.} - -\item{label}{(\code{character}) Label for identifying problematic \code{teal_data_module} transform in logging.} -} -\value{ -\code{reactive} \code{teal_data} -} -\description{ -Function returns the previous reactive if the current reactive is invalid (throws error or returns NULL). -Application: In \code{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 \code{teal} reactive -tree elements). -} -\keyword{internal} diff --git a/man/module_filter_data.Rd b/man/module_filter_data.Rd index e2dfe310f4..61d527b9ef 100644 --- a/man/module_filter_data.Rd +++ b/man/module_filter_data.Rd @@ -6,6 +6,7 @@ \alias{srv_filter_data} \alias{.make_filtered_teal_data} \alias{.observe_active_filter_changed} +\alias{.get_filter_expr} \title{Filter panel module in teal} \usage{ ui_filter_data(id) @@ -15,6 +16,8 @@ srv_filter_data(id, datasets, active_datanames, data_rv, is_active) .make_filtered_teal_data(modules, data, datasets = NULL, datanames) .observe_active_filter_changed(datasets, is_active, active_datanames, data_rv) + +.get_filter_expr(datasets, datanames) } \arguments{ \item{id}{(\code{character}) Optionally, diff --git a/man/module_init_data.Rd b/man/module_init_data.Rd index d61bb48e7a..7e9cd8b715 100644 --- a/man/module_init_data.Rd +++ b/man/module_init_data.Rd @@ -6,9 +6,9 @@ \alias{srv_init_data} \title{Data Module for teal} \usage{ -ui_init_data(id, data) +ui_init_data(id) -srv_init_data(id, data, modules, filter = teal_slices()) +srv_init_data(id, data) } \arguments{ \item{id}{(\code{character}) Optionally, @@ -16,26 +16,12 @@ a string specifying the \code{shiny} module id in cases it is used as a \code{sh 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}) -The \code{ui} component of this module does not require \code{data} if \code{teal_data_module} is not provided. -The \code{data} argument in the \code{ui} is included solely for the \verb{$ui} function of the -\code{teal_data_module}. Otherwise, it can be disregarded, ensuring that \code{ui_teal} does not depend on -the reactive data of the enclosing application.} - -\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{filter}{(\code{teal_slices}) Optionally, -specifies the initial filter using \code{\link[=teal_slices]{teal_slices()}}.} +The data which application will depend on.} } \value{ A \code{reactive} object that returns: -\itemize{ -\item \code{teal_data} when the object is validated -\item \code{shiny.silent.error} when not validated. -} +Output of the \code{data}. If \code{data} fails then returned error is handled (after \code{\link[=tryCatch]{tryCatch()}}) so that +rest of the application can respond to this respectively. } \description{ This module manages the \code{data} argument for \code{srv_teal}. The \code{teal} framework uses \code{\link[=teal_data]{teal_data()}}, diff --git a/man/module_teal.Rd b/man/module_teal.Rd index 29cab0645c..f2bc5d95b5 100644 --- a/man/module_teal.Rd +++ b/man/module_teal.Rd @@ -9,7 +9,6 @@ ui_teal( id, modules, - data = NULL, title = build_app_title(), header = tags$p(), footer = tags$p() @@ -28,12 +27,6 @@ Nested list of \code{teal_modules} or \code{teal_module} objects or a single will be displayed in the \code{teal} application. See \code{\link[=modules]{modules()}} and \code{\link[=module]{module()}} for more details.} -\item{data}{(\code{teal_data}, \code{teal_data_module}, or \code{reactive} returning \code{teal_data}) -The \code{ui} component of this module does not require \code{data} if \code{teal_data_module} is not provided. -The \code{data} argument in the \code{ui} is included solely for the \verb{$ui} function of the -\code{teal_data_module}. Otherwise, it can be disregarded, ensuring that \code{ui_teal} does not depend on -the reactive data of the enclosing application.} - \item{title}{(\code{shiny.tag} or \code{character(1)}) Optionally, 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 @@ -45,6 +38,9 @@ the header of the app.} \item{footer}{(\code{shiny.tag} or \code{character(1)}) Optionally, the footer of the app.} +\item{data}{(\code{teal_data}, \code{teal_data_module}, or \code{reactive} returning \code{teal_data}) +The data which application will depend on.} + \item{filter}{(\code{teal_slices}) Optionally, specifies the initial filter using \code{\link[=teal_slices]{teal_slices()}}.} } @@ -73,4 +69,12 @@ performed: \item data transformation in \code{\link{module_transform_data}} } } + +\subsection{Fallback on failure}{ + +\code{teal} is designed in such way that app will never crash if the error is introduced in any +custom \code{shiny} module provided by app developer (e.g. \code{\link[=teal_data_module]{teal_data_module()}}, \code{\link[=teal_transform_module]{teal_transform_module()}}). +If any module returns a failing object, the app will halt the evaluation and display a warning message. +App user should always have a chance to fix the improper input and continue without restarting the session. +} } diff --git a/man/module_teal_data.Rd b/man/module_teal_data.Rd index abcbe048ff..dee0b1087e 100644 --- a/man/module_teal_data.Rd +++ b/man/module_teal_data.Rd @@ -8,14 +8,14 @@ \alias{srv_validate_reactive_teal_data} \title{Execute and validate \code{teal_data_module}} \usage{ -ui_teal_data(id, data_module) +ui_teal_data(id, data_module = function(id) NULL) srv_teal_data( id, - data, - data_module, + data_module = function(id) NULL, modules = NULL, - validate_shiny_silent_error = TRUE + validate_shiny_silent_error = TRUE, + is_transformer_failed = reactiveValues() ) ui_validate_reactive_teal_data(id) @@ -24,7 +24,8 @@ srv_validate_reactive_teal_data( id, data, modules = NULL, - validate_shiny_silent_error = FALSE + validate_shiny_silent_error = FALSE, + hide_validation_error = reactive(FALSE) ) } \arguments{ @@ -32,13 +33,15 @@ srv_validate_reactive_teal_data( \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}.} +\item{validate_shiny_silent_error}{(\code{logical}) If \code{TRUE}, then \code{shiny.silent.error} is validated and} + +\item{is_transformer_failed}{(\code{reactiveValues}) contains \code{logical} flags named after each transformer. +Help to determine if any previous transformer failed, so that following transformers can be disabled +and display a generic failure message.} + +\item{data}{(\verb{reactive teal_data})} } \value{ \code{reactive} \code{teal_data} @@ -63,10 +66,9 @@ Output \code{data} is invalid if: } \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 +returned. If error 2-4 occurs, relevant error message is displayed to the app user. Once the issue is +resolved, the app will continue to run. \code{teal} guarantees that errors in data don't crash the app +(except error 1). } \keyword{internal} diff --git a/man/module_teal_module.Rd b/man/module_teal_module.Rd index b5bb015f81..bc46f86c61 100644 --- a/man/module_teal_module.Rd +++ b/man/module_teal_module.Rd @@ -5,7 +5,6 @@ \alias{ui_teal_module} \alias{ui_teal_module.default} \alias{ui_teal_module.teal_modules} -\alias{ui_teal_module.shiny.tag} \alias{ui_teal_module.teal_module} \alias{srv_teal_module} \alias{srv_teal_module.default} @@ -19,8 +18,6 @@ ui_teal_module(id, modules, depth = 0L) \method{ui_teal_module}{teal_modules}(id, modules, depth = 0L) -\method{ui_teal_module}{shiny.tag}(id, modules, depth = 0L) - \method{ui_teal_module}{teal_module}(id, modules, depth = 0L) srv_teal_module( @@ -30,6 +27,7 @@ srv_teal_module( datasets = NULL, slices_global, reporter = teal.reporter::Reporter$new(), + data_load_status = reactive("ok"), is_active = reactive(TRUE) ) @@ -40,6 +38,7 @@ srv_teal_module( datasets = NULL, slices_global, reporter = teal.reporter::Reporter$new(), + data_load_status = reactive("ok"), is_active = reactive(TRUE) ) @@ -50,6 +49,7 @@ srv_teal_module( datasets = NULL, slices_global, reporter = teal.reporter::Reporter$new(), + data_load_status = reactive("ok"), is_active = reactive(TRUE) ) @@ -60,6 +60,7 @@ srv_teal_module( datasets = NULL, slices_global, reporter = teal.reporter::Reporter$new(), + data_load_status = reactive("ok"), is_active = reactive(TRUE) ) } @@ -85,6 +86,15 @@ which implies in filter-panel to be "global". When \code{NULL} then filter-panel \item{slices_global}{(\code{reactiveVal} returning \code{modules_teal_slices}) see \code{\link{module_filter_manager}}} + +\item{data_load_status}{(\code{reactive} returning \code{character}) +Determines action dependent on a data loading status: +\itemize{ +\item \code{"ok"} when \code{teal_data} is returned from the data loading. +\item \code{"teal_data_module failed"} when \code{\link[=teal_data_module]{teal_data_module()}} didn't return \code{teal_data}. Disables tabs buttons. +\item \code{"external failed"} when a \code{reactive} passed to \code{srv_teal(data)} didn't return \code{teal_data}. Hides the whole tab +panel. +}} } \value{ output of currently active module. diff --git a/man/module_teal_with_splash.Rd b/man/module_teal_with_splash.Rd index 4fa6ba3e3e..98b49551fb 100644 --- a/man/module_teal_with_splash.Rd +++ b/man/module_teal_with_splash.Rd @@ -22,10 +22,7 @@ a string specifying the \code{shiny} module id in cases it is used as a \code{sh 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}) -The \code{ui} component of this module does not require \code{data} if \code{teal_data_module} is not provided. -The \code{data} argument in the \code{ui} is included solely for the \verb{$ui} function of the -\code{teal_data_module}. Otherwise, it can be disregarded, ensuring that \code{ui_teal} does not depend on -the reactive data of the enclosing application.} +The data which application will depend on.} \item{title}{(\code{shiny.tag} or \code{character(1)}) Optionally, the browser window title. Defaults to a title "teal app" with the icon of NEST. diff --git a/man/module_transform_data.Rd b/man/module_transform_data.Rd index 5dbc480880..2a4a351062 100644 --- a/man/module_transform_data.Rd +++ b/man/module_transform_data.Rd @@ -8,7 +8,13 @@ \usage{ ui_transform_data(id, transforms, class = "well") -srv_transform_data(id, data, transforms, modules) +srv_transform_data( + id, + data, + transforms, + modules, + is_transformer_failed = reactiveValues() +) } \arguments{ \item{id}{(\code{character(1)}) Module id} @@ -16,6 +22,10 @@ srv_transform_data(id, data, transforms, modules) \item{data}{(\verb{reactive teal_data})} \item{modules}{(\code{teal_modules} or \code{teal_module}) For \code{datanames} validation purpose} + +\item{is_transformer_failed}{(\code{reactiveValues}) contains \code{logical} flags named after each transformer. +Help to determine if any previous transformer failed, so that following transformers can be disabled +and display a generic failure message.} } \value{ \code{reactive} \code{teal_data} diff --git a/tests/testthat/test-module_teal.R b/tests/testthat/test-module_teal.R index 9b0be9b378..e863a1325f 100644 --- a/tests/testthat/test-module_teal.R +++ b/tests/testthat/test-module_teal.R @@ -182,7 +182,7 @@ testthat::describe("srv_teal arguments", { ), expr = NULL ), - "Must inherit from class 'teal_data'/'teal_data_module'/'reactive'/'reactiveVal'" + "Assertion on 'data' failed: Must inherit from class 'teal_data'/'teal_data_module'/'reactive', but has class 'data.frame'." # nolint: line_length ) }) @@ -217,7 +217,7 @@ testthat::describe("srv_teal teal_modules", { ) ), expr = { - testthat::expect_s4_class(data_rv(), "teal_data") + testthat::expect_error(data_init(), NULL) testthat::expect_null(modules_output$module_1()) testthat::expect_null(modules_output$module_2()) } @@ -236,7 +236,7 @@ testthat::describe("srv_teal teal_modules", { ) ), expr = { - testthat::expect_s4_class(data_rv(), "teal_data") + testthat::expect_error(data_init()) session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_identical(modules_output$module_1(), 101L) testthat::expect_null(modules_output$module_2()) @@ -259,7 +259,7 @@ testthat::describe("srv_teal teal_modules", { ) ), expr = { - testthat::expect_s4_class(data_rv(), "teal_data") + testthat::expect_error(data_init()) session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_identical(modules_output$module_1(), 101L) testthat::expect_null(modules_output$module_2()) @@ -290,7 +290,7 @@ testthat::describe("srv_teal teal_modules", { ) ), expr = { - testthat::expect_s4_class(data_rv(), "teal_data") + testthat::expect_error(data_init()) session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_identical(modules_output$module_1(), 101L) testthat::expect_null(modules_output$module_2()) @@ -313,7 +313,7 @@ testthat::describe("srv_teal teal_modules", { ) ), expr = { - testthat::expect_s4_class(data_rv(), "teal_data") + testthat::expect_error(data_init()) session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_s4_class(modules_output$module_1()(), "teal_data") } @@ -340,14 +340,14 @@ testthat::describe("srv_teal teal_modules", { ), expr = { testthat::expect_null(modules_output$module_1()) - testthat::expect_error(data_rv()) + testthat::expect_error(data_init()) session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_null(modules_output$module_1()) } ) }) - testthat::it("are not called when the teal_data_module returns validation error", { + testthat::it("are not called when teal_data_module returns validation error", { shiny::testServer( app = srv_teal, args = list( @@ -367,13 +367,14 @@ testthat::describe("srv_teal teal_modules", { ), expr = { testthat::expect_null(modules_output$module_1()) + testthat::expect_error(data_init()) session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_null(modules_output$module_1()) } ) }) - testthat::it("are not called when the teal_data_module throw en error", { + testthat::it("are not called when teal_data_module throws an error", { shiny::testServer( app = srv_teal, args = list( @@ -393,14 +394,14 @@ testthat::describe("srv_teal teal_modules", { ), expr = { testthat::expect_null(modules_output$module_1()) - testthat::expect_error(data_rv()) + testthat::expect_error(data_init()) session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_null(modules_output$module_1()) } ) }) - testthat::it("are not called when the teal_data_module returns qenv.error", { + testthat::it("are not called when teal_data_module returns qenv.error", { shiny::testServer( app = srv_teal, args = list( @@ -420,7 +421,7 @@ testthat::describe("srv_teal teal_modules", { ), expr = { testthat::expect_null(modules_output$module_1()) - testthat::expect_error(data_rv()) + testthat::expect_error(data_init()) session$setInputs(`teal_modules-active_tab` = "module_1") testthat::expect_null(modules_output$module_1()) } @@ -453,9 +454,9 @@ testthat::describe("srv_teal teal_modules", { ), expr = { testthat::expect_null(modules_output$module_1()) - session$setInputs(`data-teal_data_module-data-dataset` = "iris", `teal_modules-active_tab` = "module_1") + session$setInputs(`data-teal_data_module-dataset` = "iris", `teal_modules-active_tab` = "module_1") testthat::expect_setequal(ls(teal.code::get_env(modules_output$module_1()())), "iris") - session$setInputs(`data-teal_data_module-data-dataset` = "mtcars", `teal_modules-active_tab` = "module_2") + session$setInputs(`data-teal_data_module-dataset` = "mtcars", `teal_modules-active_tab` = "module_2") testthat::expect_setequal(ls(teal.code::get_env(modules_output$module_2()())), "mtcars") } ) @@ -487,12 +488,12 @@ testthat::describe("srv_teal teal_modules", { expr = { testthat::expect_null(modules_output$module_1()) session$setInputs( - `data-teal_data_module-data-dataset` = "iris", + `data-teal_data_module-dataset` = "iris", `teal_modules-active_tab` = "module_1" ) out <- modules_output$module_1() testthat::expect_true(!is.null(out)) - session$setInputs(`data-teal_data_module-data-dataset` = "mtcars") + session$setInputs(`data-teal_data_module-dataset` = "mtcars") testthat::expect_identical(out, modules_output$module_1()) } ) @@ -534,7 +535,7 @@ testthat::describe("srv_teal teal_modules", { trimws( rvest::html_text2( rvest::read_html( - output[["teal_modules-module_1-validate_datanames-shiny_warnings"]]$html + output[["teal_modules-module_1-validate_datanames-shiny_warnings-message"]]$html ) ) ), @@ -1577,7 +1578,6 @@ testthat::describe("srv_teal teal_module(s) transformer", { ), expr = { session$setInputs(`teal_modules-active_tab` = "module_1") - session$flushReact() data_from_transform <- modules_output$module_1()()[["data_from_transform"]] testthat::expect_identical(data_from_transform$mtcars, mtcars) expected_iris <- iris[iris$Species == "versicolor", ] @@ -1612,7 +1612,7 @@ testthat::describe("srv_teal teal_module(s) transformer", { ) }) - testthat::it("continues when transformer throws validation error and returns unchanged data", { + testthat::it("pauses when transformer throws validation error", { shiny::testServer( app = srv_teal, args = list( @@ -1635,12 +1635,12 @@ testthat::describe("srv_teal teal_module(s) transformer", { ), expr = { session$setInputs(`teal_modules-active_tab` = "module_1") - testthat::expect_identical(modules_output$module_1()()[["iris"]], iris) + testthat::expect_null(modules_output$module_1()) } ) }) - testthat::it("continues when transformer throws validation error and returns unchanged data", { + testthat::it("pauses when transformer throws validation error", { shiny::testServer( app = srv_teal, args = list( @@ -1663,73 +1663,66 @@ testthat::describe("srv_teal teal_module(s) transformer", { ), expr = { session$setInputs(`teal_modules-active_tab` = "module_1") - testthat::expect_identical(modules_output$module_1()()[["iris"]], iris) + testthat::expect_null(modules_output$module_1()) } ) }) - testthat::it("continues when transformer throws qenv error and returns unchanged data", { - testthat::skip("todo") - }) - testthat::it("upstream data change is updated on transformer fallback", { + testthat::it("pauses when transformer throws qenv error", { shiny::testServer( app = srv_teal, args = list( id = "test", - data = teal.data::teal_data(iris = iris, mtcars = mtcars), + data = teal.data::teal_data(iris = iris), modules = modules( module( label = "module_1", server = function(id, data) data, - transformers = transform_list[c("iris", "fail")] + transformers = list( + teal_transform_module( + ui = function(id) NULL, + server = function(id, data) { + reactive(within(data(), stop("my error"))) + } + ) + ) ) ) ), expr = { - session$setInputs("teal_modules-active_tab" = "module_1") - new_row_size <- 14 - session$setInputs("teal_modules-module_1-data_transform-transform_module-data-n" = new_row_size) - session$flushReact() - - testthat::expect_equal(nrow(modules_output$module_1()()[["iris"]]), new_row_size) + session$setInputs(`teal_modules-active_tab` = "module_1") + testthat::expect_null(modules_output$module_1()) } ) }) - testthat::it("upstream data change with double reactivity resolves with correct this/that", { + testthat::it("isn't called when `data` is not teal_data", { shiny::testServer( app = srv_teal, args = list( id = "test", - data = teal.data::teal_data(iris = iris, mtcars = mtcars), + data = teal.data::teal_data(iris = iris), modules = modules( module( label = "module_1", server = function(id, data) data, - transformers = transform_list[c("iris", "fail")] + transformers = list( + teal_transform_module( + ui = function(id) NULL, + server = function(id, data) { + reactive(data.frame()) + } + ) + ) ) ) ), expr = { - session$setInputs("teal_modules-active_tab" = "module_1") - - session$setInputs( - "teal_modules-module_1-data_transform-transform_module-data-n" = 12, - "teal_modules-module_1-data_transform-transform_module_1-data-add_error" = FALSE - ) - session$flushReact() - - testthat::expect_equal(nrow(modules_output$module_1()()[["iris"]]), 6) + session$setInputs(`teal_modules-active_tab` = "module_1") + testthat::expect_null(modules_output$module_1()) } ) }) - - testthat::it("continues when transformer throws qenv error and returns unchanged data") - - testthat::it("isn't called when `data` is not teal_data", { - testthat::skip("todo") - }) - # when reactive returned teal_data_module is not triggered (for example when button isn't clicked) }) testthat::describe("srv_teal summary table", { @@ -1746,7 +1739,6 @@ testthat::describe("srv_teal summary table", { ), expr = { session$setInputs("teal_modules-active_tab" = "module_1") - session$flushReact() testthat::expect_identical( module_summary_table(output, "module_1"), data.frame( @@ -1777,7 +1769,6 @@ testthat::describe("srv_teal summary table", { ), expr = { session$setInputs("teal_modules-active_tab" = "module_1") - session$flushReact() testthat::expect_identical( module_summary_table(output, "module_1"), data.frame( @@ -1810,7 +1801,6 @@ testthat::describe("srv_teal summary table", { ), expr = { session$setInputs("teal_modules-active_tab" = "module_1") - session$flushReact() testthat::expect_identical( module_summary_table(output, "module_1"), data.frame( @@ -1844,7 +1834,6 @@ testthat::describe("srv_teal summary table", { ), expr = { session$setInputs("teal_modules-active_tab" = "module_1") - session$flushReact() testthat::expect_identical( module_summary_table(output, "module_1"), data.frame( @@ -1879,7 +1868,6 @@ testthat::describe("srv_teal summary table", { ), expr = { session$setInputs("teal_modules-active_tab" = "module_1") - session$flushReact() testthat::expect_identical( module_summary_table(output, "module_1"), data.frame( @@ -1913,9 +1901,7 @@ testthat::describe("srv_teal summary table", { ), expr = { session$setInputs("teal_modules-active_tab" = "module_1") - slices_global$slices_set( - teal_slices(teal_slice("a", "name", selected = "a")) - ) + slices_global$slices_set(teal_slices(teal_slice("a", "name", selected = "a"))) session$flushReact() testthat::expect_identical( module_summary_table(output, "module_1"), @@ -1956,7 +1942,6 @@ testthat::describe("srv_teal summary table", { ), expr = { session$setInputs("teal_modules-active_tab" = "module_1") - session$flushReact() testthat::expect_identical( module_summary_table(output, "module_1"), data.frame( @@ -1986,7 +1971,6 @@ testthat::describe("srv_teal summary table", { ), expr = { session$setInputs("teal_modules-active_tab" = "module_1") - session$flushReact() testthat::expect_identical( module_summary_table(output, "module_1"), data.frame( @@ -2011,7 +1995,6 @@ testthat::describe("srv_teal summary table", { ), expr = { session$setInputs("teal_modules-active_tab" = "module_1") - session$flushReact() testthat::expect_identical( module_summary_table(output, "module_1"), data.frame( diff --git a/tests/testthat/test-shinytest2-filter_panel.R b/tests/testthat/test-shinytest2-filter_panel.R index aa65eb567d..22be061ab7 100644 --- a/tests/testthat/test-shinytest2-filter_panel.R +++ b/tests/testthat/test-shinytest2-filter_panel.R @@ -1,7 +1,7 @@ testthat::skip_if_not_installed("shinytest2") testthat::skip_if_not_installed("rvest") -testthat::test_that("e2e: module content is updated when a data is filtered in filter panel", { +testthat::test_that("e2e: module content is updated when data is filtered in filter panel", { skip_if_too_deep(5) app <- TealAppDriver$new( data = simple_teal_data(), diff --git a/tests/testthat/test-shinytest2-teal_data_module.R b/tests/testthat/test-shinytest2-teal_data_module.R index 9cba14c73c..7ba75f4788 100644 --- a/tests/testthat/test-shinytest2-teal_data_module.R +++ b/tests/testthat/test-shinytest2-teal_data_module.R @@ -31,7 +31,7 @@ testthat::test_that("e2e: teal_data_module will have a delayed load of datasets" modules = example_module(label = "Example Module") ) - app$click("teal-data-teal_data_module-data-submit") + app$click("teal-data-teal_data_module-submit") app$navigate_teal_tab("Example Module") testthat::expect_setequal(app$get_active_filter_vars(), c("dataset1", "dataset2")) @@ -67,7 +67,7 @@ testthat::test_that("e2e: teal_data_module shows validation errors", { modules = example_module(label = "Example Module") ) - app$click("teal-data-teal_data_module-data-submit") + app$click("teal-data-teal_data_module-submit") app$expect_validation_error() @@ -111,8 +111,8 @@ testthat::test_that("e2e: teal_data_module inputs change teal_data object that i modules = example_module(label = "Example Module") ) - app$set_input("teal-data-teal_data_module-data-new_column", "A_New_Column") - app$click("teal-data-teal_data_module-data-submit") + app$set_input("teal-data-teal_data_module-new_column", "A_New_Column") + app$click("teal-data-teal_data_module-submit") app$navigate_teal_tab("Example Module") # This may fail if teal_data_module does not perform the transformation @@ -157,14 +157,14 @@ testthat::test_that("e2e: teal_data_module gets removed after successful data lo modules = example_module(label = "Example Module") ) - submit <- "teal-data-teal_data_module-data-submit" + submit <- "teal-data-teal_data_module-submit" app$click(submit) - testthat::expect_false( - app$is_visible('#teal-teal_modules-active_tab a[data-value="teal_data_module"]') + testthat::expect_null( + app$get_html('#teal-teal_modules-active_tab a[data-value="teal_data_module"]') ) - testthat::expect_false( + testthat::expect_null( app$is_visible(sprintf("#%s", submit)) ) @@ -202,7 +202,7 @@ testthat::test_that("e2e: teal_data_module is still visible after successful dat modules = example_module(label = "Example Module") ) - app$click("teal-data-teal_data_module-data-submit") + app$click("teal-data-teal_data_module-submit") testthat::expect_true( app$is_visible('#teal-teal_modules-active_tab a[data-value="teal_data_module"]') @@ -256,7 +256,7 @@ testthat::test_that("e2e: teal_data_module will make other tabs inactive before c("disabled", "disabled") ) - app$click("teal-data-teal_data_module-data-submit") + app$click("teal-data-teal_data_module-submit") testthat::expect_true( is.na( From 4aa8a4bf87de49b15091ecea0d829a0ce5ef268a Mon Sep 17 00:00:00 2001 From: vedhav Date: Thu, 3 Oct 2024 07:29:43 +0000 Subject: [PATCH 17/18] [skip actions] Bump version to 0.15.2.9067 --- DESCRIPTION | 4 ++-- NEWS.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 6cd1e69ac1..0fbab7e2c9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,8 +1,8 @@ Type: Package Package: teal Title: Exploratory Web Apps for Analyzing Clinical Trials Data -Version: 0.15.2.9066 -Date: 2024-09-27 +Version: 0.15.2.9067 +Date: 2024-10-03 Authors@R: c( person("Dawid", "Kaledkowski", , "dawid.kaledkowski@roche.com", role = c("aut", "cre"), comment = c(ORCID = "0000-0001-9533-457X")), diff --git a/NEWS.md b/NEWS.md index 48072cbcd7..81ab605728 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -# teal 0.15.2.9066 +# teal 0.15.2.9067 ### New features From cae7d4ed14eee3b01ed8e27f51a2190ef41b8204 Mon Sep 17 00:00:00 2001 From: Marcin <133694481+m7pr@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:09:39 +0200 Subject: [PATCH 18/18] Run GitHub Actions with `setup-r-dependencies` installation strategy - - - - Bye Bye `staged-dependencies` (#1361) Possible because of https://github.com/insightsengineering/r.pkg.template/pull/252/files --------- Signed-off-by: Marcin <133694481+m7pr@users.noreply.github.com> --- .github/workflows/check.yaml | 32 +++++++++++++++++++++++++++++++ .github/workflows/docs.yaml | 8 ++++++++ .github/workflows/release.yaml | 33 ++++++++++++++++++++++++++++++++ .github/workflows/scheduled.yaml | 8 ++++++++ 4 files changed, 81 insertions(+) diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index e5060049d9..03297d30fc 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -42,6 +42,14 @@ jobs: checking top-level files .* NOTE unit-test-report-brand: >- https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/thumbs/teal.png + deps-installation-method: setup-r-dependencies + lookup-refs: | + insightsengineering/teal.data + insightsengineering/teal.slice + insightsengineering/teal.code + insightsengineering/teal.logger + insightsengineering/teal.reporter + insightsengineering/teal.widgets r-cmd-non-cran: name: R CMD Check (non-CRAN) 🧬 uses: insightsengineering/r.pkg.template/.github/workflows/build-check-install.yaml@main @@ -63,6 +71,14 @@ jobs: checking Rd .usage sections .* NOTE checking for unstated dependencies in vignettes .* NOTE checking top-level files .* NOTE + deps-installation-method: setup-r-dependencies + lookup-refs: | + insightsengineering/teal.data + insightsengineering/teal.slice + insightsengineering/teal.code + insightsengineering/teal.logger + insightsengineering/teal.reporter + insightsengineering/teal.widgets coverage: name: Coverage πŸ“” uses: insightsengineering/r.pkg.template/.github/workflows/test-coverage.yaml@main @@ -71,6 +87,14 @@ jobs: with: additional-env-vars: | NOT_CRAN=true + deps-installation-method: setup-r-dependencies + lookup-refs: | + insightsengineering/teal.data + insightsengineering/teal.slice + insightsengineering/teal.code + insightsengineering/teal.logger + insightsengineering/teal.reporter + insightsengineering/teal.widgets linter: if: github.event_name != 'push' name: SuperLinter πŸ¦Έβ€β™€οΈ @@ -82,6 +106,14 @@ jobs: REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} with: auto-update: true + deps-installation-method: setup-r-dependencies + lookup-refs: | + insightsengineering/teal.data + insightsengineering/teal.slice + insightsengineering/teal.code + insightsengineering/teal.logger + insightsengineering/teal.reporter + insightsengineering/teal.widgets gitleaks: name: gitleaks πŸ’§ uses: insightsengineering/r.pkg.template/.github/workflows/gitleaks.yaml@main diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 57ae800429..7c3e2f9617 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -42,3 +42,11 @@ jobs: with: default-landing-page: latest-tag additional-unit-test-report-directories: unit-test-report-non-cran + deps-installation-method: setup-r-dependencies + lookup-refs: | + insightsengineering/teal.data + insightsengineering/teal.slice + insightsengineering/teal.code + insightsengineering/teal.logger + insightsengineering/teal.reporter + insightsengineering/teal.widgets diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index aa3e7bb457..1f5cb83933 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -16,12 +16,29 @@ jobs: REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} with: default-landing-page: latest-tag + deps-installation-method: setup-r-dependencies + lookup-refs: | + insightsengineering/teal.data + insightsengineering/teal.slice + insightsengineering/teal.code + insightsengineering/teal.logger + insightsengineering/teal.reporter + insightsengineering/teal.widgets validation: name: R Package Validation report πŸ“ƒ needs: release uses: insightsengineering/r.pkg.template/.github/workflows/validation.yaml@main secrets: REPO_GITHUB_TOKEN: ${{ secrets.REPO_GITHUB_TOKEN }} + with: + deps-installation-method: setup-r-dependencies + lookup-refs: | + insightsengineering/teal.data + insightsengineering/teal.slice + insightsengineering/teal.code + insightsengineering/teal.logger + insightsengineering/teal.reporter + insightsengineering/teal.widgets release: name: Create release πŸŽ‰ uses: insightsengineering/r.pkg.template/.github/workflows/release.yaml@main @@ -46,6 +63,14 @@ jobs: checking top-level files .* NOTE unit-test-report-brand: >- https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/thumbs/teal.png + deps-installation-method: setup-r-dependencies + lookup-refs: | + insightsengineering/teal.data + insightsengineering/teal.slice + insightsengineering/teal.code + insightsengineering/teal.logger + insightsengineering/teal.reporter + insightsengineering/teal.widgets coverage: name: Coverage πŸ“” needs: [release, docs] @@ -55,6 +80,14 @@ jobs: with: additional-env-vars: | NOT_CRAN=true + deps-installation-method: setup-r-dependencies + lookup-refs: | + insightsengineering/teal.data + insightsengineering/teal.slice + insightsengineering/teal.code + insightsengineering/teal.logger + insightsengineering/teal.reporter + insightsengineering/teal.widgets wasm: name: Build WASM packages πŸ§‘β€πŸ­ needs: release diff --git a/.github/workflows/scheduled.yaml b/.github/workflows/scheduled.yaml index c4233673e5..104b05f4b0 100644 --- a/.github/workflows/scheduled.yaml +++ b/.github/workflows/scheduled.yaml @@ -56,6 +56,14 @@ jobs: ) name: revdepcheck ↩️ uses: insightsengineering/r.pkg.template/.github/workflows/revdepcheck.yaml@main + with: + lookup-refs: | + insightsengineering/teal.data + insightsengineering/teal.slice + insightsengineering/teal.code + insightsengineering/teal.logger + insightsengineering/teal.reporter + insightsengineering/teal.widgets rhub: if: > github.event_name == 'schedule' || (