diff --git a/main/coverage-report/index.html b/main/coverage-report/index.html index bbbb212469..2e095da087 100644 --- a/main/coverage-report/index.html +++ b/main/coverage-report/index.html @@ -95,7 +95,7 @@ font-size: 11px; }
1 |
- # FilteredData ------+ #' Data module for `teal` applications |
||
2 |
-
+ #' |
||
3 |
- #' Drive a `teal` application+ #' @description |
||
4 |
- #'+ #' `r lifecycle::badge("experimental")` |
||
5 |
- #' Extension of the `shinytest2::AppDriver` class with methods for+ #' |
||
6 |
- #' driving a teal application for performing interactions for `shinytest2` tests.+ #' Create a `teal_data_module` object and evaluate code on it with history tracking. |
||
8 |
- #' @keywords internal+ #' @details |
||
9 |
- #'+ #' `teal_data_module` creates a `shiny` module to interactively supply or modify data in a `teal` application. |
||
10 |
- TealAppDriver <- R6::R6Class( # nolint: object_name.+ #' The module allows for running any code (creation _and_ some modification) after the app starts or reloads. |
||
11 |
- "TealAppDriver",+ #' The body of the server function will be run in the app rather than in the global environment. |
||
12 |
- inherit = {+ #' This means it will be run every time the app starts, so use sparingly. |
||
13 |
- if (!requireNamespace("shinytest2", quietly = TRUE)) {+ #' |
||
14 |
- stop("Please install 'shinytest2' package to use this class.")+ #' Pass this module instead of a `teal_data` object in a call to [init()]. |
||
15 |
- }+ #' Note that the server function must always return a `teal_data` object wrapped in a reactive expression. |
||
16 |
- if (!requireNamespace("rvest", quietly = TRUE)) {+ #' |
||
17 |
- stop("Please install 'rvest' package to use this class.")+ #' See vignette `vignette("data-as-shiny-module", package = "teal")` for more details. |
||
18 |
- }+ #' |
||
19 |
- shinytest2::AppDriver+ #' @param ui (`function(id)`) |
||
20 |
- },+ #' `shiny` module UI function; must only take `id` argument |
||
21 |
- # public methods ----+ #' @param server (`function(id)`) |
||
22 |
- public = list(+ #' `shiny` module server function; must only take `id` argument; |
||
23 |
- #' @description+ #' must return reactive expression containing `teal_data` object |
||
24 |
- #' Initialize a `TealAppDriver` object for testing a `teal` application.+ #' @param label (`character(1)`) Label of the module. |
||
25 |
- #'+ #' @param once (`logical(1)`) |
||
26 |
- #' @param data,modules,filter,title,header,footer,landing_popup arguments passed to `init`+ #' If `TRUE`, the data module will be shown only once and will disappear after successful data loading. |
||
27 |
- #' @param timeout (`numeric`) Default number of milliseconds for any timeout or+ #' App user will no longer be able to interact with this module anymore. |
||
28 |
- #' timeout_ parameter in the `TealAppDriver` class.+ #' If `FALSE`, the data module can be reused multiple times. |
||
29 |
- #' Defaults to 20s.+ #' App user will be able to interact and change the data output from the module multiple times. |
||
30 |
- #'+ #' |
||
31 |
- #' See [`shinytest2::AppDriver`] `new` method for more details on how to change it+ #' @return |
||
32 |
- #' via options or environment variables.+ #' `teal_data_module` returns a list of class `teal_data_module` containing two elements, `ui` and |
||
33 |
- #' @param load_timeout (`numeric`) How long to wait for the app to load, in ms.+ #' `server` provided via arguments. |
||
34 |
- #' This includes the time to start R. Defaults to 100s.+ #' |
||
35 |
- #'+ #' @examples |
||
36 |
- #' See [`shinytest2::AppDriver`] `new` method for more details on how to change it+ #' tdm <- teal_data_module( |
||
37 |
- #' via options or environment variables+ #' ui = function(id) { |
||
38 |
- #' @param ... Additional arguments to be passed to `shinytest2::AppDriver$new`+ #' ns <- NS(id) |
||
39 |
- #'+ #' actionButton(ns("submit"), label = "Load data") |
||
40 |
- #'+ #' }, |
||
41 |
- #' @return Object of class `TealAppDriver`+ #' server = function(id) { |
||
42 |
- initialize = function(data,+ #' moduleServer(id, function(input, output, session) { |
||
43 |
- modules,+ #' eventReactive(input$submit, { |
||
44 |
- filter = teal_slices(),+ #' data <- within( |
||
45 |
- title = build_app_title(),+ #' teal_data(), |
||
46 |
- header = tags$p(),+ #' { |
||
47 |
- footer = tags$p(),+ #' dataset1 <- iris |
||
48 |
- landing_popup = NULL,+ #' dataset2 <- mtcars |
||
49 |
- timeout = rlang::missing_arg(),+ #' } |
||
50 |
- load_timeout = rlang::missing_arg(),+ #' ) |
||
51 |
- ...) {+ #' datanames(data) <- c("dataset1", "dataset2") |
||
52 | -! | +
- private$data <- data+ #' |
|
53 | -! | +
- private$modules <- modules+ #' data |
|
54 | -! | +
- private$filter <- filter+ #' }) |
|
55 | -! | +
- app <- init(+ #' }) |
|
56 | -! | +
- data = data,+ #' } |
|
57 | -! | +
- modules = modules,+ #' ) |
|
58 | -! | +
- filter = filter,+ #' |
|
59 | -! | +
- title = title,+ #' @name teal_data_module |
|
60 | -! | +
- header = header,+ #' @seealso [`teal.data::teal_data-class`], [teal.code::qenv()] |
|
61 | -! | +
- footer = footer,+ #' |
|
62 | -! | +
- landing_popup = landing_popup,+ #' @export |
|
63 |
- )+ teal_data_module <- function(ui, server, label = "data module", once = TRUE) { |
||
64 | -+ | 32x |
-
+ checkmate::assert_function(ui, args = "id", nargs = 1) |
65 | -+ | 31x |
- # Default timeout is hardcoded to 4s in shinytest2:::resolve_timeout+ checkmate::assert_function(server, args = "id", nargs = 1) |
66 | -+ | 29x |
- # It must be set as parameter to the AppDriver+ checkmate::assert_string(label) |
67 | -! | +29x |
- suppressWarnings(+ checkmate::assert_flag(once) |
68 | -! | +29x |
- super$initialize(+ structure( |
69 | -! | +29x |
- app_dir = shinyApp(app$ui, app$server),+ list( |
70 | -! | +29x |
- name = "teal",+ ui = ui, |
71 | -! | +29x |
- variant = shinytest2::platform_variant(),+ server = function(id) { |
72 | -! | +22x |
- timeout = rlang::maybe_missing(timeout, 20 * 1000),+ data_out <- server(id) |
73 | -! | +21x |
- load_timeout = rlang::maybe_missing(load_timeout, 100 * 1000),+ decorate_err_msg( |
74 | -+ | 21x |
- ...+ assert_reactive(data_out), |
75 | -+ | 21x |
- )+ pre = sprintf("From: 'teal_data_module()':\nA 'teal_data_module' with \"%s\" label:", label), |
76 | -+ | 21x |
- )+ post = "Please make sure that this module returns a 'reactive` object containing 'teal_data' class of object." # nolint: line_length_linter. |
77 |
-
+ ) |
||
78 |
- # Check for minimum version of Chrome that supports the tests+ } |
||
79 |
- # - Element.checkVisibility was added on 105+ ), |
||
80 | -! | +29x |
- chrome_version <- numeric_version(+ label = label, |
81 | -! | +29x |
- gsub(+ class = "teal_data_module", |
82 | -! | +29x |
- "[[:alnum:]_]+/", # Prefix that ends with forward slash+ once = once |
83 |
- "",+ ) |
||
84 | -! | +
- self$get_chromote_session()$Browser$getVersion()$product+ } |
|
85 |
- ),+ |
||
86 | -! | +
- strict = FALSE+ #' Data module for `teal` transformers. |
|
87 |
- )+ #' |
||
88 |
-
+ #' @description |
||
89 | -! | +
- required_version <- "121"+ #' `r lifecycle::badge("experimental")` |
|
90 |
-
+ #' |
||
91 | -! | +
- testthat::skip_if(+ #' Create a `teal_data_module` object for custom transformation of data for pre-processing |
|
92 | -! | +
- is.na(chrome_version),+ #' before passing the data into the module. |
|
93 | -! | +
- "Problem getting Chrome version, please contact the developers."+ #' |
|
94 |
- )+ #' @details |
||
95 | -! | +
- testthat::skip_if(+ #' `teal_transform_module` creates a [`teal_data_module`] object to transform data in a `teal` |
|
96 | -! | +
- chrome_version < required_version,+ #' application. This transformation happens after the data has passed through the filtering activity |
|
97 | -! | +
- sprintf(+ #' in teal. The transformed data is then sent to the server of the [teal_module()]. |
|
98 | -! | +
- "Chrome version '%s' is not supported, please upgrade to '%s' or higher",+ #' |
|
99 | -! | +
- chrome_version,+ #' See vignette `vignette("data-transform-as-shiny-module", package = "teal")` for more details. |
|
100 | -! | +
- required_version+ #' |
|
101 |
- )+ #' |
||
102 |
- )+ #' @inheritParams teal_data_module |
||
103 |
- # end od check+ #' @param server (`function(id, data)`) |
||
104 |
-
+ #' `shiny` module server function; that takes `id` and `data` argument, |
||
105 | -! | +
- private$set_active_ns()+ #' where the `id` is the module id and `data` is the reactive `teal_data` input. |
|
106 | -! | +
- self$wait_for_idle()+ #' The server function must return reactive expression containing `teal_data` object. |
|
107 |
- },+ #' @param datanames (`character`) |
||
108 |
- #' @description+ #' Names of the datasets that are relevant for the module. The |
||
109 |
- #' Append parent [`shinytest2::AppDriver`] `click` method with a call to `waif_for_idle()` method.+ #' filter panel will only display filters for specified `datanames`. The keyword `"all"` will show |
||
110 |
- #' @param ... arguments passed to parent [`shinytest2::AppDriver`] `click()` method.+ #' filters of all datasets. `datanames` will be automatically appended to the [modules()] `datanames`. |
||
111 |
- click = function(...) {+ #' @examples |
||
112 | -! | +
- super$click(...)+ #' my_transformers <- list( |
|
113 | -! | +
- private$wait_for_page_stability()+ #' teal_transform_module( |
|
114 |
- },+ #' label = "Custom transform for iris", |
||
115 |
- #' @description+ #' datanames = "iris", |
||
116 |
- #' Check if the app has shiny errors. This checks for global shiny errors.+ #' ui = function(id) { |
||
117 |
- #' Note that any shiny errors dependent on shiny server render will only be captured after the teal module tab+ #' ns <- NS(id) |
||
118 |
- #' is visited because shiny will not trigger server computations when the tab is invisible.+ #' tags$div( |
||
119 |
- #' So, navigate to the module tab you want to test before calling this function.+ #' numericInput(ns("n_rows"), "Subset n rows", value = 6, min = 1, max = 150, step = 1) |
||
120 |
- #' Although, this catches errors hidden in the other module tabs if they are already rendered.+ #' ) |
||
121 |
- expect_no_shiny_error = function() {+ #' }, |
||
122 | -! | +
- testthat::expect_null(+ #' server = function(id, data) { |
|
123 | -! | +
- self$get_html(".shiny-output-error:not(.shiny-output-error-validation)"),+ #' moduleServer(id, function(input, output, session) { |
|
124 | -! | +
- info = "Shiny error is observed"+ #' reactive({ |
|
125 |
- )+ #' within(data(), |
||
126 |
- },+ #' { |
||
127 |
- #' @description+ #' iris <- head(iris, num_rows) |
||
128 |
- #' Check if the app has no validation errors. This checks for global shiny validation errors.+ #' }, |
||
129 |
- expect_no_validation_error = function() {+ #' num_rows = input$n_rows |
||
130 | -! | +
- testthat::expect_null(+ #' ) |
|
131 | -! | +
- self$get_html(".shiny-output-error-validation"),+ #' }) |
|
132 | -! | +
- info = "No validation error is observed"+ #' }) |
|
133 |
- )+ #' } |
||
134 |
- },+ #' ) |
||
135 |
- #' @description+ #' ) |
||
136 |
- #' Check if the app has validation errors. This checks for global shiny validation errors.+ #' |
||
137 |
- expect_validation_error = function() {+ #' @name teal_transform_module |
||
138 | -! | +
- testthat::expect_false(+ #' |
|
139 | -! | +
- is.null(self$get_html(".shiny-output-error-validation")),+ #' @export |
|
140 | -! | +
- info = "Validation error is not observed"+ teal_transform_module <- function(ui = function(id) NULL, |
|
141 |
- )+ server = function(id, data) data, |
||
142 |
- },+ label = "transform module", |
||
143 |
- #' @description+ datanames = "all") { |
||
144 | -+ | 18x |
- #' Set the input in the `teal` app.+ checkmate::assert_function(ui, args = "id", nargs = 1) |
145 | -+ | 18x |
- #'+ checkmate::assert_function(server, args = c("id", "data"), nargs = 2) |
146 | -+ | 18x |
- #' @param input_id (character) The shiny input id with it's complete name space.+ checkmate::assert_string(label) |
147 | -+ | 18x |
- #' @param value The value to set the input to.+ structure( |
148 | -+ | 18x |
- #' @param ... Additional arguments to be passed to `shinytest2::AppDriver$set_inputs`+ list( |
149 | -+ | 18x |
- #'+ ui = ui, |
150 | -+ | 18x |
- #' @return The `TealAppDriver` object invisibly.+ server = function(id, data) { |
151 | -+ | 18x |
- set_input = function(input_id, value, ...) {+ data_out <- server(id, data) |
152 | -! | +18x |
- do.call(+ decorate_err_msg( |
153 | -! | +18x |
- self$set_inputs,+ assert_reactive(data_out), |
154 | -! | +18x |
- c(setNames(list(value), input_id), list(...))+ pre = sprintf("From: 'teal_transform_module()':\nA 'teal_transform_module' with \"%s\" label:", label), |
155 | -+ | 18x |
- )+ post = "Please make sure that this module returns a 'reactive` object containing 'teal_data' class of object." # nolint: line_length_linter. |
156 | -! | +
- invisible(self)+ ) |
|
157 |
- },+ } |
||
158 |
- #' @description+ ), |
||
159 | -+ | 18x |
- #' Navigate the teal tabs in the `teal` app.+ label = label, |
160 | -+ | 18x |
- #'+ datanames = datanames, |
161 | -+ | 18x |
- #' @param tabs (character) Labels of tabs to navigate to. The order of the tabs is important,+ class = c("teal_transform_module", "teal_data_module") |
162 |
- #' and it should start with the most parent level tab.+ ) |
||
163 |
- #' Note: In case the teal tab group has duplicate names, the first tab will be selected,+ } |
||
164 |
- #' If you wish to select the second tab with the same name, use the suffix "_1".+ |
||
165 |
- #' If you wish to select the third tab with the same name, use the suffix "_2" and so on.+ |
||
166 |
- #'+ #' Extract all `transformers` from `modules`. |
||
167 |
- #' @return The `TealAppDriver` object invisibly.+ #' |
||
168 |
- navigate_teal_tab = function(tabs) {+ #' @param modules `teal_modules` or `teal_module` |
||
169 | -! | +
- checkmate::check_character(tabs, min.len = 1)+ #' @return A list of `teal_transform_module` nested in the same way as input `modules`. |
|
170 | -! | +
- for (tab in tabs) {+ #' @keywords internal |
|
171 | -! | +
- self$set_input(+ extract_transformers <- function(modules) { |
|
172 | -! | +2x |
- "teal-teal_modules-active_tab",+ if (inherits(modules, "teal_module")) { |
173 | -! | +1x |
- get_unique_labels(tab),+ modules$transformers |
174 | -! | +1x |
- wait_ = FALSE+ } else if (inherits(modules, "teal_modules")) { |
175 | -+ | 1x |
- )+ lapply(modules$children, extract_transformers) |
176 |
- }+ } |
||
177 | -! | -
- self$wait_for_idle()- |
- |
178 | -! | -
- private$set_active_ns()- |
- |
179 | -! | +
- invisible(self)+ } |
180 | +1 |
- },+ #' Data summary |
|
181 | +2 |
- #' @description+ #' @description |
|
182 | +3 |
- #' Get the active shiny name space for different components of the teal app.+ #' Module and its utils to display the number of rows and subjects in the filtered and unfiltered data. |
|
183 | +4 |
- #'+ #' |
|
184 | +5 |
- #' @return (`list`) The list of active shiny name space of the teal components.+ #' @details Handling different data classes: |
|
185 | +6 |
- active_ns = function() {- |
- |
186 | -! | -
- if (identical(private$ns$module, character(0))) {- |
- |
187 | -! | -
- private$set_active_ns()+ #' `get_object_filter_overview()` is a pseudo S3 method which has variants for: |
|
188 | +7 |
- }- |
- |
189 | -! | -
- private$ns+ #' - `array` (`data.frame`, `DataFrame`, `array`, `Matrix` and `SummarizedExperiment`): Method variant |
|
190 | +8 |
- },+ #' can be applied to any two-dimensional objects on which [ncol()] can be used. |
|
191 | +9 |
- #' @description+ #' - `MultiAssayExperiment`: for which summary contains counts for `colData` and all `experiments`. |
|
192 | +10 |
- #' Get the active shiny name space for interacting with the module content.+ #' |
|
193 | +11 |
- #'+ #' @param id (`character(1)`) |
|
194 | +12 |
- #' @return (`string`) The active shiny name space of the component.+ #' `shiny` module instance id. |
|
195 | +13 |
- active_module_ns = function() {- |
- |
196 | -! | -
- if (identical(private$ns$module, character(0))) {- |
- |
197 | -! | -
- private$set_active_ns()+ #' @param teal_data (`reactive` returning `teal_data`) |
|
198 | +14 |
- }+ #' |
|
199 | -! | +||
15 | +
- private$ns$module+ #' |
||
200 | +16 |
- },+ #' @name module_data_summary |
|
201 | +17 |
- #' @description+ #' @rdname module_data_summary |
|
202 | +18 |
- #' Get the active shiny name space bound with a custom `element` name.+ #' @keywords internal |
|
203 | +19 |
- #'+ #' @return `NULL`. |
|
204 | +20 |
- #' @param element `character(1)` custom element name.+ NULL |
|
205 | +21 |
- #'+ |
|
206 | +22 |
- #' @return (`string`) The active shiny name space of the component bound with the input `element`.+ #' @rdname module_data_summary |
|
207 | +23 |
- active_module_element = function(element) {+ ui_data_summary <- function(id) { |
|
208 | +24 | ! |
- checkmate::assert_string(element)+ ns <- NS(id) |
209 | +25 | ! |
- sprintf("#%s-%s", self$active_module_ns(), element)- |
-
210 | -- |
- },- |
- |
211 | -- |
- #' @description- |
- |
212 | -- |
- #' Get the text of the active shiny name space bound with a custom `element` name.- |
- |
213 | -- |
- #'+ content_id <- ns("filters_overview_contents") |
|
214 | -+ | ||
26 | +! |
- #' @param element `character(1)` the text of the custom element name.+ tags$div( |
|
215 | -+ | ||
27 | +! |
- #'+ id = id, |
|
216 | -+ | ||
28 | +! |
- #' @return (`string`) The text of the active shiny name space of the component bound with the input `element`.+ class = "well", |
|
217 | -+ | ||
29 | +! |
- active_module_element_text = function(element) {+ tags$div( |
|
218 | +30 | ! |
- checkmate::assert_string(element)+ class = "row", |
219 | +31 | ! |
- self$get_text(self$active_module_element(element))+ tags$div( |
220 | -+ | ||
32 | +! |
- },+ class = "col-sm-9", |
|
221 | -+ | ||
33 | +! |
- #' @description+ tags$label("Active Filter Summary", class = "text-primary mb-4") |
|
222 | +34 |
- #' Get the active shiny name space for interacting with the filter panel.+ ), |
|
223 | -+ | ||
35 | +! |
- #'+ tags$div( |
|
224 | -+ | ||
36 | +! |
- #' @return (`string`) The active shiny name space of the component.+ class = "col-sm-3", |
|
225 | -+ | ||
37 | +! |
- active_filters_ns = function() {+ tags$i( |
|
226 | +38 | ! |
- if (identical(private$ns$filter_panel, character(0))) {+ class = "remove pull-right fa fa-angle-down", |
227 | +39 | ! |
- private$set_active_ns()+ style = "cursor: pointer;", |
228 | -+ | ||
40 | +! |
- }+ title = "fold/expand data summary panel", |
|
229 | +41 | ! |
- private$ns$filter_panel+ onclick = sprintf("togglePanelItems(this, '%s', 'fa-angle-right', 'fa-angle-down');", content_id) |
230 | +42 |
- },+ ) |
|
231 | +43 |
- #' @description+ ) |
|
232 | +44 |
- #' Get the active shiny name space for interacting with the data-summary panel.+ ), |
|
233 | -+ | ||
45 | +! |
- #'+ tags$div( |
|
234 | -+ | ||
46 | +! |
- #' @return (`string`) The active shiny name space of the data-summary component.+ id = content_id, |
|
235 | -+ | ||
47 | +! |
- active_data_summary_ns = function() {+ tags$div( |
|
236 | +48 | ! |
- if (identical(private$ns$data_summary, character(0))) {+ class = "teal_active_summary_filter_panel", |
237 | +49 | ! |
- private$set_active_ns()+ tableOutput(ns("table")) |
238 | +50 |
- }- |
- |
239 | -! | -
- private$ns$data_summary+ ) |
|
240 | +51 |
- },+ ) |
|
241 | +52 |
- #' @description+ ) |
|
242 | +53 |
- #' Get the active shiny name space bound with a custom `element` name.+ } |
|
243 | +54 |
- #'+ |
|
244 | +55 |
- #' @param element `character(1)` custom element name.+ #' @rdname module_data_summary |
|
245 | +56 |
- #'+ srv_data_summary <- function(id, teal_data) { |
|
246 | -+ | ||
57 | +94x |
- #' @return (`string`) The active shiny name space of the component bound with the input `element`.+ assert_reactive(teal_data) |
|
247 | -+ | ||
58 | +94x |
- active_data_summary_element = function(element) {+ moduleServer( |
|
248 | -! | +||
59 | +94x |
- checkmate::assert_string(element)+ id = id, |
|
249 | -! | +||
60 | +94x |
- sprintf("#%s-%s", self$active_data_summary_ns(), element)+ function(input, output, session) { |
|
250 | -+ | ||
61 | +94x |
- },+ logger::log_debug("srv_data_summary initializing") |
|
251 | +62 |
- #' @description+ |
|
252 | -+ | ||
63 | +94x |
- #' Get the input from the module in the `teal` app.+ summary_table <- reactive({ |
|
253 | -+ | ||
64 | +101x |
- #' This function will only access inputs from the name space of the current active teal module.+ req(inherits(teal_data(), "teal_data")) |
|
254 | -+ | ||
65 | +72x |
- #'+ if (!length(ls(teal.code::get_env(teal_data())))) { |
|
255 | -+ | ||
66 | +1x |
- #' @param input_id (character) The shiny input id to get the value from.+ return(NULL) |
|
256 | +67 |
- #'+ } |
|
257 | +68 |
- #' @return The value of the shiny input.+ + |
+ |
69 | +71x | +
+ filter_overview <- get_filter_overview(teal_data)+ |
+ |
70 | +71x | +
+ names(filter_overview)[[1]] <- "Data Name" |
|
258 | +71 |
- get_active_module_input = function(input_id) {+ |
|
259 | -! | +||
72 | +71x |
- checkmate::check_string(input_id)+ filter_overview$Obs <- ifelse( |
|
260 | -! | +||
73 | +71x |
- self$get_value(input = sprintf("%s-%s", self$active_module_ns(), input_id))+ !is.na(filter_overview$obs), |
|
261 | -+ | ||
74 | +71x |
- },+ sprintf("%s/%s", filter_overview$obs_filtered, filter_overview$obs), |
|
262 | -+ | ||
75 | +71x |
- #' @description+ ifelse(!is.na(filter_overview$obs_filtered), sprintf("%s", filter_overview$obs_filtered), "") |
|
263 | +76 |
- #' Get the output from the module in the `teal` app.+ ) |
|
264 | +77 |
- #' This function will only access outputs from the name space of the current active teal module.+ |
|
265 | -+ | ||
78 | +71x |
- #'+ filter_overview$Subjects <- ifelse( |
|
266 | -+ | ||
79 | +71x |
- #' @param output_id (character) The shiny output id to get the value from.+ !is.na(filter_overview$subjects),+ |
+ |
80 | +71x | +
+ sprintf("%s/%s", filter_overview$subjects_filtered, filter_overview$subjects), |
|
267 | +81 |
- #'+ "" |
|
268 | +82 |
- #' @return The value of the shiny output.+ ) |
|
269 | +83 |
- get_active_module_output = function(output_id) {+ |
|
270 | -! | +||
84 | +71x |
- checkmate::check_string(output_id)+ filter_overview <- filter_overview[, colnames(filter_overview) %in% c("Data Name", "Obs", "Subjects")] |
|
271 | -! | +||
85 | +71x |
- self$get_value(output = sprintf("%s-%s", self$active_module_ns(), output_id))+ Filter(function(col) !all(col == ""), filter_overview) |
|
272 | +86 |
- },+ }) |
|
273 | +87 |
- #' @description+ |
|
274 | -+ | ||
88 | +94x |
- #' Get the output from the module's `teal.widgets::table_with_settings` or `DT::DTOutput` in the `teal` app.+ output$table <- renderUI({ |
|
275 | -+ | ||
89 | +101x |
- #' This function will only access outputs from the name space of the current active teal module.+ summary_table_out <- try(summary_table(), silent = TRUE) |
|
276 | -+ | ||
90 | +101x |
- #'+ if (inherits(summary_table_out, "try-error")) { |
|
277 | +91 |
- #' @param table_id (`character(1)`) The id of the table in the active teal module's name space.+ # Ignore silent shiny error |
|
278 | -+ | ||
92 | +29x |
- #' @param which (integer) If there is more than one table, which should be extracted.+ if (!inherits(attr(summary_table_out, "condition"), "shiny.silent.error")) { |
|
279 | -+ | ||
93 | +! |
- #' By default it will look for a table that is built using `teal.widgets::table_with_settings`.+ stop("Error occurred during data processing. See details in the main panel.") |
|
280 | +94 |
- #'+ } |
|
281 | -+ | ||
95 | +72x |
- #' @return The data.frame with table contents.+ } else if (is.null(summary_table_out)) {+ |
+ |
96 | +1x | +
+ "no datasets to show" |
|
282 | +97 |
- get_active_module_table_output = function(table_id, which = 1) {+ } else { |
|
283 | -! | +||
98 | +71x |
- checkmate::check_number(which, lower = 1)+ body_html <- apply( |
|
284 | -! | +||
99 | +71x |
- checkmate::check_string(table_id)+ summary_table_out, |
|
285 | -! | +||
100 | +71x |
- table <- rvest::html_table(+ 1, |
|
286 | -! | +||
101 | +71x |
- self$get_html_rvest(self$active_module_element(table_id)),+ function(x) { |
|
287 | -! | +||
102 | +131x |
- fill = TRUE+ tags$tr( |
|
288 | -+ | ||
103 | +131x |
- )+ tagList( |
|
289 | -! | +||
104 | +131x |
- if (length(table) == 0) {+ tags$td( |
|
290 | -! | +||
105 | +131x |
- data.frame()+ if (all(x[-1] == "")) { |
|
291 | -+ | ||
106 | +1x |
- } else {+ icon( |
|
292 | -! | +||
107 | +1x |
- table[[which]]+ name = "fas fa-exclamation-triangle", |
|
293 | -+ | ||
108 | +1x |
- }+ title = "Unsupported dataset", |
|
294 | -+ | ||
109 | +1x |
- },+ `data-container` = "body", |
|
295 | -+ | ||
110 | +1x |
- #' @description+ `data-toggle` = "popover", |
|
296 | -+ | ||
111 | +1x |
- #' Get the output from the module's `teal.widgets::plot_with_settings` in the `teal` app.+ `data-content` = "object not supported by the data_summary module" |
|
297 | +112 |
- #' This function will only access plots from the name space of the current active teal module.+ ) |
|
298 | +113 |
- #'+ },+ |
+ |
114 | +131x | +
+ x[1] |
|
299 | +115 |
- #' @param plot_id (`character(1)`) The id of the plot in the active teal module's name space.+ ),+ |
+ |
116 | +131x | +
+ lapply(x[-1], tags$td) |
|
300 | +117 |
- #'+ ) |
|
301 | +118 |
- #' @return The `src` attribute as `character(1)` vector.+ ) |
|
302 | +119 |
- get_active_module_plot_output = function(plot_id) {+ } |
|
303 | -! | +||
120 | +
- checkmate::check_string(plot_id)+ ) |
||
304 | -! | +||
121 | +
- self$get_attr(+ |
||
305 | -! | +||
122 | +71x |
- self$active_module_element(sprintf("%s-plot_main > img", plot_id)),+ header_labels <- names(summary_table()) |
|
306 | -! | +||
123 | +71x |
- "src"+ header_html <- tags$tr(tagList(lapply(header_labels, tags$td))) |
|
307 | +124 |
- )+ |
|
308 | -+ | ||
125 | +71x |
- },+ table_html <- tags$table( |
|
309 | -+ | ||
126 | +71x |
- #' @description+ class = "table custom-table", |
|
310 | -+ | ||
127 | +71x |
- #' Set the input in the module in the `teal` app.+ tags$thead(header_html), |
|
311 | -+ | ||
128 | +71x |
- #' This function will only set inputs in the name space of the current active teal module.+ tags$tbody(body_html) |
|
312 | +129 |
- #'+ ) |
|
313 | -+ | ||
130 | +71x |
- #' @param input_id (character) The shiny input id to get the value from.+ table_html |
|
314 | +131 |
- #' @param value The value to set the input to.+ } |
|
315 | +132 |
- #' @param ... Additional arguments to be passed to `shinytest2::AppDriver$set_inputs`+ }) |
|
316 | +133 |
- #'+ |
|
317 | -+ | ||
134 | +94x |
- #' @return The `TealAppDriver` object invisibly.+ summary_table # testing purpose |
|
318 | +135 |
- set_active_module_input = function(input_id, value, ...) {- |
- |
319 | -! | -
- checkmate::check_string(input_id)- |
- |
320 | -! | -
- checkmate::check_string(value)- |
- |
321 | -! | -
- self$set_input(+ } |
|
322 | -! | +||
136 | +
- sprintf("%s-%s", self$active_module_ns(), input_id),+ ) |
||
323 | -! | +||
137 | +
- value,+ } |
||
324 | +138 |
- ...+ |
|
325 | +139 |
- )+ #' @rdname module_data_summary |
|
326 | -! | +||
140 | +
- dots <- rlang::list2(...)+ get_filter_overview <- function(teal_data) { |
||
327 | -! | +||
141 | +71x |
- if (!isFALSE(dots[["wait"]])) self$wait_for_idle() # Default behavior is to wait+ datanames <- teal.data::datanames(teal_data()) |
|
328 | -! | +||
142 | +71x |
- invisible(self)+ joinkeys <- teal.data::join_keys(teal_data()) |
|
329 | +143 |
- },+ |
|
330 | -+ | ||
144 | +71x |
- #' @description+ filtered_data_objs <- sapply( |
|
331 | -+ | ||
145 | +71x |
- #' Get the active datasets that can be accessed via the filter panel of the current active teal module.+ datanames, |
|
332 | -+ | ||
146 | +71x |
- get_active_filter_vars = function() {+ function(name) teal.code::get_var(teal_data(), name), |
|
333 | -! | +||
147 | +71x |
- displayed_datasets_index <- self$is_visible(+ simplify = FALSE |
|
334 | -! | +||
148 | +
- sprintf("#%s-filters-filter_active_vars_contents > span", self$active_filters_ns())+ ) |
||
335 | -+ | ||
149 | +71x |
- )+ unfiltered_data_objs <- teal.code::get_var(teal_data(), ".raw_data") |
|
336 | +150 | ||
337 | -! | -
- available_datasets <- self$get_text(- |
- |
338 | -! | -
- sprintf(- |
- |
339 | -! | +||
151 | +71x |
- "#%s-filters-filter_active_vars_contents .filter_panel_dataname",+ rows <- lapply( |
|
340 | -! | +||
152 | +71x |
- self$active_filters_ns()+ datanames, |
|
341 | -+ | ||
153 | +71x |
- )+ function(dataname) { |
|
342 | -+ | ||
154 | +131x |
- )+ parent <- teal.data::parent(joinkeys, dataname) |
|
343 | +155 | - - | -|
344 | -! | -
- available_datasets[displayed_datasets_index]+ # todo: what should we display for a parent dataset? |
|
345 | +156 |
- },+ # - Obs and Subjects |
|
346 | +157 |
- #' @description+ # - Obs only |
|
347 | +158 |
- #' Get the active data summary table+ # - Subjects only |
|
348 | +159 |
- #' @return `data.frame`+ # todo (for later): summary table should be displayed in a way that child datasets |
|
349 | +160 |
- get_active_data_summary_table = function() {+ # are indented under their parent dataset to form a tree structure |
|
350 | -! | +||
161 | +131x |
- summary_table <- rvest::html_table(+ subject_keys <- if (length(parent) > 0) { |
|
351 | -! | +||
162 | +7x |
- self$get_html_rvest(self$active_data_summary_element("table")),+ names(joinkeys[dataname, parent]) |
|
352 | -! | +||
163 | +
- fill = TRUE+ } else { |
||
353 | -! | +||
164 | +124x |
- )[[1]]+ joinkeys[dataname, dataname] |
|
354 | +165 |
-
+ } |
|
355 | -! | +||
166 | +131x |
- col_names <- unlist(summary_table[1, ], use.names = FALSE)+ get_object_filter_overview( |
|
356 | -! | +||
167 | +131x |
- summary_table <- summary_table[-1, ]+ filtered_data = filtered_data_objs[[dataname]], |
|
357 | -! | +||
168 | +131x |
- colnames(summary_table) <- col_names+ unfiltered_data = unfiltered_data_objs[[dataname]], |
|
358 | -! | +||
169 | +131x |
- if (nrow(summary_table) > 0) {+ dataname = dataname, |
|
359 | -! | +||
170 | +131x |
- summary_table+ subject_keys = subject_keys |
|
360 | +171 |
- } else {- |
- |
361 | -! | -
- NULL+ ) |
|
362 | +172 |
- }+ } |
|
363 | +173 |
- },+ ) |
|
364 | +174 |
- #' @description+ |
|
365 | -+ | ||
175 | +71x |
- #' Test if `DOM` elements are visible on the page with a JavaScript call.+ unssuported_idx <- vapply(rows, function(x) all(is.na(x[-1])), logical(1)) # this is mainly for vectors |
|
366 | -+ | ||
176 | +71x |
- #' @param selector (`character(1)`) `CSS` selector to check visibility.+ do.call(rbind, c(rows[!unssuported_idx], rows[unssuported_idx])) |
|
367 | +177 |
- #' A `CSS` id will return only one element if the UI is well formed.+ } |
|
368 | +178 |
- #' @param content_visibility_auto,opacity_property,visibility_property (`logical(1)`) See more information+ |
|
369 | +179 |
- #' on <https://developer.mozilla.org/en-US/docs/Web/API/Element/checkVisibility>.+ #' @rdname module_data_summary |
|
370 | +180 |
- #'+ #' @param filtered_data (`list`) of filtered objects |
|
371 | +181 |
- #' @return Logical vector with all occurrences of the selector.+ #' @param unfiltered_data (`list`) of unfiltered objects |
|
372 | +182 |
- is_visible = function(selector,+ #' @param dataname (`character(1)`) |
|
373 | +183 |
- content_visibility_auto = FALSE,+ get_object_filter_overview <- function(filtered_data, unfiltered_data, dataname, subject_keys) { |
|
374 | -+ | ||
184 | +131x |
- opacity_property = FALSE,+ if (inherits(filtered_data, c("data.frame", "DataFrame", "array", "Matrix", "SummarizedExperiment"))) { |
|
375 | -+ | ||
185 | +130x |
- visibility_property = FALSE) {+ get_object_filter_overview_array(filtered_data, unfiltered_data, dataname, subject_keys) |
|
376 | -! | +||
186 | +1x |
- checkmate::assert_string(selector)+ } else if (inherits(filtered_data, "MultiAssayExperiment")) { |
|
377 | +187 | ! |
- checkmate::assert_flag(content_visibility_auto)+ get_object_filter_overview_MultiAssayExperiment(filtered_data, unfiltered_data, dataname) |
378 | -! | +||
188 | +
- checkmate::assert_flag(opacity_property)+ } else { |
||
379 | -! | +||
189 | +1x |
- checkmate::assert_flag(visibility_property)+ data.frame( |
|
380 | -+ | ||
190 | +1x |
-
+ dataname = dataname, |
|
381 | -! | +||
191 | +1x |
- private$wait_for_page_stability()+ obs = NA, |
|
382 | -+ | ||
192 | +1x |
-
+ obs_filtered = NA, |
|
383 | -! | +||
193 | +1x |
- testthat::skip_if_not(+ subjects = NA, |
|
384 | -! | +||
194 | +1x |
- self$get_js("typeof Element.prototype.checkVisibility === 'function'"),+ subjects_filtered = NA |
|
385 | -! | +||
195 | +
- "Element.prototype.checkVisibility is not supported in the current browser."+ ) |
||
386 | +196 |
- )+ } |
|
387 | +197 |
-
+ } |
|
388 | -! | +||
198 | +
- unlist(+ |
||
389 | -! | +||
199 | +
- self$get_js(+ #' @rdname module_data_summary |
||
390 | -! | +||
200 | +
- sprintf(+ get_object_filter_overview_array <- function(filtered_data, # nolint: object_length. |
||
391 | -! | +||
201 | +
- "Array.from(document.querySelectorAll('%s')).map(el => el.checkVisibility({%s, %s, %s}))",+ unfiltered_data, |
||
392 | -! | +||
202 | +
- selector,+ dataname, |
||
393 | +203 |
- # Extra parameters+ subject_keys) { |
|
394 | -! | +||
204 | +130x |
- sprintf("contentVisibilityAuto: %s", tolower(content_visibility_auto)),+ if (length(subject_keys) == 0) { |
|
395 | -! | +||
205 | +117x |
- sprintf("opacityProperty: %s", tolower(opacity_property)),+ data.frame( |
|
396 | -! | +||
206 | +117x |
- sprintf("visibilityProperty: %s", tolower(visibility_property))+ dataname = dataname, |
|
397 | -+ | ||
207 | +117x |
- )+ obs = ifelse(!is.null(nrow(unfiltered_data)), nrow(unfiltered_data), NA), |
|
398 | -+ | ||
208 | +117x |
- )+ obs_filtered = nrow(filtered_data), |
|
399 | -+ | ||
209 | +117x |
- )+ subjects = NA, |
|
400 | -+ | ||
210 | +117x |
- },+ subjects_filtered = NA |
|
401 | +211 |
- #' @description+ ) |
|
402 | +212 |
- #' Get the active filter variables from a dataset in the `teal` app.+ } else { |
|
403 | -+ | ||
213 | +13x |
- #'+ data.frame( |
|
404 | -+ | ||
214 | +13x |
- #' @param dataset_name (character) The name of the dataset to get the filter variables from.+ dataname = dataname, |
|
405 | -+ | ||
215 | +13x |
- #' If `NULL`, the filter variables for all the datasets will be returned in a list.+ obs = ifelse(!is.null(nrow(unfiltered_data)), nrow(unfiltered_data), NA),+ |
+ |
216 | +13x | +
+ obs_filtered = nrow(filtered_data),+ |
+ |
217 | +13x | +
+ subjects = nrow(unique(unfiltered_data[subject_keys])),+ |
+ |
218 | +13x | +
+ subjects_filtered = nrow(unique(filtered_data[subject_keys])) |
|
406 | +219 |
- get_active_data_filters = function(dataset_name = NULL) {+ ) |
|
407 | -! | +||
220 | +
- checkmate::check_string(dataset_name, null.ok = TRUE)+ } |
||
408 | -! | +||
221 | +
- datasets <- self$get_active_filter_vars()+ } |
||
409 | -! | +||
222 | +
- checkmate::assert_subset(dataset_name, datasets)+ |
||
410 | -! | +||
223 | +
- active_filters <- lapply(+ #' @rdname module_data_summary |
||
411 | -! | +||
224 | +
- datasets,+ get_object_filter_overview_MultiAssayExperiment <- function(filtered_data, # nolint: object_length, object_name. |
||
412 | -! | +||
225 | +
- function(x) {+ unfiltered_data, |
||
413 | -! | +||
226 | +
- var_names <- gsub(+ dataname) { |
||
414 | +227 | ! |
- pattern = "\\s",+ experiment_names <- names(unfiltered_data) |
415 | +228 | ! |
- replacement = "",+ mae_info <- data.frame( |
416 | +229 | ! |
- self$get_text(+ dataname = dataname, |
417 | +230 | ! |
- sprintf(+ obs = NA, |
418 | +231 | ! |
- "#%s-filters-%s .filter-card-varname",+ obs_filtered = NA, |
419 | +232 | ! |
- self$active_filters_ns(),+ subjects = nrow(unfiltered_data@colData), |
420 | +233 | ! |
- x- |
-
421 | -- |
- )+ subjects_filtered = nrow(filtered_data@colData) |
|
422 | +234 |
- )+ ) |
|
423 | +235 |
- )+ |
|
424 | +236 | ! |
- structure(+ experiment_obs_info <- do.call("rbind", lapply( |
425 | +237 | ! |
- lapply(var_names, private$get_active_filter_selection, dataset_name = x),+ experiment_names, |
426 | +238 | ! |
- names = var_names+ function(experiment_name) { |
427 | -+ | ||
239 | +! |
- )+ transform( |
|
428 | -+ | ||
240 | +! |
- }+ get_object_filter_overview( |
|
429 | -+ | ||
241 | +! |
- )+ filtered_data[[experiment_name]], |
|
430 | +242 | ! |
- names(active_filters) <- datasets+ unfiltered_data[[experiment_name]], |
431 | +243 | ! |
- if (is.null(dataset_name)) {+ dataname = experiment_name, |
432 | +244 | ! |
- return(active_filters)+ subject_keys = join_keys() # empty join keys |
433 | +245 |
- }+ ), |
|
434 | +246 | ! |
- active_filters[[dataset_name]]- |
-
435 | -- |
- },+ dataname = paste0(" - ", experiment_name) |
|
436 | +247 |
- #' @description+ ) |
|
437 | +248 |
- #' Add a new variable from the dataset to be filtered.+ } |
|
438 | +249 |
- #'+ )) |
|
439 | +250 |
- #' @param dataset_name (character) The name of the dataset to add the filter variable to.+ |
|
440 | -+ | ||
251 | +! |
- #' @param var_name (character) The name of the variable to add to the filter panel.+ get_experiment_keys <- function(mae, experiment) { |
|
441 | -+ | ||
252 | +! |
- #' @param ... Additional arguments to be passed to `shinytest2::AppDriver$set_inputs`+ sample_subset <- mae@sampleMap[mae@sampleMap$colname %in% colnames(experiment), ] |
|
442 | -+ | ||
253 | +! |
- #'+ length(unique(sample_subset$primary)) |
|
443 | +254 |
- #' @return The `TealAppDriver` object invisibly.+ } |
|
444 | +255 |
- add_filter_var = function(dataset_name, var_name, ...) {+ |
|
445 | +256 | ! |
- checkmate::check_string(dataset_name)+ experiment_subjects_info <- do.call("rbind", lapply( |
446 | +257 | ! |
- checkmate::check_string(var_name)+ experiment_names, |
447 | +258 | ! |
- private$set_active_ns()+ function(experiment_name) { |
448 | +259 | ! |
- self$click(+ data.frame( |
449 | +260 | ! |
- selector = sprintf(+ subjects = get_experiment_keys(filtered_data, unfiltered_data[[experiment_name]]), |
450 | +261 | ! |
- "#%s-filters-%s-add_filter_icon",+ subjects_filtered = get_experiment_keys(filtered_data, filtered_data[[experiment_name]]) |
451 | -! | +||
262 | +
- private$ns$filter_panel,+ ) |
||
452 | -! | +||
263 | +
- dataset_name+ } |
||
453 | +264 |
- )+ )) |
|
454 | +265 |
- )+ |
|
455 | +266 | ! |
- self$set_input(+ experiment_info <- cbind(experiment_obs_info[, c("dataname", "obs", "obs_filtered")], experiment_subjects_info) |
456 | +267 | ! |
- sprintf(+ rbind(mae_info, experiment_info) |
457 | -! | +||
268 | +
- "%s-filters-%s-%s-filter-var_to_add",+ } |
||
458 | -! | +
1 | +
- private$ns$filter_panel,+ # FilteredData ------ |
||
459 | -! | +||
2 | +
- dataset_name,+ |
||
460 | -! | +||
3 | +
- dataset_name+ #' Drive a `teal` application |
||
461 | +4 |
- ),+ #' |
|
462 | -! | +||
5 | +
- var_name,+ #' Extension of the `shinytest2::AppDriver` class with methods for |
||
463 | +6 |
- ...+ #' driving a teal application for performing interactions for `shinytest2` tests. |
|
464 | +7 |
- )+ #' |
|
465 | -! | +||
8 | +
- invisible(self)+ #' @keywords internal |
||
466 | +9 |
- },+ #' |
|
467 | +10 |
- #' @description+ TealAppDriver <- R6::R6Class( # nolint: object_name. |
|
468 | +11 |
- #' Remove an active filter variable of a dataset from the active filter variables panel.+ "TealAppDriver", |
|
469 | +12 |
- #'+ inherit = { |
|
470 | +13 |
- #' @param dataset_name (character) The name of the dataset to remove the filter variable from.+ if (!requireNamespace("shinytest2", quietly = TRUE)) { |
|
471 | +14 |
- #' If `NULL`, all the filter variables will be removed.+ stop("Please install 'shinytest2' package to use this class.") |
|
472 | +15 |
- #' @param var_name (character) The name of the variable to remove from the filter panel.+ } |
|
473 | +16 |
- #' If `NULL`, all the filter variables of the dataset will be removed.+ if (!requireNamespace("rvest", quietly = TRUE)) { |
|
474 | +17 |
- #'+ stop("Please install 'rvest' package to use this class.") |
|
475 | +18 |
- #' @return The `TealAppDriver` object invisibly.+ } |
|
476 | +19 |
- remove_filter_var = function(dataset_name = NULL, var_name = NULL) {+ shinytest2::AppDriver |
|
477 | -! | +||
20 | +
- checkmate::check_string(dataset_name, null.ok = TRUE)+ }, |
||
478 | -! | +||
21 | +
- checkmate::check_string(var_name, null.ok = TRUE)+ # public methods ---- |
||
479 | -! | +||
22 | +
- if (is.null(dataset_name)) {+ public = list( |
||
480 | -! | +||
23 | +
- remove_selector <- sprintf(+ #' @description |
||
481 | -! | +||
24 | +
- "#%s-active-remove_all_filters",- |
- ||
482 | -! | -
- self$active_filters_ns()+ #' Initialize a `TealAppDriver` object for testing a `teal` application. |
|
483 | +25 |
- )- |
- |
484 | -! | -
- } else if (is.null(var_name)) {- |
- |
485 | -! | -
- remove_selector <- sprintf(- |
- |
486 | -! | -
- "#%s-active-%s-remove_filters",- |
- |
487 | -! | -
- self$active_filters_ns(),- |
- |
488 | -! | -
- dataset_name+ #' |
|
489 | +26 |
- )+ #' @param data,modules,filter,title,header,footer,landing_popup arguments passed to `init` |
|
490 | +27 |
- } else {- |
- |
491 | -! | -
- remove_selector <- sprintf(+ #' @param timeout (`numeric`) Default number of milliseconds for any timeout or |
|
492 | -! | +||
28 | +
- "#%s-active-%s-filter-%s_%s-remove",+ #' timeout_ parameter in the `TealAppDriver` class. |
||
493 | -! | +||
29 | +
- self$active_filters_ns(),+ #' Defaults to 20s. |
||
494 | -! | +||
30 | +
- dataset_name,+ #' |
||
495 | -! | +||
31 | +
- dataset_name,+ #' See [`shinytest2::AppDriver`] `new` method for more details on how to change it |
||
496 | -! | +||
32 | +
- var_name+ #' via options or environment variables. |
||
497 | +33 |
- )+ #' @param load_timeout (`numeric`) How long to wait for the app to load, in ms. |
|
498 | +34 |
- }+ #' This includes the time to start R. Defaults to 100s. |
|
499 | -! | +||
35 | +
- self$click(+ #' |
||
500 | -! | +||
36 | +
- selector = remove_selector+ #' See [`shinytest2::AppDriver`] `new` method for more details on how to change it |
||
501 | +37 |
- )+ #' via options or environment variables |
|
502 | -! | +||
38 | +
- invisible(self)+ #' @param ... Additional arguments to be passed to `shinytest2::AppDriver$new` |
||
503 | +39 |
- },+ #' |
|
504 | +40 |
- #' @description+ #' |
|
505 | +41 |
- #' Set the active filter values for a variable of a dataset in the active filter variable panel.+ #' @return Object of class `TealAppDriver` |
|
506 | +42 |
- #'+ initialize = function(data, |
|
507 | +43 |
- #' @param dataset_name (character) The name of the dataset to set the filter value for.+ modules, |
|
508 | +44 |
- #' @param var_name (character) The name of the variable to set the filter value for.+ filter = teal_slices(), |
|
509 | +45 |
- #' @param input The value to set the filter to.+ title = build_app_title(), |
|
510 | +46 |
- #' @param ... Additional arguments to be passed to `shinytest2::AppDriver$set_inputs`+ header = tags$p(), |
|
511 | +47 |
- #'+ footer = tags$p(), |
|
512 | +48 |
- #' @return The `TealAppDriver` object invisibly.+ landing_popup = NULL, |
|
513 | +49 |
- set_active_filter_selection = function(dataset_name,+ timeout = rlang::missing_arg(), |
|
514 | +50 |
- var_name,+ load_timeout = rlang::missing_arg(), |
|
515 | +51 |
- input,+ ...) { |
|
516 | -+ | ||
52 | +! |
- ...) {+ private$data <- data |
|
517 | +53 | ! |
- checkmate::check_string(dataset_name)+ private$modules <- modules |
518 | +54 | ! |
- checkmate::check_string(var_name)+ private$filter <- filter |
519 | +55 | ! |
- checkmate::check_string(input)+ app <- init( |
520 | -+ | ||
56 | +! |
-
+ data = data, |
|
521 | +57 | ! |
- input_id_prefix <- sprintf(+ modules = modules, |
522 | +58 | ! |
- "%s-filters-%s-filter-%s_%s-inputs",+ filter = filter, |
523 | +59 | ! |
- self$active_filters_ns(),+ title = title, |
524 | +60 | ! |
- dataset_name,+ header = header, |
525 | +61 | ! |
- dataset_name,+ footer = footer, |
526 | +62 | ! |
- var_name+ landing_popup = landing_popup, |
527 | +63 |
) |
|
528 | +64 | ||
529 | +65 |
- # Find the type of filter (based on filter panel)+ # Default timeout is hardcoded to 4s in shinytest2:::resolve_timeout |
|
530 | -! | +||
66 | +
- supported_suffix <- c("selection", "selection_manual")+ # It must be set as parameter to the AppDriver |
||
531 | +67 | ! |
- slices_suffix <- supported_suffix[+ suppressWarnings( |
532 | +68 | ! |
- match(+ super$initialize( |
533 | +69 | ! |
- TRUE,+ app_dir = shinyApp(app$ui, app$server), |
534 | +70 | ! |
- vapply(+ name = "teal", |
535 | +71 | ! |
- supported_suffix,+ variant = shinytest2::platform_variant(), |
536 | +72 | ! |
- function(suffix) {+ timeout = rlang::maybe_missing(timeout, 20 * 1000), |
537 | +73 | ! |
- !is.null(self$get_html(sprintf("#%s-%s", input_id_prefix, suffix)))+ load_timeout = rlang::maybe_missing(load_timeout, 100 * 1000), |
538 | +74 |
- },- |
- |
539 | -! | -
- logical(1)+ ... |
|
540 | +75 |
- )+ ) |
|
541 | +76 |
- )+ ) |
|
542 | +77 |
- ]+ |
|
543 | +78 |
-
+ # Check for minimum version of Chrome that supports the tests |
|
544 | +79 |
- # Generate correct namespace+ # - Element.checkVisibility was added on 105 |
|
545 | +80 | ! |
- slices_input_id <- sprintf(+ chrome_version <- numeric_version( |
546 | +81 | ! |
- "%s-filters-%s-filter-%s_%s-inputs-%s",+ gsub( |
547 | +82 | ! |
- self$active_filters_ns(),+ "[[:alnum:]_]+/", # Prefix that ends with forward slash |
548 | -! | +||
83 | +
- dataset_name,+ "", |
||
549 | +84 | ! |
- dataset_name,+ self$get_chromote_session()$Browser$getVersion()$product |
550 | -! | +||
85 | +
- var_name,+ ), |
||
551 | +86 | ! |
- slices_suffix+ strict = FALSE |
552 | +87 |
) |
|
553 | +88 | ||
554 | -! | -
- if (identical(slices_suffix, "selection_manual")) {- |
- |
555 | +89 | ! |
- checkmate::assert_numeric(input, len = 2)+ required_version <- "121" |
556 | +90 | ||
557 | +91 | ! |
- dots <- rlang::list2(...)+ testthat::skip_if( |
558 | +92 | ! |
- checkmate::assert_choice(dots$priority_, formals(self$set_inputs)[["priority_"]], null.ok = TRUE)+ is.na(chrome_version), |
559 | +93 | ! |
- checkmate::assert_flag(dots$wait_, null.ok = TRUE)+ "Problem getting Chrome version, please contact the developers." |
560 | +94 |
-
+ ) |
|
561 | +95 | ! |
- self$run_js(+ testthat::skip_if( |
562 | +96 | ! |
- sprintf(+ chrome_version < required_version, |
563 | +97 | ! |
- "Shiny.setInputValue('%s:sw.numericRange', [%f, %f], {priority: '%s'})",+ sprintf( |
564 | +98 | ! |
- slices_input_id,+ "Chrome version '%s' is not supported, please upgrade to '%s' or higher", |
565 | +99 | ! |
- input[[1]],+ chrome_version, |
566 | +100 | ! |
- input[[2]],+ required_version |
567 | -! | +||
101 | +
- priority_ = ifelse(is.null(dots$priority_), "input", dots$priority_)+ ) |
||
568 | +102 |
- )+ ) |
|
569 | +103 |
- )+ # end od check |
|
570 | +104 | ||
571 | +105 | ! |
- if (isTRUE(dots$wait_) || is.null(dots$wait_)) {+ private$set_active_ns() |
572 | +106 | ! |
- self$wait_for_idle(+ self$wait_for_idle() |
573 | -! | +||
107 | +
- timeout = if (is.null(dots$timeout_)) rlang::missing_arg() else dots$timeout_+ }, |
||
574 | +108 |
- )+ #' @description |
|
575 | +109 |
- }+ #' Append parent [`shinytest2::AppDriver`] `click` method with a call to `waif_for_idle()` method. |
|
576 | -! | +||
110 | +
- } else if (identical(slices_suffix, "selection")) {+ #' @param ... arguments passed to parent [`shinytest2::AppDriver`] `click()` method. |
||
577 | -! | +||
111 | +
- self$set_input(+ click = function(...) { |
||
578 | +112 | ! |
- slices_input_id,+ super$click(...) |
579 | +113 | ! |
- input,+ private$wait_for_page_stability() |
580 | +114 |
- ...+ }, |
|
581 | +115 |
- )+ #' @description |
|
582 | +116 |
- } else {+ #' Check if the app has shiny errors. This checks for global shiny errors. |
|
583 | -! | +||
117 | +
- stop("Filter selection set not supported for this slice.")+ #' Note that any shiny errors dependent on shiny server render will only be captured after the teal module tab |
||
584 | +118 |
- }+ #' is visited because shiny will not trigger server computations when the tab is invisible. |
|
585 | +119 |
-
+ #' So, navigate to the module tab you want to test before calling this function. |
|
586 | -! | +||
120 | +
- invisible(self)+ #' Although, this catches errors hidden in the other module tabs if they are already rendered. |
||
587 | +121 | ++ |
+ expect_no_shiny_error = function() {+ |
+
122 | +! | +
+ testthat::expect_null(+ |
+ |
123 | +! | +
+ self$get_html(".shiny-output-error:not(.shiny-output-error-validation)"),+ |
+ |
124 | +! | +
+ info = "Shiny error is observed"+ |
+ |
125 | ++ |
+ )+ |
+ |
126 |
}, |
||
588 | +127 |
#' @description |
|
589 | +128 |
- #' Extract `html` attribute (found by a `selector`).+ #' Check if the app has no validation errors. This checks for global shiny validation errors. |
|
590 | +129 |
- #'+ expect_no_validation_error = function() {+ |
+ |
130 | +! | +
+ testthat::expect_null(+ |
+ |
131 | +! | +
+ self$get_html(".shiny-output-error-validation"),+ |
+ |
132 | +! | +
+ info = "No validation error is observed" |
|
591 | +133 |
- #' @param selector (`character(1)`) specifying the selector to be used to get the content of a specific node.+ ) |
|
592 | +134 |
- #' @param attribute (`character(1)`) name of an attribute to retrieve from a node specified by `selector`.+ }, |
|
593 | +135 |
- #'+ #' @description |
|
594 | +136 |
- #' @return The `character` vector.+ #' Check if the app has validation errors. This checks for global shiny validation errors. |
|
595 | +137 |
- get_attr = function(selector, attribute) {+ expect_validation_error = function() { |
|
596 | +138 | ! |
- rvest::html_attr(+ testthat::expect_false( |
597 | +139 | ! |
- rvest::html_nodes(self$get_html_rvest("html"), selector),+ is.null(self$get_html(".shiny-output-error-validation")), |
598 | +140 | ! |
- attribute+ info = "Validation error is not observed" |
599 | +141 |
) |
|
600 | +142 |
}, |
|
601 | +143 |
#' @description |
|
602 | +144 |
- #' Wrapper around `get_html` that passes the output directly to `rvest::read_html`.+ #' Set the input in the `teal` app. |
|
603 | +145 |
#' |
|
604 | +146 |
- #' @param selector `(character(1))` passed to `get_html`.+ #' @param input_id (character) The shiny input id with it's complete name space. |
|
605 | +147 |
- #'+ #' @param value The value to set the input to. |
|
606 | +148 |
- #' @return An XML document.+ #' @param ... Additional arguments to be passed to `shinytest2::AppDriver$set_inputs` |
|
607 | +149 |
- get_html_rvest = function(selector) {+ #' |
|
608 | -! | +||
150 | +
- rvest::read_html(self$get_html(selector))+ #' @return The `TealAppDriver` object invisibly. |
||
609 | +151 |
- },+ set_input = function(input_id, value, ...) { |
|
610 | -+ | ||
152 | +! |
- #' Wrapper around `get_url()` method that opens the app in the browser.+ do.call( |
|
611 | -+ | ||
153 | +! |
- #'+ self$set_inputs, |
|
612 | -+ | ||
154 | +! |
- #' @return Nothing. Opens the underlying teal app in the browser.+ c(setNames(list(value), input_id), list(...)) |
|
613 | +155 |
- open_url = function() {+ ) |
|
614 | +156 | ! |
- browseURL(self$get_url())+ invisible(self) |
615 | +157 |
}, |
|
616 | +158 |
#' @description |
|
617 | +159 |
- #' Waits until a specified input, output, or export value.+ #' Navigate the teal tabs in the `teal` app. |
|
618 | +160 |
- #' This function serves as a wrapper around the `wait_for_value` method,+ #' |
|
619 | +161 |
- #' providing a more flexible interface for waiting on different types of values within the active module namespace.+ #' @param tabs (character) Labels of tabs to navigate to. The order of the tabs is important, |
|
620 | +162 |
- #' @param input,output,export A name of an input, output, or export value.+ #' and it should start with the most parent level tab. |
|
621 | +163 |
- #' Only one of these parameters may be used.+ #' Note: In case the teal tab group has duplicate names, the first tab will be selected, |
|
622 | +164 |
- #' @param ... Must be empty. Allows for parameter expansion.+ #' If you wish to select the second tab with the same name, use the suffix "_1". |
|
623 | +165 |
- #' Parameter with additional value to passed in `wait_for_value`.+ #' If you wish to select the third tab with the same name, use the suffix "_2" and so on. |
|
624 | +166 |
- wait_for_active_module_value = function(input = rlang::missing_arg(),+ #' |
|
625 | +167 |
- output = rlang::missing_arg(),+ #' @return The `TealAppDriver` object invisibly. |
|
626 | +168 |
- export = rlang::missing_arg(),+ navigate_teal_tab = function(tabs) { |
|
627 | -+ | ||
169 | +! |
- ...) {+ checkmate::check_character(tabs, min.len = 1) |
|
628 | +170 | ! |
- ns <- shiny::NS(self$active_module_ns())+ for (tab in tabs) { |
629 | -+ | ||
171 | +! |
-
+ self$set_input( |
|
630 | +172 | ! |
- if (!rlang::is_missing(input) && checkmate::test_string(input, min.chars = 1)) input <- ns(input)+ "teal-teal_modules-active_tab", |
631 | +173 | ! |
- if (!rlang::is_missing(output) && checkmate::test_string(output, min.chars = 1)) output <- ns(output)+ get_unique_labels(tab), |
632 | +174 | ! |
- if (!rlang::is_missing(export) && checkmate::test_string(export, min.chars = 1)) export <- ns(export)+ wait_ = FALSE |
633 | +175 |
-
+ ) |
|
634 | -! | +||
176 | +
- self$wait_for_value(+ } |
||
635 | +177 | ! |
- input = input,+ self$wait_for_idle() |
636 | +178 | ! |
- output = output,+ private$set_active_ns() |
637 | +179 | ! |
- export = export,+ invisible(self) |
638 | +180 |
- ...+ }, |
|
639 | +181 |
- )+ #' @description |
|
640 | +182 |
- }+ #' Get the active shiny name space for different components of the teal app. |
|
641 | +183 |
- ),+ #' |
|
642 | +184 |
- # private members ----+ #' @return (`list`) The list of active shiny name space of the teal components. |
|
643 | +185 |
- private = list(+ active_ns = function() { |
|
644 | -+ | ||
186 | +! |
- # private attributes ----+ if (identical(private$ns$module, character(0))) { |
|
645 | -+ | ||
187 | +! |
- data = NULL,+ private$set_active_ns() |
|
646 | +188 |
- modules = NULL,+ } |
|
647 | -+ | ||
189 | +! |
- filter = teal_slices(),+ private$ns |
|
648 | +190 |
- ns = list(+ }, |
|
649 | +191 |
- module = character(0),+ #' @description |
|
650 | +192 |
- filter_panel = character(0)+ #' Get the active shiny name space for interacting with the module content. |
|
651 | +193 |
- ),+ #' |
|
652 | +194 |
- # private methods ----+ #' @return (`string`) The active shiny name space of the component. |
|
653 | +195 |
- set_active_ns = function() {+ active_module_ns = function() { |
|
654 | +196 | ! |
- all_inputs <- self$get_values()$input+ if (identical(private$ns$module, character(0))) { |
655 | +197 | ! |
- active_tab_inputs <- all_inputs[grepl("-active_tab$", names(all_inputs))]+ private$set_active_ns() |
656 | +198 | - - | -|
657 | -! | -
- tab_ns <- unlist(lapply(names(active_tab_inputs), function(name) {- |
- |
658 | -! | -
- gsub(+ } |
|
659 | +199 | ! |
- pattern = "-active_tab$",+ private$ns$module |
660 | -! | +||
200 | +
- replacement = sprintf("-%s", active_tab_inputs[[name]]),+ }, |
||
661 | -! | +||
201 | +
- name+ #' @description |
||
662 | +202 |
- )+ #' Get the active shiny name space bound with a custom `element` name. |
|
663 | +203 |
- }))+ #' |
|
664 | -! | +||
204 | +
- active_ns <- tab_ns[1]+ #' @param element `character(1)` custom element name. |
||
665 | -! | +||
205 | +
- if (length(tab_ns) > 1) {+ #' |
||
666 | -! | +||
206 | +
- for (i in 2:length(tab_ns)) {+ #' @return (`string`) The active shiny name space of the component bound with the input `element`. |
||
667 | -! | +||
207 | +
- next_ns <- tab_ns[i]+ active_module_element = function(element) { |
||
668 | +208 | ! |
- if (grepl(pattern = active_ns, next_ns)) {+ checkmate::assert_string(element) |
669 | +209 | ! |
- active_ns <- next_ns+ sprintf("#%s-%s", self$active_module_ns(), element) |
670 | +210 |
- }+ }, |
|
671 | +211 |
- }+ #' @description |
|
672 | +212 |
- }+ #' Get the text of the active shiny name space bound with a custom `element` name. |
|
673 | -! | +||
213 | +
- private$ns$module <- sprintf("%s-%s", active_ns, "module")+ #' |
||
674 | +214 |
-
+ #' @param element `character(1)` the text of the custom element name. |
|
675 | -! | +||
215 | +
- components <- c("filter_panel", "data_summary")+ #' |
||
676 | -! | +||
216 | +
- for (component in components) {+ #' @return (`string`) The text of the active shiny name space of the component bound with the input `element`. |
||
677 | +217 |
- if (+ active_module_element_text = function(element) { |
|
678 | +218 | ! |
- !is.null(self$get_html(sprintf("#%s-%s-panel", active_ns, component))) ||+ checkmate::assert_string(element) |
679 | +219 | ! |
- !is.null(self$get_html(sprintf("#%s-%s-table", active_ns, component)))+ self$get_text(self$active_module_element(element)) |
680 | +220 |
- ) {+ }, |
|
681 | -! | +||
221 | +
- private$ns[[component]] <- sprintf("%s-%s", active_ns, component)+ #' @description |
||
682 | +222 |
- } else {- |
- |
683 | -! | -
- private$ns[[component]] <- sprintf("%s-module_%s", active_ns, component)+ #' Get the active shiny name space for interacting with the filter panel. |
|
684 | +223 |
- }+ #' |
|
685 | +224 |
- }+ #' @return (`string`) The active shiny name space of the component. |
|
686 | +225 |
- },+ active_filters_ns = function() { |
|
687 | -+ | ||
226 | +! |
- # @description+ if (identical(private$ns$filter_panel, character(0))) { |
|
688 | -+ | ||
227 | +! |
- # Get the active filter values from the active filter selection of dataset from the filter panel.+ private$set_active_ns() |
|
689 | +228 |
- #+ } |
|
690 | -+ | ||
229 | +! |
- # @param dataset_name (character) The name of the dataset to get the filter values from.+ private$ns$filter_panel |
|
691 | +230 |
- # @param var_name (character) The name of the variable to get the filter values from.+ }, |
|
692 | +231 |
- #+ #' @description |
|
693 | +232 |
- # @return The value of the active filter selection.+ #' Get the active shiny name space for interacting with the data-summary panel. |
|
694 | +233 |
- get_active_filter_selection = function(dataset_name, var_name) {- |
- |
695 | -! | -
- checkmate::check_string(dataset_name)- |
- |
696 | -! | -
- checkmate::check_string(var_name)+ #' |
|
697 | -! | +||
234 | +
- input_id_prefix <- sprintf(+ #' @return (`string`) The active shiny name space of the data-summary component. |
||
698 | -! | +||
235 | +
- "%s-filters-%s-filter-%s_%s-inputs",+ active_data_summary_ns = function() { |
||
699 | +236 | ! |
- self$active_filters_ns(),+ if (identical(private$ns$data_summary, character(0))) { |
700 | +237 | ! |
- dataset_name,+ private$set_active_ns() |
701 | -! | +||
238 | +
- dataset_name,+ } |
||
702 | +239 | ! |
- var_name+ private$ns$data_summary |
703 | +240 |
- )+ }, |
|
704 | +241 |
-
+ #' @description |
|
705 | +242 |
- # Find the type of filter (categorical or range)- |
- |
706 | -! | -
- supported_suffix <- c("selection", "selection_manual")+ #' Get the active shiny name space bound with a custom `element` name. |
|
707 | -! | +||
243 | +
- for (suffix in supported_suffix) {+ #' |
||
708 | -! | +||
244 | +
- if (!is.null(self$get_html(sprintf("#%s-%s", input_id_prefix, suffix)))) {+ #' @param element `character(1)` custom element name. |
||
709 | -! | +||
245 | +
- return(self$get_value(input = sprintf("%s-%s", input_id_prefix, suffix)))+ #' |
||
710 | +246 |
- }+ #' @return (`string`) The active shiny name space of the component bound with the input `element`. |
|
711 | +247 |
- }+ active_data_summary_element = function(element) { |
|
712 | -+ | ||
248 | +! |
-
+ checkmate::assert_string(element) |
|
713 | +249 | ! |
- NULL # If there are not any supported filters+ sprintf("#%s-%s", self$active_data_summary_ns(), element) |
714 | +250 |
}, |
|
715 | +251 |
- # @description+ #' @description |
|
716 | +252 |
- # Check if the page is stable without any `DOM` updates in the body of the app.+ #' Get the input from the module in the `teal` app. |
|
717 | +253 |
- # This is achieved by blocing the R process by sleeping until the page is unchanged till the `stability_period`.+ #' This function will only access inputs from the name space of the current active teal module. |
|
718 | +254 |
- # @param stability_period (`numeric(1)`) The time in milliseconds to wait till the page to be stable.+ #' |
|
719 | +255 |
- # @param check_interval (`numeric(1)`) The time in milliseconds to check for changes in the page.+ #' @param input_id (character) The shiny input id to get the value from. |
|
720 | +256 |
- # The stability check is reset when a change is detected in the page after sleeping for check_interval.+ #' |
|
721 | +257 |
- wait_for_page_stability = function(stability_period = 2000, check_interval = 200) {- |
- |
722 | -! | -
- previous_content <- self$get_html("body")- |
- |
723 | -! | -
- end_time <- Sys.time() + (stability_period / 1000)+ #' @return The value of the shiny input. |
|
724 | +258 | - - | -|
725 | -! | -
- repeat {+ get_active_module_input = function(input_id) { |
|
726 | +259 | ! |
- Sys.sleep(check_interval / 1000)+ checkmate::check_string(input_id) |
727 | +260 | ! |
- current_content <- self$get_html("body")+ self$get_value(input = sprintf("%s-%s", self$active_module_ns(), input_id)) |
728 | +261 |
-
+ }, |
|
729 | -! | +||
262 | +
- if (!identical(previous_content, current_content)) {+ #' @description |
||
730 | -! | +||
263 | +
- previous_content <- current_content+ #' Get the output from the module in the `teal` app. |
||
731 | -! | +||
264 | +
- end_time <- Sys.time() + (stability_period / 1000)+ #' This function will only access outputs from the name space of the current active teal module. |
||
732 | -! | +||
265 | +
- } else if (Sys.time() >= end_time) {+ #' |
||
733 | -! | +||
266 | +
- break+ #' @param output_id (character) The shiny output id to get the value from. |
||
734 | +267 |
- }+ #' |
|
735 | +268 |
- }+ #' @return The value of the shiny output. |
|
736 | +269 |
- }+ get_active_module_output = function(output_id) { |
|
737 | -+ | ||
270 | +! |
- )+ checkmate::check_string(output_id) |
|
738 | -+ | ||
271 | +! |
- )+ self$get_value(output = sprintf("%s-%s", self$active_module_ns(), output_id)) |
1 | +272 |
- #' Generate lockfile for application's environment reproducibility+ }, |
|
2 | +273 |
- #'+ #' @description |
|
3 | +274 |
- #' @param lockfile_path (`character`) path to the lockfile.+ #' Get the output from the module's `teal.widgets::table_with_settings` or `DT::DTOutput` in the `teal` app. |
|
4 | +275 |
- #'+ #' This function will only access outputs from the name space of the current active teal module. |
|
5 | +276 |
- #' @section Different ways of creating lockfile:+ #' |
|
6 | +277 |
- #' `teal` leverages [renv::snapshot()], which offers multiple methods for lockfile creation.+ #' @param table_id (`character(1)`) The id of the table in the active teal module's name space. |
|
7 | +278 |
- #'+ #' @param which (integer) If there is more than one table, which should be extracted. |
|
8 | +279 |
- #' - **Working directory lockfile**: `teal`, by default, will create an `implicit` type lockfile that uses+ #' By default it will look for a table that is built using `teal.widgets::table_with_settings`. |
|
9 | +280 |
- #' `renv::dependencies()` to detect all R packages in the current project's working directory.+ #' |
|
10 | +281 |
- #' - **`DESCRIPTION`-based lockfile**: To generate a lockfile based on a `DESCRIPTION` file in your working+ #' @return The data.frame with table contents. |
|
11 | +282 |
- #' directory, set `renv::settings$snapshot.type("explicit")`. The naming convention for `type` follows+ get_active_module_table_output = function(table_id, which = 1) { |
|
12 | -+ | ||
283 | +! |
- #' `renv::snapshot()`. For the `"explicit"` type, refer to `renv::settings$package.dependency.fields()` for the+ checkmate::check_number(which, lower = 1) |
|
13 | -+ | ||
284 | +! |
- #' `DESCRIPTION` fields included in the lockfile.+ checkmate::check_string(table_id) |
|
14 | -+ | ||
285 | +! |
- #' - **Custom files-based lockfile**: To specify custom files as the basis for the lockfile, set+ table <- rvest::html_table( |
|
15 | -+ | ||
286 | +! |
- #' `renv::settings$snapshot.type("custom")` and configure the `renv.snapshot.filter` option.+ self$get_html_rvest(self$active_module_element(table_id)), |
|
16 | -+ | ||
287 | +! |
- #'+ fill = TRUE |
|
17 | +288 |
- #' @section lockfile usage:+ ) |
|
18 | -+ | ||
289 | +! |
- #' After creating the lockfile, you can restore the application's environment using `renv::restore()`.+ if (length(table) == 0) { |
|
19 | -+ | ||
290 | +! |
- #'+ data.frame() |
|
20 | +291 |
- #' @seealso [renv::snapshot()], [renv::restore()].+ } else { |
|
21 | -+ | ||
292 | +! |
- #'+ table[[which]] |
|
22 | +293 |
- #' @return `NULL`+ } |
|
23 | +294 |
- #'+ }, |
|
24 | +295 |
- #' @name module_teal_lockfile+ #' @description |
|
25 | +296 |
- #' @rdname module_teal_lockfile+ #' Get the output from the module's `teal.widgets::plot_with_settings` in the `teal` app. |
|
26 | +297 |
- #'+ #' This function will only access plots from the name space of the current active teal module. |
|
27 | +298 |
- #' @keywords internal+ #' |
|
28 | +299 |
- NULL+ #' @param plot_id (`character(1)`) The id of the plot in the active teal module's name space. |
|
29 | +300 |
-
+ #' |
|
30 | +301 |
- #' @rdname module_teal_lockfile+ #' @return The `src` attribute as `character(1)` vector. |
|
31 | +302 |
- ui_teal_lockfile <- function(id) {+ get_active_module_plot_output = function(plot_id) { |
|
32 | +303 | ! |
- ns <- NS(id)+ checkmate::check_string(plot_id) |
33 | +304 | ! |
- shiny::tagList(+ self$get_attr( |
34 | +305 | ! |
- tags$span("", id = ns("lockFileStatus")),+ self$active_module_element(sprintf("%s-plot_main > img", plot_id)), |
35 | +306 | ! |
- shinyjs::disabled(downloadLink(ns("lockFileLink"), "Download lockfile"))+ "src" |
36 | +307 |
- )+ ) |
|
37 | +308 |
- }+ }, |
|
38 | +309 |
-
+ #' @description |
|
39 | +310 |
- #' @rdname module_teal_lockfile+ #' Set the input in the module in the `teal` app. |
|
40 | +311 |
- srv_teal_lockfile <- function(id) {+ #' This function will only set inputs in the name space of the current active teal module. |
|
41 | -71x | -
- moduleServer(id, function(input, output, session) {+ | |
312 | ++ |
+ #' |
|
42 | -71x | +||
313 | +
- logger::log_debug("Initialize srv_teal_lockfile.")+ #' @param input_id (character) The shiny input id to get the value from. |
||
43 | -71x | +||
314 | +
- enable_lockfile_download <- function() {+ #' @param value The value to set the input to. |
||
44 | -! | +||
315 | +
- shinyjs::html("lockFileStatus", "Application lockfile ready.")+ #' @param ... Additional arguments to be passed to `shinytest2::AppDriver$set_inputs`+ |
+ ||
316 | ++ |
+ #'+ |
+ |
317 | ++ |
+ #' @return The `TealAppDriver` object invisibly.+ |
+ |
318 | ++ |
+ set_active_module_input = function(input_id, value, ...) { |
|
45 | +319 | ! |
- shinyjs::hide("lockFileStatus", anim = TRUE)+ checkmate::check_string(input_id) |
46 | +320 | ! |
- shinyjs::enable("lockFileLink")+ checkmate::check_string(value) |
47 | +321 | ! |
- output$lockFileLink <- shiny::downloadHandler(+ self$set_input( |
48 | +322 | ! |
- filename = function() {+ sprintf("%s-%s", self$active_module_ns(), input_id), |
49 | +323 | ! |
- "renv.lock"+ value, |
50 | +324 |
- },+ ... |
|
51 | -! | +||
325 | +
- content = function(file) {+ ) |
||
52 | +326 | ! |
- file.copy(lockfile_path, file)+ dots <- rlang::list2(...) |
53 | +327 | ! |
- file- |
-
54 | -- |
- },+ if (!isFALSE(dots[["wait"]])) self$wait_for_idle() # Default behavior is to wait |
|
55 | +328 | ! |
- contentType = "application/json"+ invisible(self) |
56 | +329 |
- )+ }, |
|
57 | +330 |
- }+ #' @description |
|
58 | -71x | +||
331 | +
- disable_lockfile_download <- function() {+ #' Get the active datasets that can be accessed via the filter panel of the current active teal module. |
||
59 | -! | +||
332 | +
- warning("Lockfile creation failed.", call. = FALSE)+ get_active_filter_vars = function() { |
||
60 | +333 | ! |
- shinyjs::html("lockFileStatus", "Lockfile creation failed.")+ displayed_datasets_index <- self$is_visible( |
61 | +334 | ! |
- shinyjs::hide("lockFileLink")+ sprintf("#%s-filters-filter_active_vars_contents > span", self$active_filters_ns()) |
62 | +335 |
- }+ ) |
|
63 | +336 | ||
64 | -71x | +||
337 | +! |
- shiny::onStop(function() {+ available_datasets <- self$get_text( |
|
65 | -71x | +||
338 | +! |
- if (file.exists(lockfile_path) && !shiny::isRunning()) {+ sprintf( |
|
66 | -1x | +||
339 | +! |
- logger::log_debug("Removing lockfile after shutting down the app")+ "#%s-filters-filter_active_vars_contents .filter_panel_dataname", |
|
67 | -1x | +||
340 | +! |
- file.remove(lockfile_path)+ self$active_filters_ns() |
|
68 | +341 |
- }+ ) |
|
69 | +342 |
- })+ ) |
|
70 | +343 | ||
71 | -71x | -
- lockfile_path <- "teal_app.lock"- |
- |
72 | -71x | +||
344 | +! |
- mode <- getOption("teal.lockfile.mode", default = "")+ available_datasets[displayed_datasets_index] |
|
73 | +345 | - - | -|
74 | -71x | -
- if (!(mode %in% c("auto", "enabled", "disabled"))) {+ }, |
|
75 | -! | +||
346 | +
- stop("'teal.lockfile.mode' option can only be one of \"auto\", \"disabled\" or \"disabled\". ")+ #' @description |
||
76 | +347 |
- }+ #' Get the active data summary table |
|
77 | +348 |
-
+ #' @return `data.frame` |
|
78 | -71x | +||
349 | +
- if (mode == "disabled") {+ get_active_data_summary_table = function() { |
||
79 | -1x | +||
350 | +! |
- logger::log_debug("'teal.lockfile.mode' option is set to 'disabled'. Hiding lockfile download button.")+ summary_table <- rvest::html_table( |
|
80 | -1x | +||
351 | +! |
- shinyjs::hide("lockFileLink")+ self$get_html_rvest(self$active_data_summary_element("table")), |
|
81 | -1x | +||
352 | +! |
- return(NULL)+ fill = TRUE |
|
82 | -+ | ||
353 | +! |
- }+ )[[1]] |
|
83 | +354 | ||
84 | -70x | +||
355 | +! |
- if (file.exists(lockfile_path)) {+ col_names <- unlist(summary_table[1, ], use.names = FALSE) |
|
85 | +356 | ! |
- logger::log_debug("Lockfile has already been created for this app - skipping automatic creation.")+ summary_table <- summary_table[-1, ] |
86 | +357 | ! |
- enable_lockfile_download()+ colnames(summary_table) <- col_names |
87 | +358 | ! |
- return(NULL)+ if (nrow(summary_table) > 0) { |
88 | -+ | ||
359 | +! |
- }+ summary_table |
|
89 | +360 | - - | -|
90 | -70x | -
- if (mode == "auto" && .is_disabled_lockfile_scenario()) {- |
- |
91 | -69x | -
- logger::log_debug(+ } else { |
|
92 | -69x | +||
361 | +! |
- "Automatic lockfile creation disabled. Execution scenario satisfies teal:::.is_disabled_lockfile_scenario()."+ NULL |
|
93 | +362 |
- )- |
- |
94 | -69x | -
- shinyjs::hide("lockFileLink")- |
- |
95 | -69x | -
- return(NULL)+ } |
|
96 | +363 |
- }+ }, |
|
97 | +364 |
-
+ #' @description |
|
98 | -1x | +||
365 | +
- if (!.is_lockfile_deps_installed()) {+ #' Test if `DOM` elements are visible on the page with a JavaScript call. |
||
99 | -! | +||
366 | +
- warning("Automatic lockfile creation disabled. `mirai` and `renv` packages must be installed.")+ #' @param selector (`character(1)`) `CSS` selector to check visibility. |
||
100 | -! | +||
367 | +
- shinyjs::hide("lockFileLink")+ #' A `CSS` id will return only one element if the UI is well formed. |
||
101 | -! | +||
368 | +
- return(NULL)+ #' @param content_visibility_auto,opacity_property,visibility_property (`logical(1)`) See more information |
||
102 | +369 |
- }+ #' on <https://developer.mozilla.org/en-US/docs/Web/API/Element/checkVisibility>. |
|
103 | +370 |
-
+ #' |
|
104 | +371 |
- # - Will be run only if the lockfile doesn't exist (see the if-s above)+ #' @return Logical vector with all occurrences of the selector. |
|
105 | +372 |
- # - We render to the tempfile because the process might last after session is closed and we don't+ is_visible = function(selector, |
|
106 | +373 |
- # want to make a "teal_app.renv" then. This is why we copy only during active session.+ content_visibility_auto = FALSE, |
|
107 | -1x | +||
374 | +
- process <- .teal_lockfile_process_invoke(lockfile_path)+ opacity_property = FALSE, |
||
108 | -1x | +||
375 | +
- observeEvent(process$status(), {+ visibility_property = FALSE) { |
||
109 | +376 | ! |
- if (process$status() %in% c("initial", "running")) {+ checkmate::assert_string(selector) |
110 | +377 | ! |
- shinyjs::html("lockFileStatus", "Creating lockfile...")+ checkmate::assert_flag(content_visibility_auto) |
111 | +378 | ! |
- } else if (process$status() == "success") {+ checkmate::assert_flag(opacity_property) |
112 | +379 | ! |
- result <- process$result()+ checkmate::assert_flag(visibility_property) |
113 | -! | +||
380 | +
- if (any(grepl("Lockfile written to", result$out))) {+ |
||
114 | +381 | ! |
- logger::log_debug("Lockfile containing { length(result$res$Packages) } packages created.")+ private$wait_for_page_stability() |
115 | -! | +||
382 | +
- if (any(grepl("(WARNING|ERROR):", result$out))) {+ |
||
116 | +383 | ! |
- warning("Lockfile created with warning(s) or error(s):", call. = FALSE)+ testthat::skip_if_not( |
117 | +384 | ! |
- for (i in result$out) {+ self$get_js("typeof Element.prototype.checkVisibility === 'function'"), |
118 | +385 | ! |
- warning(i, call. = FALSE)+ "Element.prototype.checkVisibility is not supported in the current browser." |
119 | +386 |
- }+ ) |
|
120 | +387 |
- }+ |
|
121 | +388 | ! |
- enable_lockfile_download()- |
-
122 | -- |
- } else {+ unlist( |
|
123 | +389 | ! |
- disable_lockfile_download()+ self$get_js( |
124 | -+ | ||
390 | +! |
- }+ sprintf( |
|
125 | +391 | ! |
- } else if (process$status() == "error") {+ "Array.from(document.querySelectorAll('%s')).map(el => el.checkVisibility({%s, %s, %s}))", |
126 | +392 | ! |
- disable_lockfile_download()+ selector, |
127 | +393 |
- }+ # Extra parameters |
|
128 | -+ | ||
394 | +! |
- })+ sprintf("contentVisibilityAuto: %s", tolower(content_visibility_auto)), |
|
129 | -+ | ||
395 | +! |
-
+ sprintf("opacityProperty: %s", tolower(opacity_property)), |
|
130 | -1x | +||
396 | +! |
- NULL+ sprintf("visibilityProperty: %s", tolower(visibility_property)) |
|
131 | +397 |
- })+ ) |
|
132 | +398 |
- }+ ) |
|
133 | +399 |
-
+ ) |
|
134 | +400 |
- utils::globalVariables(c("opts", "sysenv", "libpaths", "wd", "lockfilepath", "run")) # needed for mirai call+ }, |
|
135 | +401 |
- #' @rdname module_teal_lockfile+ #' @description |
|
136 | +402 |
- .teal_lockfile_process_invoke <- function(lockfile_path) {- |
- |
137 | -1x | -
- mirai_obj <- NULL+ #' Get the active filter variables from a dataset in the `teal` app. |
|
138 | -1x | +||
403 | +
- process <- shiny::ExtendedTask$new(function() {+ #' |
||
139 | -1x | +||
404 | +
- m <- mirai::mirai(+ #' @param dataset_name (character) The name of the dataset to get the filter variables from. |
||
140 | +405 |
- {+ #' If `NULL`, the filter variables for all the datasets will be returned in a list. |
|
141 | -1x | +||
406 | +
- options(opts)+ get_active_data_filters = function(dataset_name = NULL) { |
||
142 | -1x | +||
407 | +! |
- do.call(Sys.setenv, sysenv)+ checkmate::check_string(dataset_name, null.ok = TRUE) |
|
143 | -1x | +||
408 | +! |
- .libPaths(libpaths)+ datasets <- self$get_active_filter_vars() |
|
144 | -1x | +||
409 | +! |
- setwd(wd)+ checkmate::assert_subset(dataset_name, datasets) |
|
145 | -1x | +||
410 | +! |
- run(lockfile_path = lockfile_path)+ active_filters <- lapply( |
|
146 | -+ | ||
411 | +! |
- },+ datasets, |
|
147 | -1x | +||
412 | +! |
- run = .renv_snapshot,+ function(x) { |
|
148 | -1x | +||
413 | +! |
- lockfile_path = lockfile_path,+ var_names <- gsub( |
|
149 | -1x | +||
414 | +! |
- opts = options(),+ pattern = "\\s", |
|
150 | -1x | +||
415 | +! |
- libpaths = .libPaths(),+ replacement = "", |
|
151 | -1x | +||
416 | +! |
- sysenv = as.list(Sys.getenv()),+ self$get_text( |
|
152 | -1x | +||
417 | +! |
- wd = getwd()+ sprintf( |
|
153 | -+ | ||
418 | +! |
- )+ "#%s-filters-%s .filter-card-varname", |
|
154 | -1x | +||
419 | +! |
- mirai_obj <<- m+ self$active_filters_ns(), |
|
155 | -1x | +||
420 | +! |
- m+ x |
|
156 | +421 |
- })+ ) |
|
157 | +422 |
-
+ ) |
|
158 | -1x | +||
423 | +
- shiny::onStop(function() {+ ) |
||
159 | -1x | +||
424 | +! |
- if (mirai::unresolved(mirai_obj)) {+ structure( |
|
160 | +425 | ! |
- logger::log_debug("Terminating a running lockfile process...")+ lapply(var_names, private$get_active_filter_selection, dataset_name = x), |
161 | +426 | ! |
- mirai::stop_mirai(mirai_obj) # this doesn't stop running - renv will be created even if session is closed+ names = var_names |
162 | +427 |
- }+ ) |
|
163 | +428 |
- })+ } |
|
164 | +429 |
-
+ ) |
|
165 | -1x | +||
430 | +! |
- suppressWarnings({ # 'package:stats' may not be available when loading+ names(active_filters) <- datasets |
|
166 | -1x | +||
431 | +! |
- process$invoke()+ if (is.null(dataset_name)) { |
|
167 | -+ | ||
432 | +! |
- })+ return(active_filters) |
|
168 | +433 |
-
+ } |
|
169 | -1x | +||
434 | +! |
- logger::log_debug("Lockfile creation started based on { getwd() }.")+ active_filters[[dataset_name]] |
|
170 | +435 |
-
+ }, |
|
171 | -1x | +||
436 | +
- process+ #' @description |
||
172 | +437 |
- }+ #' Add a new variable from the dataset to be filtered. |
|
173 | +438 |
-
+ #' |
|
174 | +439 |
- #' @rdname module_teal_lockfile+ #' @param dataset_name (character) The name of the dataset to add the filter variable to. |
|
175 | +440 |
- .renv_snapshot <- function(lockfile_path) {+ #' @param var_name (character) The name of the variable to add to the filter panel. |
|
176 | -1x | +||
441 | +
- out <- utils::capture.output(+ #' @param ... Additional arguments to be passed to `shinytest2::AppDriver$set_inputs` |
||
177 | -1x | +||
442 | +
- res <- renv::snapshot(+ #' |
||
178 | -1x | +||
443 | +
- lockfile = lockfile_path,+ #' @return The `TealAppDriver` object invisibly. |
||
179 | -1x | +||
444 | +
- prompt = FALSE,+ add_filter_var = function(dataset_name, var_name, ...) { |
||
180 | -1x | +||
445 | +! |
- force = TRUE,+ checkmate::check_string(dataset_name) |
|
181 | -1x | +||
446 | +! |
- type = renv::settings$snapshot.type() # see the section "Different ways of creating lockfile" above here+ checkmate::check_string(var_name) |
|
182 | -+ | ||
447 | +! |
- )+ private$set_active_ns() |
|
183 | -+ | ||
448 | +! |
- )+ self$click( |
|
184 | -+ | ||
449 | +! |
-
+ selector = sprintf( |
|
185 | -1x | +||
450 | +! |
- list(out = out, res = res)+ "#%s-filters-%s-add_filter_icon", |
|
186 | -+ | ||
451 | +! |
- }+ private$ns$filter_panel, |
|
187 | -+ | ||
452 | +! |
-
+ dataset_name |
|
188 | +453 |
- #' @rdname module_teal_lockfile+ ) |
|
189 | +454 |
- .is_lockfile_deps_installed <- function() {+ ) |
|
190 | -1x | +||
455 | +! |
- requireNamespace("mirai", quietly = TRUE) && requireNamespace("renv", quietly = TRUE)+ self$set_input( |
|
191 | -+ | ||
456 | +! |
- }+ sprintf( |
|
192 | -+ | ||
457 | +! |
-
+ "%s-filters-%s-%s-filter-var_to_add", |
|
193 | -+ | ||
458 | +! |
- #' @rdname module_teal_lockfile+ private$ns$filter_panel, |
|
194 | -+ | ||
459 | +! |
- .is_disabled_lockfile_scenario <- function() {+ dataset_name, |
|
195 | -69x | +||
460 | +! |
- identical(Sys.getenv("CALLR_IS_RUNNING"), "true") || # inside callr process+ dataset_name |
|
196 | -69x | +||
461 | +
- identical(Sys.getenv("TESTTHAT"), "true") || # inside devtools::test+ ), |
||
197 | -69x | +||
462 | +! |
- !identical(Sys.getenv("QUARTO_PROJECT_ROOT"), "") || # inside Quarto process+ var_name, |
|
198 | +463 |
- (+ ... |
|
199 | -69x | +||
464 | +
- ("CheckExEnv" %in% search()) || any(c("_R_CHECK_TIMINGS_", "_R_CHECK_LICENSE_") %in% names(Sys.getenv()))+ ) |
||
200 | -69x | +||
465 | +! |
- ) # inside R CMD CHECK+ invisible(self) |
|
201 | +466 |
- }+ }, |
1 | +467 |
- #' Get client timezone+ #' @description |
|
2 | +468 |
- #'+ #' Remove an active filter variable of a dataset from the active filter variables panel. |
|
3 | +469 |
- #' User timezone in the browser may be different to the one on the server.+ #' |
|
4 | +470 |
- #' This script can be run to register a `shiny` input which contains information about the timezone in the browser.+ #' @param dataset_name (character) The name of the dataset to remove the filter variable from. |
|
5 | +471 |
- #'+ #' If `NULL`, all the filter variables will be removed. |
|
6 | +472 |
- #' @param ns (`function`) namespace function passed from the `session` object in the `shiny` server.+ #' @param var_name (character) The name of the variable to remove from the filter panel. |
|
7 | +473 |
- #' For `shiny` modules this will allow for proper name spacing of the registered input.+ #' If `NULL`, all the filter variables of the dataset will be removed. |
|
8 | +474 |
- #'+ #' |
|
9 | +475 |
- #' @return `NULL`, invisibly.+ #' @return The `TealAppDriver` object invisibly. |
|
10 | +476 |
- #'+ remove_filter_var = function(dataset_name = NULL, var_name = NULL) { |
|
11 | -+ | ||
477 | +! |
- #' @keywords internal+ checkmate::check_string(dataset_name, null.ok = TRUE) |
|
12 | -+ | ||
478 | +! |
- #'+ checkmate::check_string(var_name, null.ok = TRUE) |
|
13 | -+ | ||
479 | +! |
- get_client_timezone <- function(ns) {+ if (is.null(dataset_name)) { |
|
14 | -71x | +||
480 | +! |
- script <- sprintf(+ remove_selector <- sprintf( |
|
15 | -71x | +||
481 | +! |
- "Shiny.setInputValue(`%s`, Intl.DateTimeFormat().resolvedOptions().timeZone)",+ "#%s-active-remove_all_filters", |
|
16 | -71x | +||
482 | +! |
- ns("timezone")+ self$active_filters_ns() |
|
17 | +483 |
- )+ ) |
|
18 | -71x | +||
484 | +! |
- shinyjs::runjs(script) # function does not return anything+ } else if (is.null(var_name)) { |
|
19 | -71x | +||
485 | +! |
- invisible(NULL)+ remove_selector <- sprintf( |
|
20 | -+ | ||
486 | +! |
- }+ "#%s-active-%s-remove_filters", |
|
21 | -+ | ||
487 | +! |
-
+ self$active_filters_ns(), |
|
22 | -+ | ||
488 | +! |
- #' Resolve the expected bootstrap theme+ dataset_name |
|
23 | +489 |
- #' @noRd+ ) |
|
24 | +490 |
- #' @keywords internal+ } else { |
|
25 | -+ | ||
491 | +! |
- get_teal_bs_theme <- function() {+ remove_selector <- sprintf( |
|
26 | -4x | +||
492 | +! |
- bs_theme <- getOption("teal.bs_theme")+ "#%s-active-%s-filter-%s_%s-remove", |
|
27 | -+ | ||
493 | +! |
-
+ self$active_filters_ns(), |
|
28 | -4x | +||
494 | +! |
- if (is.null(bs_theme)) {+ dataset_name, |
|
29 | -1x | +||
495 | +! |
- return(NULL)+ dataset_name,+ |
+ |
496 | +! | +
+ var_name |
|
30 | +497 |
- }+ ) |
|
31 | +498 |
-
+ } |
|
32 | -3x | +||
499 | +! |
- if (!checkmate::test_class(bs_theme, "bs_theme")) {+ self$click( |
|
33 | -2x | +||
500 | +! |
- warning(+ selector = remove_selector |
|
34 | -2x | +||
501 | +
- "Assertion on 'teal.bs_theme' option value failed: ",+ ) |
||
35 | -2x | +||
502 | +! |
- checkmate::check_class(bs_theme, "bs_theme"),+ invisible(self) |
|
36 | -2x | +||
503 | +
- ". The default Shiny Bootstrap theme will be used."+ }, |
||
37 | +504 |
- )+ #' @description |
|
38 | -2x | +||
505 | +
- return(NULL)+ #' Set the active filter values for a variable of a dataset in the active filter variable panel. |
||
39 | +506 |
- }+ #' |
|
40 | +507 |
-
+ #' @param dataset_name (character) The name of the dataset to set the filter value for. |
|
41 | -1x | +||
508 | +
- bs_theme+ #' @param var_name (character) The name of the variable to set the filter value for. |
||
42 | +509 |
- }+ #' @param input The value to set the filter to. |
|
43 | +510 |
-
+ #' @param ... Additional arguments to be passed to `shinytest2::AppDriver$set_inputs` |
|
44 | +511 |
- #' Return parentnames along with datanames.+ #' |
|
45 | +512 |
- #' @noRd+ #' @return The `TealAppDriver` object invisibly. |
|
46 | +513 |
- #' @keywords internal+ set_active_filter_selection = function(dataset_name, |
|
47 | +514 |
- .include_parent_datanames <- function(datanames, join_keys) {+ var_name, |
|
48 | -153x | +||
515 | +
- ordered_datanames <- datanames+ input, |
||
49 | -153x | +||
516 | +
- for (i in datanames) {+ ...) { |
||
50 | -276x | +||
517 | +! |
- parents <- character(0)+ checkmate::check_string(dataset_name) |
|
51 | -276x | +||
518 | +! |
- while (length(i) > 0) {+ checkmate::check_string(var_name) |
|
52 | -289x | +||
519 | +! |
- parent_i <- teal.data::parent(join_keys, i)+ checkmate::check_string(input) |
|
53 | -289x | +||
520 | +
- parents <- c(parent_i, parents)+ |
||
54 | -289x | +||
521 | +! |
- i <- parent_i+ input_id_prefix <- sprintf( |
|
55 | -+ | ||
522 | +! |
- }+ "%s-filters-%s-filter-%s_%s-inputs", |
|
56 | -276x | +||
523 | +! |
- ordered_datanames <- c(parents, ordered_datanames)+ self$active_filters_ns(), |
|
57 | -+ | ||
524 | +! |
- }+ dataset_name, |
|
58 | -153x | +||
525 | +! |
- unique(ordered_datanames)+ dataset_name, |
|
59 | -+ | ||
526 | +! |
- }+ var_name |
|
60 | +527 |
-
+ ) |
|
61 | +528 |
- #' Return topologicaly sorted datanames+ |
|
62 | +529 |
- #' @noRd+ # Find the type of filter (based on filter panel) |
|
63 | -+ | ||
530 | +! |
- #' @keywords internal+ supported_suffix <- c("selection", "selection_manual") |
|
64 | -+ | ||
531 | +! |
- .topologically_sort_datanames <- function(datanames, join_keys) {+ slices_suffix <- supported_suffix[ |
|
65 | -131x | +||
532 | +! |
- datanames_with_parents <- .include_parent_datanames(datanames, join_keys)+ match( |
|
66 | -131x | +||
533 | +! |
- intersect(datanames, datanames_with_parents)+ TRUE, |
|
67 | -+ | ||
534 | +! |
- }+ vapply( |
|
68 | -+ | ||
535 | +! |
-
+ supported_suffix, |
|
69 | -+ | ||
536 | +! |
- #' Create a `FilteredData`+ function(suffix) { |
|
70 | -+ | ||
537 | +! |
- #'+ !is.null(self$get_html(sprintf("#%s-%s", input_id_prefix, suffix))) |
|
71 | +538 |
- #' Create a `FilteredData` object from a `teal_data` object.+ }, |
|
72 | -+ | ||
539 | +! |
- #'+ logical(1) |
|
73 | +540 |
- #' @param x (`teal_data`) object+ ) |
|
74 | +541 |
- #' @param datanames (`character`) vector of data set names to include; must be subset of `datanames(x)`+ ) |
|
75 | +542 |
- #' @return A `FilteredData` object.+ ] |
|
76 | +543 |
- #' @keywords internal+ |
|
77 | +544 |
- teal_data_to_filtered_data <- function(x, datanames = ls(teal.code::get_env(x))) {+ # Generate correct namespace |
|
78 | -65x | +||
545 | +! |
- checkmate::assert_class(x, "teal_data")+ slices_input_id <- sprintf( |
|
79 | -65x | +||
546 | +! |
- checkmate::assert_character(datanames, min.chars = 1L, any.missing = FALSE)+ "%s-filters-%s-filter-%s_%s-inputs-%s", |
|
80 | -+ | ||
547 | +! |
- # Otherwise, FilteredData will be created in the modules' scope later+ self$active_filters_ns(), |
|
81 | -65x | +||
548 | +! |
- teal.slice::init_filtered_data(+ dataset_name, |
|
82 | -65x | +||
549 | +! |
- x = Filter(+ dataset_name, |
|
83 | -65x | +||
550 | +! |
- length,+ var_name, |
|
84 | -65x | +||
551 | +! |
- sapply(datanames, function(dn) x[[dn]], simplify = FALSE)+ slices_suffix |
|
85 | +552 |
- ),+ ) |
|
86 | -65x | +||
553 | +
- join_keys = teal.data::join_keys(x)+ |
||
87 | -+ | ||
554 | +! |
- )+ if (identical(slices_suffix, "selection_manual")) { |
|
88 | -+ | ||
555 | +! |
- }+ checkmate::assert_numeric(input, len = 2) |
|
89 | +556 | ||
90 | -+ | ||
557 | +! |
-
+ dots <- rlang::list2(...) |
|
91 | -+ | ||
558 | +! |
- #' Template function for `TealReportCard` creation and customization+ checkmate::assert_choice(dots$priority_, formals(self$set_inputs)[["priority_"]], null.ok = TRUE) |
|
92 | -+ | ||
559 | +! |
- #'+ checkmate::assert_flag(dots$wait_, null.ok = TRUE) |
|
93 | +560 |
- #' This function generates a report card with a title,+ |
|
94 | -+ | ||
561 | +! |
- #' an optional description, and the option to append the filter state list.+ self$run_js( |
|
95 | -+ | ||
562 | +! |
- #'+ sprintf( |
|
96 | -+ | ||
563 | +! |
- #' @param title (`character(1)`) title of the card (unless overwritten by label)+ "Shiny.setInputValue('%s:sw.numericRange', [%f, %f], {priority: '%s'})", |
|
97 | -+ | ||
564 | +! |
- #' @param label (`character(1)`) label provided by the user when adding the card+ slices_input_id, |
|
98 | -+ | ||
565 | +! |
- #' @param description (`character(1)`) optional, additional description+ input[[1]], |
|
99 | -+ | ||
566 | +! |
- #' @param with_filter (`logical(1)`) flag indicating to add filter state+ input[[2]], |
|
100 | -+ | ||
567 | +! |
- #' @param filter_panel_api (`FilterPanelAPI`) object with API that allows the generation+ priority_ = ifelse(is.null(dots$priority_), "input", dots$priority_) |
|
101 | +568 |
- #' of the filter state in the report+ ) |
|
102 | +569 |
- #'+ ) |
|
103 | +570 |
- #' @return (`TealReportCard`) populated with a title, description and filter state.+ |
|
104 | -+ | ||
571 | +! |
- #'+ if (isTRUE(dots$wait_) || is.null(dots$wait_)) { |
|
105 | -+ | ||
572 | +! |
- #' @export+ self$wait_for_idle(+ |
+ |
573 | +! | +
+ timeout = if (is.null(dots$timeout_)) rlang::missing_arg() else dots$timeout_ |
|
106 | +574 |
- report_card_template <- function(title, label, description = NULL, with_filter, filter_panel_api) {+ ) |
|
107 | -2x | +||
575 | +
- checkmate::assert_string(title)+ } |
||
108 | -2x | +||
576 | +! |
- checkmate::assert_string(label)+ } else if (identical(slices_suffix, "selection")) { |
|
109 | -2x | +||
577 | +! |
- checkmate::assert_string(description, null.ok = TRUE)+ self$set_input( |
|
110 | -2x | +||
578 | +! |
- checkmate::assert_flag(with_filter)+ slices_input_id, |
|
111 | -2x | +||
579 | +! |
- checkmate::assert_class(filter_panel_api, classes = "FilterPanelAPI")+ input, |
|
112 | +580 |
-
+ ... |
|
113 | -2x | +||
581 | +
- card <- teal::TealReportCard$new()+ ) |
||
114 | -2x | +||
582 | +
- title <- if (label == "") title else label+ } else { |
||
115 | -2x | +||
583 | +! |
- card$set_name(title)+ stop("Filter selection set not supported for this slice.") |
|
116 | -2x | +||
584 | +
- card$append_text(title, "header2")+ } |
||
117 | -1x | +||
585 | +
- if (!is.null(description)) card$append_text(description, "header3")+ |
||
118 | -1x | +||
586 | +! |
- if (with_filter) card$append_fs(filter_panel_api$get_filter_state())+ invisible(self) |
|
119 | -2x | +||
587 | +
- card+ }, |
||
120 | +588 |
- }+ #' @description |
|
121 | +589 |
-
+ #' Extract `html` attribute (found by a `selector`). |
|
122 | +590 |
-
+ #' |
|
123 | +591 |
- #' Check `datanames` in modules+ #' @param selector (`character(1)`) specifying the selector to be used to get the content of a specific node. |
|
124 | +592 |
- #'+ #' @param attribute (`character(1)`) name of an attribute to retrieve from a node specified by `selector`. |
|
125 | +593 |
- #' This function ensures specified `datanames` in modules match those in the data object,+ #' |
|
126 | +594 |
- #' returning error messages or `TRUE` for successful validation.+ #' @return The `character` vector. |
|
127 | +595 |
- #'+ get_attr = function(selector, attribute) {+ |
+ |
596 | +! | +
+ rvest::html_attr(+ |
+ |
597 | +! | +
+ rvest::html_nodes(self$get_html_rvest("html"), selector),+ |
+ |
598 | +! | +
+ attribute |
|
128 | +599 |
- #' @param modules (`teal_modules`) object+ ) |
|
129 | +600 |
- #' @param datanames (`character`) names of datasets available in the `data` object+ }, |
|
130 | +601 |
- #'+ #' @description |
|
131 | +602 |
- #' @return A `character(1)` containing error message or `TRUE` if validation passes.+ #' Wrapper around `get_html` that passes the output directly to `rvest::read_html`. |
|
132 | +603 |
- #' @keywords internal+ #' |
|
133 | +604 |
- check_modules_datanames <- function(modules, datanames) {+ #' @param selector `(character(1))` passed to `get_html`. |
|
134 | -111x | +||
605 | +
- checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"))+ #' |
||
135 | -111x | +||
606 | +
- checkmate::assert_character(datanames)+ #' @return An XML document. |
||
136 | +607 |
-
+ get_html_rvest = function(selector) { |
|
137 | -111x | +||
608 | +! |
- recursive_check_datanames <- function(modules, datanames) {+ rvest::read_html(self$get_html(selector)) |
|
138 | +609 |
- # check teal_modules against datanames+ }, |
|
139 | -129x | +||
610 | +
- if (inherits(modules, "teal_modules")) {+ #' Wrapper around `get_url()` method that opens the app in the browser. |
||
140 | -13x | +||
611 | +
- result <- lapply(modules$children, function(module) recursive_check_datanames(module, datanames = datanames))+ #' |
||
141 | -13x | +||
612 | +
- result <- result[vapply(result, Negate(is.null), logical(1L))]+ #' @return Nothing. Opens the underlying teal app in the browser. |
||
142 | -13x | +||
613 | +
- list(+ open_url = function() { |
||
143 | -13x | +||
614 | +! |
- string = do.call(c, as.list(unname(sapply(result, function(x) x$string)))),+ browseURL(self$get_url()) |
|
144 | -13x | +||
615 | +
- html = function(with_module_name = TRUE) {+ }, |
||
145 | -5x | +||
616 | +
- tagList(+ #' @description |
||
146 | -5x | +||
617 | +
- lapply(+ #' Waits until a specified input, output, or export value. |
||
147 | -5x | +||
618 | +
- result,+ #' This function serves as a wrapper around the `wait_for_value` method, |
||
148 | -5x | +||
619 | +
- function(x) x$html(with_module_name = with_module_name)+ #' providing a more flexible interface for waiting on different types of values within the active module namespace. |
||
149 | +620 |
- )+ #' @param input,output,export A name of an input, output, or export value. |
|
150 | +621 |
- )+ #' Only one of these parameters may be used. |
|
151 | +622 |
- }+ #' @param ... Must be empty. Allows for parameter expansion. |
|
152 | +623 |
- )+ #' Parameter with additional value to passed in `wait_for_value`. |
|
153 | +624 |
- } else {+ wait_for_active_module_value = function(input = rlang::missing_arg(), |
|
154 | -116x | +||
625 | +
- extra_datanames <- setdiff(modules$datanames, c("all", datanames))+ output = rlang::missing_arg(), |
||
155 | -116x | +||
626 | +
- if (length(extra_datanames)) {+ export = rlang::missing_arg(), |
||
156 | -4x | +||
627 | +
- list(+ ...) { |
||
157 | -4x | +||
628 | +! |
- string = build_datanames_error_message(+ ns <- shiny::NS(self$active_module_ns()) |
|
158 | -4x | +||
629 | +
- modules$label,+ |
||
159 | -4x | +||
630 | +! |
- datanames,+ if (!rlang::is_missing(input) && checkmate::test_string(input, min.chars = 1)) input <- ns(input) |
|
160 | -4x | +||
631 | +! |
- extra_datanames,+ if (!rlang::is_missing(output) && checkmate::test_string(output, min.chars = 1)) output <- ns(output) |
|
161 | -4x | +||
632 | +! |
- tags = list(+ if (!rlang::is_missing(export) && checkmate::test_string(export, min.chars = 1)) export <- ns(export) |
|
162 | -4x | +||
633 | +
- span = function(..., .noWS = NULL) { # nolint: object_name+ |
||
163 | -25x | +||
634 | +! |
- trimws(paste(..., sep = ifelse(is.null(.noWS), " ", ""), collapse = " "))+ self$wait_for_value( |
|
164 | -+ | ||
635 | +! |
- },+ input = input, |
|
165 | -4x | +||
636 | +! |
- code = function(x) toString(dQuote(x, q = FALSE))+ output = output, |
|
166 | -+ | ||
637 | +! |
- ),+ export = export, |
|
167 | -4x | +||
638 | +
- tagList = function(...) trimws(paste(...))+ ... |
||
168 | +639 |
- ),+ ) |
|
169 | +640 |
- # Build HTML representation of the error message with <pre> formatting+ } |
|
170 | -4x | +||
641 | +
- html = function(with_module_name = TRUE) {+ ), |
||
171 | -3x | +||
642 | +
- tagList(+ # private members ---- |
||
172 | -3x | +||
643 | +
- build_datanames_error_message(+ private = list( |
||
173 | -3x | +||
644 | +
- if (with_module_name) modules$label,+ # private attributes ---- |
||
174 | -3x | +||
645 | +
- datanames,+ data = NULL, |
||
175 | -3x | +||
646 | +
- extra_datanames+ modules = NULL, |
||
176 | +647 |
- ),+ filter = teal_slices(), |
|
177 | -3x | +||
648 | +
- tags$br(.noWS = "before")+ ns = list( |
||
178 | +649 |
- )+ module = character(0), |
|
179 | +650 |
- }+ filter_panel = character(0) |
|
180 | +651 |
- )+ ), |
|
181 | +652 |
- }+ # private methods ---- |
|
182 | +653 |
- }+ set_active_ns = function() { |
|
183 | -+ | ||
654 | +! |
- }+ all_inputs <- self$get_values()$input |
|
184 | -111x | +||
655 | +! |
- check_datanames <- recursive_check_datanames(modules, datanames)+ active_tab_inputs <- all_inputs[grepl("-active_tab$", names(all_inputs))] |
|
185 | -111x | +||
656 | +
- if (length(check_datanames)) {+ |
||
186 | -16x | +||
657 | +! |
- check_datanames+ tab_ns <- unlist(lapply(names(active_tab_inputs), function(name) { |
|
187 | -+ | ||
658 | +! |
- } else {+ gsub( |
|
188 | -95x | +||
659 | +! |
- TRUE+ pattern = "-active_tab$", |
|
189 | -+ | ||
660 | +! |
- }+ replacement = sprintf("-%s", active_tab_inputs[[name]]), |
|
190 | -+ | ||
661 | +! |
- }+ name |
|
191 | +662 |
-
+ ) |
|
192 | +663 |
- #' Check `datanames` in filters+ })) |
|
193 | -+ | ||
664 | +! |
- #'+ active_ns <- tab_ns[1] |
|
194 | -+ | ||
665 | +! |
- #' This function checks whether `datanames` in filters correspond to those in `data`,+ if (length(tab_ns) > 1) { |
|
195 | -+ | ||
666 | +! |
- #' returning character vector with error messages or `TRUE` if all checks pass.+ for (i in 2:length(tab_ns)) { |
|
196 | -+ | ||
667 | +! |
- #'+ next_ns <- tab_ns[i] |
|
197 | -+ | ||
668 | +! |
- #' @param filters (`teal_slices`) object+ if (grepl(pattern = active_ns, next_ns)) { |
|
198 | -+ | ||
669 | +! |
- #' @param datanames (`character`) names of datasets available in the `data` object+ active_ns <- next_ns |
|
199 | +670 |
- #'+ } |
|
200 | +671 |
- #' @return A `character(1)` containing error message or TRUE if validation passes.+ } |
|
201 | +672 |
- #' @keywords internal+ }+ |
+ |
673 | +! | +
+ private$ns$module <- sprintf("%s-%s", active_ns, "module") |
|
202 | +674 |
- check_filter_datanames <- function(filters, datanames) {+ |
|
203 | -65x | +||
675 | +! |
- checkmate::assert_class(filters, "teal_slices")+ components <- c("filter_panel", "data_summary") |
|
204 | -65x | +||
676 | +! |
- checkmate::assert_character(datanames)+ for (component in components) { |
|
205 | +677 |
-
+ if ( |
|
206 | -+ | ||
678 | +! |
- # check teal_slices against datanames+ !is.null(self$get_html(sprintf("#%s-%s-panel", active_ns, component))) || |
|
207 | -65x | +||
679 | +! |
- out <- unlist(sapply(+ !is.null(self$get_html(sprintf("#%s-%s-table", active_ns, component))) |
|
208 | -65x | +||
680 | +
- filters, function(filter) {+ ) { |
||
209 | -24x | +||
681 | +! |
- dataname <- shiny::isolate(filter$dataname)+ private$ns[[component]] <- sprintf("%s-%s", active_ns, component) |
|
210 | -24x | +||
682 | +
- if (!dataname %in% datanames) {+ } else { |
||
211 | -3x | +||
683 | +! |
- sprintf(+ private$ns[[component]] <- sprintf("%s-module_%s", active_ns, component) |
|
212 | -3x | +||
684 | +
- "- Filter '%s' refers to dataname not available in 'data':\n %s not in (%s)",+ } |
||
213 | -3x | +||
685 | +
- shiny::isolate(filter$id),+ } |
||
214 | -3x | +||
686 | +
- dQuote(dataname, q = FALSE),+ }, |
||
215 | -3x | +||
687 | +
- toString(dQuote(datanames, q = FALSE))+ # @description |
||
216 | +688 |
- )+ # Get the active filter values from the active filter selection of dataset from the filter panel. |
|
217 | +689 |
- }+ # |
|
218 | +690 |
- }+ # @param dataset_name (character) The name of the dataset to get the filter values from. |
|
219 | +691 |
- ))+ # @param var_name (character) The name of the variable to get the filter values from. |
|
220 | +692 |
-
+ # |
|
221 | +693 |
-
+ # @return The value of the active filter selection. |
|
222 | -65x | +||
694 | +
- if (length(out)) {+ get_active_filter_selection = function(dataset_name, var_name) { |
||
223 | -3x | +||
695 | +! |
- paste(out, collapse = "\n")+ checkmate::check_string(dataset_name) |
|
224 | -+ | ||
696 | +! |
- } else {+ checkmate::check_string(var_name) |
|
225 | -62x | +||
697 | +! |
- TRUE+ input_id_prefix <- sprintf( |
|
226 | -+ | ||
698 | +! |
- }+ "%s-filters-%s-filter-%s_%s-inputs", |
|
227 | -+ | ||
699 | +! |
- }+ self$active_filters_ns(), |
|
228 | -+ | ||
700 | +! |
-
+ dataset_name, |
|
229 | -+ | ||
701 | +! |
- #' Function for validating the title parameter of `teal::init`+ dataset_name, |
|
230 | -+ | ||
702 | +! |
- #'+ var_name |
|
231 | +703 |
- #' Checks if the input of the title from `teal::init` will create a valid title and favicon tag.+ ) |
|
232 | +704 |
- #' @param shiny_tag (`shiny.tag`) Object to validate for a valid title.+ |
|
233 | +705 |
- #' @keywords internal+ # Find the type of filter (categorical or range) |
|
234 | -+ | ||
706 | +! |
- validate_app_title_tag <- function(shiny_tag) {+ supported_suffix <- c("selection", "selection_manual") |
|
235 | -7x | -
- checkmate::assert_class(shiny_tag, "shiny.tag")- |
- |
236 | -7x | -
- checkmate::assert_true(shiny_tag$name == "head")- |
- |
237 | -6x | -
- child_names <- vapply(shiny_tag$children, `[[`, character(1L), "name")- |
- |
238 | -6x | +||
707 | +! |
- checkmate::assert_subset(c("title", "link"), child_names, .var.name = "child tags")+ for (suffix in supported_suffix) { |
|
239 | -4x | +||
708 | +! |
- rel_attr <- shiny_tag$children[[which(child_names == "link")]]$attribs$rel+ if (!is.null(self$get_html(sprintf("#%s-%s", input_id_prefix, suffix)))) { |
|
240 | -4x | +||
709 | +! |
- checkmate::assert_subset(+ return(self$get_value(input = sprintf("%s-%s", input_id_prefix, suffix))) |
|
241 | -4x | +||
710 | +
- rel_attr,+ } |
||
242 | -4x | +||
711 | +
- c("icon", "shortcut icon"),+ } |
||
243 | -4x | +||
712 | +
- .var.name = "Link tag's rel attribute",+ |
||
244 | -4x | +||
713 | +! |
- empty.ok = FALSE+ NULL # If there are not any supported filters |
|
245 | +714 |
- )+ }, |
|
246 | +715 |
- }+ # @description |
|
247 | +716 |
-
+ # Check if the page is stable without any `DOM` updates in the body of the app. |
|
248 | +717 |
- #' Build app title with favicon+ # This is achieved by blocing the R process by sleeping until the page is unchanged till the `stability_period`. |
|
249 | +718 |
- #'+ # @param stability_period (`numeric(1)`) The time in milliseconds to wait till the page to be stable. |
|
250 | +719 |
- #' A helper function to create the browser title along with a logo.+ # @param check_interval (`numeric(1)`) The time in milliseconds to check for changes in the page. |
|
251 | +720 |
- #'+ # The stability check is reset when a change is detected in the page after sleeping for check_interval. |
|
252 | +721 |
- #' @param title (`character`) The browser title for the `teal` app.+ wait_for_page_stability = function(stability_period = 2000, check_interval = 200) { |
|
253 | -+ | ||
722 | +! |
- #' @param favicon (`character`) The path for the icon for the title.+ previous_content <- self$get_html("body") |
|
254 | -+ | ||
723 | +! |
- #' The image/icon path can be remote or the static path accessible by `shiny`, like the `www/`+ end_time <- Sys.time() + (stability_period / 1000) |
|
255 | +724 |
- #'+ |
|
256 | -+ | ||
725 | +! |
- #' @return A `shiny.tag` containing the element that adds the title and logo to the `shiny` app.+ repeat { |
|
257 | -+ | ||
726 | +! |
- #' @export+ Sys.sleep(check_interval / 1000) |
|
258 | -+ | ||
727 | +! |
- build_app_title <- function(+ current_content <- self$get_html("body") |
|
259 | +728 |
- title = "teal app",+ |
|
260 | -+ | ||
729 | +! |
- favicon = "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/nest.png") {+ if (!identical(previous_content, current_content)) { |
|
261 | -12x | +||
730 | +! |
- checkmate::assert_string(title, null.ok = TRUE)+ previous_content <- current_content |
|
262 | -12x | +||
731 | +! |
- checkmate::assert_string(favicon, null.ok = TRUE)+ end_time <- Sys.time() + (stability_period / 1000) |
|
263 | -12x | +||
732 | +! |
- tags$head(+ } else if (Sys.time() >= end_time) { |
|
264 | -12x | +||
733 | +! |
- tags$title(title),+ break |
|
265 | -12x | +||
734 | +
- tags$link(+ } |
||
266 | -12x | +||
735 | +
- rel = "icon",+ } |
||
267 | -12x | +||
736 | +
- href = favicon,+ } |
||
268 | -12x | +||
737 | +
- sizes = "any"+ ) |
||
269 | +738 |
- )+ ) |
270 | +1 |
- )+ #' Filter panel module in teal |
||
271 | +2 |
- }+ #' |
||
272 | +3 |
-
+ #' Creates filter panel module from `teal_data` object and returns `teal_data`. It is build in a way |
||
273 | +4 |
- #' Application ID+ #' that filter panel changes and anything what happens before (e.g. [`module_init_data`]) is triggering |
||
274 | +5 |
- #'+ #' further reactive events only if something has changed and if the module is visible. Thanks to |
||
275 | +6 |
- #' Creates App ID used to match filter snapshots to application.+ #' this special implementation all modules' data are recalculated only for those modules which are |
||
276 | +7 |
- #'+ #' currently displayed. |
||
277 | +8 |
- #' Calculate app ID that will be used to stamp filter state snapshots.+ #' |
||
278 | +9 |
- #' App ID is a hash of the app's data and modules.+ #' @return A `eventReactive` containing `teal_data` containing filtered objects and filter code. |
||
279 | +10 |
- #' See "transferring snapshots" section in ?snapshot.+ #' `eventReactive` triggers only if all conditions are met: |
||
280 | +11 |
- #'+ #' - tab is selected (`is_active`) |
||
281 | +12 |
- #' @param data (`teal_data` or `teal_data_module`) as accepted by `init`+ #' - when filters are changed (`get_filter_expr` is different than previous) |
||
282 | +13 |
- #' @param modules (`teal_modules`) object as accepted by `init`+ #' |
||
283 | +14 |
- #'+ #' @inheritParams module_teal_module |
||
284 | +15 |
- #' @return A single character string.+ #' @param active_datanames (`reactive` returning `character`) this module's data names |
||
285 | +16 |
- #'+ #' @name module_filter_data |
||
286 | +17 |
#' @keywords internal |
||
287 | +18 |
- create_app_id <- function(data, modules) {+ NULL |
||
288 | -20x | +|||
19 | +
- checkmate::assert_multi_class(data, c("teal_data", "teal_data_module"))+ |
|||
289 | -19x | +|||
20 | +
- checkmate::assert_class(modules, "teal_modules")+ #' @rdname module_filter_data |
|||
290 | +21 |
-
+ ui_filter_data <- function(id) { |
||
291 | -18x | +|||
22 | +! |
- data <- if (inherits(data, "teal_data")) {+ ns <- shiny::NS(id) |
||
292 | -16x | +|||
23 | +! |
- as.list(teal.code::get_env(data))+ uiOutput(ns("panel")) |
||
293 | -18x | +|||
24 | +
- } else if (inherits(data, "teal_data_module")) {+ } |
|||
294 | -2x | +|||
25 | +
- deparse1(body(data$server))+ |
|||
295 | +26 |
- }+ #' @rdname module_filter_data |
||
296 | -18x | +|||
27 | +
- modules <- lapply(modules, defunction)+ srv_filter_data <- function(id, datasets, active_datanames, data_rv, is_active) { |
|||
297 | -+ | |||
28 | +95x |
-
+ assert_reactive(datasets) |
||
298 | -18x | +29 | +95x |
- rlang::hash(list(data = data, modules = modules))+ moduleServer(id, function(input, output, session) { |
299 | -+ | |||
30 | +95x |
- }+ active_corrected <- reactive(intersect(active_datanames(), datasets()$datanames())) |
||
300 | +31 | |||
301 | -+ | |||
32 | +95x |
- #' Go through list and extract bodies of encountered functions as string, recursively.+ output$panel <- renderUI({ |
||
302 | -+ | |||
33 | +83x |
- #' @keywords internal+ req(inherits(datasets(), "FilteredData"))+ |
+ ||
34 | +75x | +
+ isolate({ |
||
303 | +35 |
- #' @noRd+ # render will be triggered only when FilteredData object changes (not when filters change) |
||
304 | +36 |
- defunction <- function(x) {+ # technically it means that teal_data_module needs to be refreshed |
||
305 | -219x | +37 | +75x |
- if (is.list(x)) {+ logger::log_debug("srv_filter_panel rendering filter panel.") |
306 | -64x | +38 | +75x |
- lapply(x, defunction)+ if (length(active_corrected())) { |
307 | -155x | +39 | +74x |
- } else if (is.function(x)) {+ datasets()$srv_active("filters", active_datanames = active_corrected) |
308 | -48x | +40 | +74x |
- deparse1(body(x))+ datasets()$ui_active(session$ns("filters"), active_datanames = active_corrected) |
309 | +41 |
- } else {- |
- ||
310 | -107x | -
- x+ } |
||
311 | +42 |
- }+ }) |
||
312 | +43 |
- }+ }) |
||
313 | +44 | |||
314 | -+ | |||
45 | +95x |
- #' Get unique labels+ trigger_data <- .observe_active_filter_changed(datasets, is_active, active_corrected, data_rv) |
||
315 | +46 |
- #'+ |
||
316 | -+ | |||
47 | +95x |
- #' Get unique labels for the modules to avoid namespace conflicts.+ eventReactive(trigger_data(), { |
||
317 | -+ | |||
48 | +73x |
- #'+ .make_filtered_teal_data(modules, data = data_rv(), datasets = datasets(), datanames = active_corrected()) |
||
318 | +49 |
- #' @param labels (`character`) vector of labels+ }) |
||
319 | +50 |
- #'+ }) |
||
320 | +51 |
- #' @return (`character`) vector of unique labels+ } |
||
321 | +52 |
- #'+ |
||
322 | +53 |
- #' @keywords internal+ #' @rdname module_filter_data |
||
323 | +54 |
- get_unique_labels <- function(labels) {+ .make_filtered_teal_data <- function(modules, data, datasets = NULL, datanames) { |
||
324 | -136x | +55 | +73x |
- make.unique(gsub("[^[:alnum:]]", "_", tolower(labels)), sep = "_")+ data <- eval_code( |
325 | -+ | |||
56 | +73x |
- }+ data, |
||
326 | -+ | |||
57 | +73x |
-
+ paste0( |
||
327 | -+ | |||
58 | +73x |
- #' Remove ANSI escape sequences from a string+ ".raw_data <- list2env(list(", |
||
328 | -+ | |||
59 | +73x |
- #' @noRd+ toString(sprintf("%1$s = %1$s", datanames)), |
||
329 | -+ | |||
60 | +73x |
- strip_style <- function(string) {+ "))\n", |
||
330 | -7x | +61 | +73x |
- checkmate::assert_string(string)+ "lockEnvironment(.raw_data) #@linksto .raw_data" # this is environment and it is shared by qenvs. CAN'T MODIFY! |
331 | +62 |
-
+ ) |
||
332 | -7x | +|||
63 | +
- gsub(+ ) |
|||
333 | -7x | -
- "(?:(?:\\x{001b}\\[)|\\x{009b})(?:(?:[0-9]{1,3})?(?:(?:;[0-9]{0,3})*)?[A-M|f-m])|\\x{001b}[A-M]",- |
- ||
334 | -+ | 64 | +73x |
- "",+ filtered_code <- .get_filter_expr(datasets = datasets, datanames = datanames) |
335 | -7x | +65 | +73x |
- string,+ filtered_teal_data <- .append_evaluated_code(data, filtered_code) |
336 | -7x | +66 | +73x |
- perl = TRUE,+ filtered_datasets <- sapply(datanames, function(x) datasets$get_data(x, filtered = TRUE), simplify = FALSE) |
337 | -7x | +67 | +73x |
- useBytes = TRUE+ filtered_teal_data <- .append_modified_data(filtered_teal_data, filtered_datasets) |
338 | -+ | |||
68 | +73x |
- )+ filtered_teal_data |
||
339 | +69 |
} |
||
340 | +70 | |||
341 | -- |
- #' Convert character list to human readable html with commas and "and"- |
- ||
342 | -- |
- #' @noRd- |
- ||
343 | -- |
- paste_datanames_character <- function(x,- |
- ||
344 | +71 |
- tags = list(span = shiny::tags$span, code = shiny::tags$code),+ #' @rdname module_filter_data |
||
345 | +72 |
- tagList = shiny::tagList) { # nolint: object_name.- |
- ||
346 | -12x | -
- checkmate::assert_character(x)+ .observe_active_filter_changed <- function(datasets, is_active, active_datanames, data_rv) { |
||
347 | -12x | +73 | +95x |
- do.call(+ previous_signature <- reactiveVal(NULL) |
348 | -12x | +74 | +95x |
- tagList,+ filter_changed <- reactive({ |
349 | -12x | +75 | +163x |
- lapply(seq_along(x), function(.ix) {+ req(inherits(datasets(), "FilteredData")) |
350 | -12x | +76 | +159x |
- tagList(+ new_signature <- c( |
351 | -12x | +77 | +159x |
- tags$code(x[.ix]),+ teal.data::get_code(data_rv()), |
352 | -12x | -
- if (.ix != length(x)) {- |
- ||
353 | -! | -
- tags$span(ifelse(.ix == length(x) - 1, " and ", ", "))- |
- ||
354 | -- |
- }- |
- ||
355 | -- |
- )- |
- ||
356 | -- |
- })- |
- ||
357 | -- |
- )- |
- ||
358 | -- |
- }- |
- ||
359 | -+ | 78 | +159x |
-
+ .get_filter_expr(datasets = datasets(), datanames = active_datanames()) |
360 | +79 |
- #' Build datanames error string for error message+ ) |
||
361 | -+ | |||
80 | +159x |
- #'+ if (!identical(previous_signature(), new_signature)) { |
||
362 | -+ | |||
81 | +76x |
- #' tags and tagList are overwritten in arguments allowing to create strings for+ previous_signature(new_signature) |
||
363 | -+ | |||
82 | +76x |
- #' logging purposes+ TRUE |
||
364 | +83 |
- #' @noRd+ } else { |
||
365 | -+ | |||
84 | +83x |
- build_datanames_error_message <- function(label = NULL,+ FALSE |
||
366 | +85 |
- datanames,+ } |
||
367 | +86 |
- extra_datanames,+ }) |
||
368 | +87 |
- tags = list(span = shiny::tags$span, code = shiny::tags$code),+ |
||
369 | -+ | |||
88 | +95x |
- tagList = shiny::tagList) { # nolint: object_name.+ trigger_data <- reactiveVal(NULL) |
||
370 | -7x | +89 | +95x |
- tags$span(+ observe({ |
371 | -7x | +90 | +197x |
- tags$span(ifelse(length(extra_datanames) > 1, "Datasets", "Dataset")),+ if (isTRUE(is_active() && filter_changed())) { |
372 | -7x | +91 | +76x |
- paste_datanames_character(extra_datanames, tags, tagList),+ isolate({ |
373 | -7x | +92 | +76x |
- tags$span(+ if (is.null(trigger_data())) { |
374 | -7x | +93 | +68x |
- paste0(+ trigger_data(0) |
375 | -7x | +|||
94 | +
- ifelse(length(extra_datanames) > 1, "are missing", "is missing"),+ } else { |
|||
376 | -7x | +95 | +8x |
- ifelse(is.null(label), ".", sprintf(" for tab '%s'.", label))+ trigger_data(trigger_data() + 1) |
377 | +96 |
- )+ } |
||
378 | +97 |
- ),- |
- ||
379 | -7x | -
- if (length(datanames) >= 1) {- |
- ||
380 | -5x | -
- tagList(- |
- ||
381 | -5x | -
- tags$span(ifelse(length(datanames) > 1, "Datasets", "Dataset")),- |
- ||
382 | -5x | -
- tags$span("available in data:"),+ }) |
||
383 | -5x | +|||
98 | +
- tagList(+ } |
|||
384 | -5x | +|||
99 | +
- tags$span(+ }) |
|||
385 | -5x | +|||
100 | +
- paste_datanames_character(datanames, tags, tagList),+ |
|||
386 | -5x | +101 | +95x |
- tags$span(".", .noWS = "outside"),+ trigger_data |
387 | -5x | +|||
102 | +
- .noWS = c("outside")+ } |
|||
388 | +103 |
- )+ |
||
389 | +104 |
- )+ #' @rdname module_filter_data |
||
390 | +105 |
- )+ .get_filter_expr <- function(datasets, datanames) { |
||
391 | -+ | |||
106 | +232x |
- } else {+ if (length(datanames)) { |
||
392 | -2x | +107 | +229x |
- tags$span("No datasets are available in data.")+ teal.slice::get_filter_expr(datasets = datasets, datanames = datanames) |
393 | +108 |
- }+ } else {+ |
+ ||
109 | +3x | +
+ NULL |
||
394 | +110 |
- )+ } |
||
395 | +111 |
}@@ -9468,14 +9194,14 @@ teal coverage - 57.64% |
1 |
- #' Filter state snapshot management+ #' Manage multiple `FilteredData` objects |
||
3 |
- #' Capture and restore snapshots of the global (app) filter state.+ #' @description |
||
4 |
- #'+ #' Oversee filter states across the entire application. |
||
5 |
- #' This module introduces snapshots: stored descriptions of the filter state of the entire application.+ #' |
||
6 |
- #' Snapshots allow the user to save the current filter state of the application for later use in the session,+ #' @section Slices global: |
||
7 |
- #' as well as to save it to file in order to share it with an app developer or other users,+ #' The key role in maintaining the module-specific filter states is played by the `.slicesGlobal` |
||
8 |
- #' who in turn can upload it to their own session.+ #' object. It is a reference class that holds the following fields: |
||
9 |
- #'+ #' - `all_slices` (`reactiveVal`) - reactive value containing all filters registered in an app. |
||
10 |
- #' The snapshot manager is accessed with the camera icon in the tabset bar.+ #' - `module_slices_api` (`reactiveValues`) - reactive field containing references to each modules' |
||
11 |
- #' At the beginning of a session it presents three icons: a camera, an upload, and an circular arrow.+ #' `FilteredData` object methods. At this moment it is used only in `srv_filter_manager` to display |
||
12 |
- #' Clicking the camera captures a snapshot, clicking the upload adds a snapshot from a file+ #' the filter states in a table combining informations from `all_slices` and from |
||
13 |
- #' and applies the filter states therein, and clicking the arrow resets initial application state.+ #' `FilteredData$get_available_teal_slices()`. |
||
14 |
- #' As snapshots are added, they will show up as rows in a table and each will have a select button and a save button.+ #' |
||
15 |
- #'+ #' During a session only new filters are added to `all_slices` unless [`module_snapshot_manager`] is |
||
16 |
- #' @section Server logic:+ #' used to restore previous state. Filters from `all_slices` can be activated or deactivated in a |
||
17 |
- #' Snapshots are basically `teal_slices` objects, however, since each module is served by a separate instance+ #' module which is linked (both ways) by `attr(, "mapping")` so that: |
||
18 |
- #' of `FilteredData` and these objects require shared state, `teal_slice` is a `reactiveVal` so `teal_slices`+ #' - If module's filter is added or removed in its `FilteredData` object, this information is passed |
||
19 |
- #' cannot be stored as is. Therefore, `teal_slices` are reversibly converted to a list of lists representation+ #' to `SlicesGlobal` which updates `attr(, "mapping")` accordingly. |
||
20 |
- #' (attributes are maintained).+ #' - When mapping changes in a `SlicesGlobal`, filters are set or removed from module's |
||
21 |
- #'+ #' `FilteredData`. |
||
22 |
- #' Snapshots are stored in a `reactiveVal` as a named list.+ #' |
||
23 |
- #' The first snapshot is the initial state of the application and the user can add a snapshot whenever they see fit.+ #' @section Filter manager: |
||
24 |
- #'+ #' Filter-manager is split into two parts: |
||
25 |
- #' For every snapshot except the initial one, a piece of UI is generated that contains+ #' 1. `ui/srv_filter_manager_panel` - Called once for the whole app. This module observes changes in |
||
26 |
- #' the snapshot name, a select button to restore that snapshot, and a save button to save it to a file.+ #' the filters in `slices_global` and displays them in a table utilizing information from `mapping`: |
||
27 |
- #' The initial snapshot is restored by a separate "reset" button.+ #' - (`TRUE`) - filter is active in the module |
||
28 |
- #' It cannot be saved directly but a user is welcome to capture the initial state as a snapshot and save that.+ #' - (`FALSE`) - filter is inactive in the module |
||
29 |
- #'+ #' - (`NA`) - filter is not available in the module |
||
30 |
- #' @section Snapshot mechanics:+ #' 2. `ui/srv_module_filter_manager` - Called once for each `teal_module`. Handling filter states |
||
31 |
- #' When a snapshot is captured, the user is prompted to name it.+ #' for of single module and keeping module `FilteredData` consistent with `slices_global`, so that |
||
32 |
- #' Names are displayed as is but since they are used to create button ids,+ #' local filters are always reflected in the `slices_global` and its mapping and vice versa. |
||
33 |
- #' under the hood they are converted to syntactically valid strings.+ #' |
||
34 |
- #' New snapshot names are validated so that their valid versions are unique.+ #' |
||
35 |
- #' Leading and trailing white space is trimmed.+ #' @param id (`character(1)`) |
||
36 |
- #'+ #' `shiny` module instance id. |
||
37 |
- #' The module can read the global state of the application from `slices_global` and `mapping_matrix`.+ #' |
||
38 |
- #' The former provides a list of all existing `teal_slice`s and the latter says which slice is active in which module.+ #' @param slices_global (`reactiveVal`) |
||
39 |
- #' Once a name has been accepted, `slices_global` is converted to a list of lists - a snapshot.+ #' containing `teal_slices`. |
||
40 |
- #' The snapshot contains the `mapping` attribute of the initial application state+ #' |
||
41 |
- #' (or one that has been restored), which may not reflect the current one,+ #' @param module_fd (`FilteredData`) |
||
42 |
- #' so `mapping_matrix` is transformed to obtain the current mapping, i.e. a list that,+ #' Object containing the data to be filtered in a single `teal` module. |
||
43 |
- #' when passed to the `mapping` argument of [teal_slices()], would result in the current mapping.+ #' |
||
44 |
- #' This is substituted as the snapshot's `mapping` attribute and the snapshot is added to the snapshot list.+ #' @return |
||
45 |
- #'+ #' Module returns a `slices_global` (`reactiveVal`) containing a `teal_slices` object with mapping. |
||
46 |
- #' To restore app state, a snapshot is retrieved from storage and rebuilt into a `teal_slices` object.+ #' |
||
47 |
- #' Then state of all `FilteredData` objects (provided in `datasets`) is cleared+ #' @encoding UTF-8 |
||
48 |
- #' and set anew according to the `mapping` attribute of the snapshot.+ #' |
||
49 |
- #' The snapshot is then set as the current content of `slices_global`.+ #' @name module_filter_manager |
||
50 |
- #'+ #' @rdname module_filter_manager |
||
51 |
- #' To save a snapshot, the snapshot is retrieved and reassembled just like for restoring,+ #' |
||
52 |
- #' and then saved to file with [slices_store()].+ NULL |
||
53 |
- #'+ |
||
54 |
- #' When a snapshot is uploaded, it will first be added to storage just like a newly created one,+ #' @rdname module_filter_manager |
||
55 |
- #' and then used to restore app state much like a snapshot taken from storage.+ ui_filter_manager_panel <- function(id) { |
||
56 | -+ | ! |
- #' Upon clicking the upload icon the user will be prompted for a file to upload+ ns <- NS(id) |
57 | -+ | ! |
- #' and may choose to name the new snapshot. The name defaults to the name of the file (the extension is dropped)+ tags$button( |
58 | -+ | ! |
- #' and normal naming rules apply. Loading the file yields a `teal_slices` object,+ id = ns("show_filter_manager"), |
59 | -+ | ! |
- #' which is disassembled for storage and used directly for restoring app state.+ class = "btn action-button wunder_bar_button", |
60 | -+ | ! |
- #'+ title = "View filter mapping", |
61 | -+ | ! |
- #' @section Transferring snapshots:+ suppressMessages(icon("fas fa-grip")) |
62 |
- #' Snapshots uploaded from disk should only be used in the same application they come from,+ ) |
||
63 |
- #' _i.e._ an application that uses the same data and the same modules.+ } |
||
64 |
- #' To ensure this is the case, `init` stamps `teal_slices` with an app id that is stored in the `app_id` attribute of+ |
||
65 |
- #' a `teal_slices` object. When a snapshot is restored from file, its `app_id` is compared to that+ #' @rdname module_filter_manager |
||
66 |
- #' of the current app state and only if the match is the snapshot admitted to the session.+ #' @keywords internal |
||
67 |
- #'+ srv_filter_manager_panel <- function(id, slices_global) { |
||
68 | -+ | 69x |
- #' @section Bookmarks:+ checkmate::assert_string(id) |
69 | -+ | 69x |
- #' An `onBookmark` callback creates a snapshot of the current filter state.+ checkmate::assert_class(slices_global, ".slicesGlobal") |
70 | -+ | 69x |
- #' This is done on the app session, not the module session.+ moduleServer(id, function(input, output, session) { |
71 | -+ | 69x |
- #' (The snapshot will be retrieved by `module_teal` in order to set initial app state in a restored app.)+ setBookmarkExclude(c("show_filter_manager")) |
72 | -+ | 69x |
- #' Then that snapshot, and the previous snapshot history are dumped into the `values.rds` file in `<bookmark_dir>`.+ observeEvent(input$show_filter_manager, { |
73 | -+ | ! |
- #'+ logger::log_debug("srv_filter_manager_panel@1 show_filter_manager button has been clicked.") |
74 | -+ | ! |
- #' @param id (`character(1)`) `shiny` module instance id.+ showModal( |
75 | -+ | ! |
- #' @param slices_global (`reactiveVal`) that contains a `teal_slices` object+ modalDialog( |
76 | -+ | ! |
- #' containing all `teal_slice`s existing in the app, both active and inactive.+ ui_filter_manager(session$ns("filter_manager")), |
77 | -+ | ! |
- #'+ class = "filter_manager_modal", |
78 | -+ | ! |
- #' @return `list` containing the snapshot history, where each element is an unlisted `teal_slices` object.+ size = "l", |
79 | -+ | ! |
- #'+ footer = NULL, |
80 | -+ | ! |
- #' @name module_snapshot_manager+ easyClose = TRUE |
81 |
- #' @rdname module_snapshot_manager+ ) |
||
82 |
- #'+ ) |
||
83 |
- #' @author Aleksander Chlebowski+ }) |
||
84 | -+ | 69x |
- #' @keywords internal+ srv_filter_manager("filter_manager", slices_global = slices_global) |
85 |
- NULL+ }) |
||
86 |
-
+ } |
||
87 |
- #' @rdname module_snapshot_manager+ |
||
88 |
- ui_snapshot_manager_panel <- function(id) {+ #' @rdname module_filter_manager |
||
89 | -! | +
- ns <- NS(id)+ ui_filter_manager <- function(id) { |
|
90 | ! |
- tags$button(+ ns <- NS(id) |
|
91 | ! |
- id = ns("show_snapshot_manager"),+ actionButton(ns("filter_manager"), NULL, icon = icon("fas fa-filter")) |
|
92 | ! |
- class = "btn action-button wunder_bar_button",+ tags$div( |
|
93 | ! |
- title = "View filter mapping",+ class = "filter_manager_content", |
|
94 | ! |
- suppressMessages(icon("fas fa-camera"))+ tableOutput(ns("slices_table")) |
|
98 |
- #' @rdname module_snapshot_manager+ #' @rdname module_filter_manager |
||
99 |
- srv_snapshot_manager_panel <- function(id, slices_global) {+ srv_filter_manager <- function(id, slices_global) { |
||
100 | 69x |
- moduleServer(id, function(input, output, session) {+ checkmate::assert_string(id) |
|
101 | 69x |
- logger::log_debug("srv_snapshot_manager_panel initializing")+ checkmate::assert_class(slices_global, ".slicesGlobal") |
|
102 | -69x | +
- setBookmarkExclude(c("show_snapshot_manager"))+ |
|
103 | 69x |
- observeEvent(input$show_snapshot_manager, {+ moduleServer(id, function(input, output, session) { |
|
104 | -! | +69x |
- logger::log_debug("srv_snapshot_manager_panel@1 show_snapshot_manager button has been clicked.")+ logger::log_debug("filter_manager_srv initializing.") |
105 | -! | +
- showModal(+ |
|
106 | -! | +
- modalDialog(+ # Bookmark slices global with mapping. |
|
107 | -! | +69x |
- ui_snapshot_manager(session$ns("module")),+ session$onBookmark(function(state) { |
108 | ! |
- class = "snapshot_manager_modal",+ logger::log_debug("filter_manager_srv@onBookmark: storing filter state") |
|
109 | ! |
- size = "m",+ state$values$filter_state_on_bookmark <- as.list( |
|
110 | ! |
- footer = NULL,+ slices_global$all_slices(), |
|
111 | ! |
- easyClose = TRUE+ recursive = TRUE |
|
112 |
- )+ ) |
||
113 |
- )+ }) |
||
114 |
- })+ |
||
115 | 69x |
- srv_snapshot_manager("module", slices_global = slices_global)+ bookmarked_slices <- restoreValue(session$ns("filter_state_on_bookmark"), NULL) |
|
116 | -+ | 69x |
- })+ if (!is.null(bookmarked_slices)) { |
117 | -+ | ! |
- }+ logger::log_debug("filter_manager_srv: restoring filter state from bookmark.") |
118 | -+ | ! |
-
+ slices_global$slices_set(bookmarked_slices) |
119 |
- #' @rdname module_snapshot_manager+ } |
||
120 |
- ui_snapshot_manager <- function(id) {+ |
||
121 | -! | +69x |
- ns <- NS(id)+ mapping_table <- reactive({ |
122 | -! | +
- tags$div(+ # We want this to be reactive on slices_global$all_slices() only as get_available_teal_slices() |
|
123 | -! | +
- class = "manager_content",+ # is dependent on slices_global$all_slices(). |
|
124 | -! | +77x |
- tags$div(+ module_labels <- setdiff( |
125 | -! | +77x |
- class = "manager_table_row",+ names(attr(slices_global$all_slices(), "mapping")), |
126 | -! | +77x |
- tags$span(tags$b("Snapshot manager")),+ "Report previewer" |
127 | -! | +
- actionLink(ns("snapshot_add"), label = NULL, icon = icon("fas fa-camera"), title = "add snapshot"),+ ) |
|
128 | -! | +77x |
- actionLink(ns("snapshot_load"), label = NULL, icon = icon("fas fa-upload"), title = "upload snapshot"),+ isolate({ |
129 | -! | +77x |
- actionLink(ns("snapshot_reset"), label = NULL, icon = icon("fas fa-undo"), title = "reset initial state"),+ mm <- as.data.frame( |
130 | -! | +77x |
- NULL+ sapply( |
131 | -+ | 77x |
- ),+ module_labels, |
132 | -! | +77x |
- uiOutput(ns("snapshot_list"))+ simplify = FALSE, |
133 | -+ | 77x |
- )+ function(module_label) { |
134 | -+ | 90x |
- }+ available_slices <- slices_global$module_slices_api[[module_label]]$get_available_teal_slices() |
135 | -+ | 83x |
-
+ global_ids <- sapply(slices_global$all_slices(), `[[`, "id", simplify = FALSE) |
136 | -+ | 83x |
- #' @rdname module_snapshot_manager+ module_ids <- sapply(slices_global$slices_get(module_label), `[[`, "id", simplify = FALSE) |
137 | -+ | 83x |
- srv_snapshot_manager <- function(id, slices_global) {+ allowed_ids <- vapply(available_slices, `[[`, character(1L), "id") |
138 | -69x | +83x |
- checkmate::assert_character(id)+ active_ids <- global_ids %in% module_ids |
139 | -+ | 83x |
-
+ setNames(nm = global_ids, ifelse(global_ids %in% allowed_ids, active_ids, NA)) |
140 | -69x | +
- moduleServer(id, function(input, output, session) {+ } |
|
141 | -69x | +
- logger::log_debug("srv_snapshot_manager initializing")+ ), |
|
142 | -+ | 77x |
-
+ check.names = FALSE |
143 |
- # Set up bookmarking callbacks ----+ ) |
||
144 | -+ | 70x |
- # Register bookmark exclusions (all buttons and text fields).+ colnames(mm)[colnames(mm) == "global_filters"] <- "Global filters" |
145 | -69x | +
- setBookmarkExclude(c(+ |
|
146 | -69x | +70x |
- "snapshot_add", "snapshot_load", "snapshot_reset",+ mm |
147 | -69x | +
- "snapshot_name_accept", "snaphot_file_accept",+ }) |
|
148 | -69x | +
- "snapshot_name", "snapshot_file"+ }) |
|
149 |
- ))+ |
||
150 | -+ | 69x |
- # Add snapshot history to bookmark.+ output$slices_table <- renderTable( |
151 | 69x |
- session$onBookmark(function(state) {+ expr = { |
|
152 | -! | +77x |
- logger::log_debug("srv_snapshot_manager@onBookmark: storing snapshot and bookmark history")+ logger::log_debug("filter_manager_srv@1 rendering slices_table.") |
153 | -! | +77x |
- state$values$snapshot_history <- snapshot_history() # isolate this?+ mm <- mapping_table() |
154 |
- })+ |
||
155 |
-
+ # Display logical values as UTF characters. |
||
156 | -69x | +70x |
- ns <- session$ns+ mm[] <- lapply(mm, ifelse, yes = intToUtf8(9989), no = intToUtf8(10060)) |
157 | -+ | 70x |
-
+ mm[] <- lapply(mm, function(x) ifelse(is.na(x), intToUtf8(128306), x)) |
158 |
- # Track global filter states ----+ |
||
159 | -69x | +
- snapshot_history <- reactiveVal({+ # Display placeholder if no filters defined. |
|
160 | -+ | 70x |
- # Restore directly from bookmarked state, if applicable.+ if (nrow(mm) == 0L) { |
161 | -69x | +46x |
- restoreValue(+ mm <- data.frame(`Filter manager` = "No filters specified.", check.names = FALSE) |
162 | -69x | +46x |
- ns("snapshot_history"),+ rownames(mm) <- "" |
163 | -69x | +
- list("Initial application state" = shiny::isolate(as.list(slices_global$all_slices(), recursive = TRUE)))+ } |
|
164 | -+ | 70x |
- )+ mm |
165 |
- })+ }, |
||
166 | -+ | 69x |
-
+ rownames = TRUE |
167 |
- # Snapshot current application state ----+ ) |
||
168 |
- # Name snaphsot.+ |
||
169 | 69x |
- observeEvent(input$snapshot_add, {+ mapping_table # for testing purpose |
|
170 | -! | +
- logger::log_debug("srv_snapshot_manager: snapshot_add button clicked")+ }) |
|
171 | -! | +
- showModal(+ } |
|
172 | -! | +
- modalDialog(+ |
|
173 | -! | +
- textInput(ns("snapshot_name"), "Name the snapshot", width = "100%", placeholder = "Meaningful, unique name"),+ #' @rdname module_filter_manager |
|
174 | -! | +
- footer = tagList(+ srv_module_filter_manager <- function(id, module_fd, slices_global) { |
|
175 | -! | +95x |
- actionButton(ns("snapshot_name_accept"), "Accept", icon = icon("far fa-thumbs-up")),+ checkmate::assert_string(id) |
176 | -! | +95x |
- modalButton(label = "Cancel", icon = icon("far fa-thumbs-down"))+ assert_reactive(module_fd) |
177 | -+ | 95x |
- ),+ checkmate::assert_class(slices_global, ".slicesGlobal") |
178 | -! | +
- size = "s"+ |
|
179 | -+ | 95x |
- )+ moduleServer(id, function(input, output, session) { |
180 | -+ | 95x |
- )+ logger::log_debug("srv_module_filter_manager initializing for module: { id }.") |
181 |
- })+ # Track filter global and local states. |
||
182 | -+ | 95x |
- # Store snaphsot.+ slices_global_module <- reactive({ |
183 | -69x | +169x |
- observeEvent(input$snapshot_name_accept, {+ slices_global$slices_get(module_label = id) |
184 | -! | +
- logger::log_debug("srv_snapshot_manager: snapshot_name_accept button clicked")+ }) |
|
185 | -! | +95x |
- snapshot_name <- trimws(input$snapshot_name)+ slices_module <- reactive(req(module_fd())$get_filter_state()) |
186 | -! | +
- if (identical(snapshot_name, "")) {+ |
|
187 | -! | +95x |
- logger::log_debug("srv_snapshot_manager: snapshot name rejected")+ module_fd_previous <- reactiveVal(NULL) |
188 | -! | +
- showNotification(+ |
|
189 | -! | +
- "Please name the snapshot.",+ # Set (reactively) available filters for the module. |
|
190 | -! | +95x |
- type = "message"+ obs1 <- observeEvent(module_fd(), priority = 1, { |
191 | -+ | 75x |
- )+ logger::log_debug("srv_module_filter_manager@1 setting initial slices for module: { id }.") |
192 | -! | +
- updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name")+ # Filters relevant for the module in module-specific app. |
|
193 | -! | +75x |
- } else if (is.element(make.names(snapshot_name), make.names(names(snapshot_history())))) {+ slices <- slices_global_module() |
194 | -! | +
- logger::log_debug("srv_snapshot_manager: snapshot name rejected")+ |
|
195 | -! | +
- showNotification(+ # Clean up previous filter states and refresh cache of previous module_fd with current |
|
196 | -! | +3x |
- "This name is in conflict with other snapshot names. Please choose a different one.",+ if (!is.null(module_fd_previous())) module_fd_previous()$finalize() |
197 | -! | +75x |
- type = "message"+ module_fd_previous(module_fd()) |
198 |
- )+ |
||
199 | -! | +
- updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name")+ # Setting filter states from slices_global: |
|
200 |
- } else {+ # 1. when app initializes slices_global set to initial filters (specified by app developer) |
||
201 | -! | +
- logger::log_debug("srv_snapshot_manager: snapshot name accepted, adding snapshot")+ # 2. when data reinitializes slices_global reflects latest filter states |
|
202 | -! | +
- snapshot <- as.list(slices_global$all_slices(), recursive = TRUE)+ |
|
203 | -! | +75x |
- snapshot_update <- c(snapshot_history(), list(snapshot))+ module_fd()$set_filter_state(slices) |
204 | -! | +
- names(snapshot_update)[length(snapshot_update)] <- snapshot_name+ |
|
205 | -! | +
- snapshot_history(snapshot_update)+ # irrelevant filters are discarded in FilteredData$set_available_teal_slices |
|
206 | -! | +
- removeModal()+ # it means we don't need to subset slices_global$all_slices() from filters refering to irrelevant datasets |
|
207 | -+ | 75x |
- # Reopen filter manager modal by clicking button in the main application.+ module_fd()$set_available_teal_slices(slices_global$all_slices) |
208 | -! | +
- shinyjs::click(id = "teal-wunder_bar-show_snapshot_manager", asis = TRUE)+ |
|
209 |
- }+ # this needed in filter_manager_srv |
||
210 | -+ | 75x |
- })+ slices_global$module_slices_api_set( |
211 | -+ | 75x |
-
+ id, |
212 | -+ | 75x |
- # Upload a snapshot file ----+ list( |
213 | -+ | 75x |
- # Select file.+ get_available_teal_slices = module_fd()$get_available_teal_slices(), |
214 | -69x | +75x |
- observeEvent(input$snapshot_load, {+ set_filter_state = module_fd()$set_filter_state, # for testing purpose |
215 | -! | +75x |
- logger::log_debug("srv_snapshot_manager: snapshot_load button clicked")+ get_filter_state = module_fd()$get_filter_state # for testing purpose |
216 | -! | +
- showModal(+ ) |
|
217 | -! | +
- modalDialog(+ ) |
|
218 | -! | +
- fileInput(ns("snapshot_file"), "Choose snapshot file", accept = ".json", width = "100%"),+ }) |
|
219 | -! | +
- textInput(+ |
|
220 | -! | +
- ns("snapshot_name"),+ # Update global state and mapping matrix when module filters change. |
|
221 | -! | +95x |
- "Name the snapshot (optional)",+ obs2 <- observeEvent(slices_module(), priority = 0, { |
222 | -! | +99x |
- width = "100%",+ this_slices <- slices_module() |
223 | -! | +99x |
- placeholder = "Meaningful, unique name"+ slices_global$slices_append(this_slices) # append new slices to the all_slices list |
224 | -+ | 99x |
- ),+ mapping_elem <- setNames(nm = id, list(vapply(this_slices, `[[`, character(1L), "id"))) |
225 | -! | +99x |
- footer = tagList(+ slices_global$slices_active(mapping_elem) |
226 | -! | +
- actionButton(ns("snaphot_file_accept"), "Accept", icon = icon("far fa-thumbs-up")),+ }) |
|
227 | -! | +
- modalButton(label = "Cancel", icon = icon("far fa-thumbs-down"))+ |
|
228 | -+ | 95x |
- )+ obs3 <- observeEvent(slices_global_module(), { |
229 | -+ | 116x |
- )+ global_vs_module <- setdiff_teal_slices(slices_global_module(), slices_module()) |
230 | -+ | 116x |
- )+ module_vs_global <- setdiff_teal_slices(slices_module(), slices_global_module()) |
231 | -+ | 108x |
- })+ if (length(global_vs_module) || length(module_vs_global)) { |
232 |
- # Store new snapshot to list and restore filter states.+ # Comment: (Nota Bene) Normally new filters for a module are added through module-filter-panel, and slices |
||
233 | -69x | +
- observeEvent(input$snaphot_file_accept, {+ # global are updated automatically so slices_module -> slices_global_module are equal. |
|
234 | -! | +
- logger::log_debug("srv_snapshot_manager: snapshot_file_accept button clicked")+ # this if is valid only when a change is made on the global level so the change needs to be propagated down |
|
235 | -! | +
- snapshot_name <- trimws(input$snapshot_name)+ # to the module (for example through snapshot manager). If it happens both slices are different |
|
236 | -! | +13x |
- if (identical(snapshot_name, "")) {+ logger::log_debug("srv_module_filter_manager@3 (N.B.) global state has changed for a module:{ id }.") |
237 | -! | +13x |
- logger::log_debug("srv_snapshot_manager: no snapshot name provided, naming after file")+ module_fd()$clear_filter_states() |
238 | -! | +13x |
- snapshot_name <- tools::file_path_sans_ext(input$snapshot_file$name)+ module_fd()$set_filter_state(slices_global_module()) |
240 | -! | +
- if (is.element(make.names(snapshot_name), make.names(names(snapshot_history())))) {+ }) |
|
241 | -! | +
- logger::log_debug("srv_snapshot_manager: snapshot name rejected")+ |
|
242 | -! | +95x |
- showNotification(+ slices_module # returned for testing purpose |
243 | -! | +
- "This name is in conflict with other snapshot names. Please choose a different one.",+ }) |
|
244 | -! | +
- type = "message"+ } |
|
245 |
- )+ |
||
246 | -! | +
- updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name")+ #' @importFrom shiny reactiveVal reactiveValues |
|
247 |
- } else {+ methods::setOldClass("reactiveVal") |
||
248 |
- # Restore snapshot and verify app compatibility.+ methods::setOldClass("reactivevalues") |
||
249 | -! | +
- logger::log_debug("srv_snapshot_manager: snapshot name accepted, loading snapshot")+ |
|
250 | -! | +
- snapshot_state <- try(slices_restore(input$snapshot_file$datapath))+ #' @importFrom methods new |
|
251 | -! | +
- if (!inherits(snapshot_state, "modules_teal_slices")) {+ #' @rdname module_filter_manager |
|
252 | -! | +
- logger::log_debug("srv_snapshot_manager: snapshot file corrupt")+ .slicesGlobal <- methods::setRefClass(".slicesGlobal", # nolint: object_name. |
|
253 | -! | +
- showNotification(+ fields = list( |
|
254 | -! | +
- "File appears to be corrupt.",+ all_slices = "reactiveVal", |
|
255 | -! | +
- type = "error"+ module_slices_api = "reactivevalues" |
|
256 |
- )+ ), |
||
257 | -! | +
- } else if (!identical(attr(snapshot_state, "app_id"), attr(slices_global$all_slices(), "app_id"))) {+ methods = list( |
|
258 | -! | +
- logger::log_debug("srv_snapshot_manager: snapshot not compatible with app")+ initialize = function(slices = teal_slices(), module_labels) { |
|
259 | -! | +70x |
- showNotification(+ shiny::isolate({ |
260 | -! | +70x |
- "This snapshot file is not compatible with the app and cannot be loaded.",+ checkmate::assert_class(slices, "teal_slices") |
261 | -! | +
- type = "warning"+ # needed on init to not mix "global_filters" with module-specific-slots |
|
262 | -+ | 70x |
- )+ if (isTRUE(attr(slices, "module_specific"))) { |
263 | -+ | 9x |
- } else {+ old_mapping <- attr(slices, "mapping") |
264 | -+ | 9x |
- # Add to snapshot history.+ new_mapping <- sapply(module_labels, simplify = FALSE, function(module_label) { |
265 | -! | +18x |
- logger::log_debug("srv_snapshot_manager: snapshot loaded, adding to history")+ unique(unlist(old_mapping[c(module_label, "global_filters")])) |
266 | -! | +
- snapshot <- as.list(slices_global$all_slices(), recursive = TRUE)+ }) |
|
267 | -! | +9x |
- snapshot_update <- c(snapshot_history(), list(snapshot))+ attr(slices, "mapping") <- new_mapping |
268 | -! | +
- names(snapshot_update)[length(snapshot_update)] <- snapshot_name+ } |
|
269 | -! | +70x |
- snapshot_history(snapshot_update)+ .self$all_slices <<- shiny::reactiveVal(slices) |
270 | -+ | 70x |
- ### Begin simplified restore procedure. ###+ .self$module_slices_api <<- shiny::reactiveValues() |
271 | -! | +70x |
- logger::log_debug("srv_snapshot_manager: restoring snapshot")+ .self$slices_append(slices) |
272 | -! | +70x |
- slices_global$slices_set(snapshot_state)+ .self$slices_active(attr(slices, "mapping")) |
273 | -! | +70x |
- removeModal()+ invisible(.self) |
274 |
- ### End simplified restore procedure. ###+ }) |
||
275 |
- }+ }, |
||
276 |
- }+ is_module_specific = function() { |
||
277 | -+ | 247x |
- })+ isTRUE(attr(.self$all_slices(), "module_specific")) |
278 |
- # Apply newly added snapshot.+ }, |
||
279 |
-
+ module_slices_api_set = function(module_label, functions_list) { |
||
280 | -+ | 75x |
- # Restore initial state ----+ shiny::isolate({ |
281 | -69x | +75x |
- observeEvent(input$snapshot_reset, {+ if (!.self$is_module_specific()) { |
282 | -2x | +61x |
- logger::log_debug("srv_snapshot_manager: snapshot_reset button clicked, restoring snapshot")+ module_label <- "global_filters" |
283 | -2x | +
- s <- "Initial application state"+ } |
|
284 | -+ | 75x |
- ### Begin restore procedure. ###+ if (!identical(.self$module_slices_api[[module_label]], functions_list)) { |
285 | -2x | +75x |
- snapshot <- snapshot_history()[[s]]+ .self$module_slices_api[[module_label]] <- functions_list |
286 |
- # todo: as.teal_slices looses module-mapping if is not global+ } |
||
287 | -2x | +75x |
- snapshot_state <- as.teal_slices(snapshot)+ invisible(.self) |
288 | -2x | +
- slices_global$slices_set(snapshot_state)+ }) |
|
289 | -2x | +
- removeModal()+ }, |
|
290 |
- ### End restore procedure. ###+ slices_deactivate_all = function(module_label) { |
||
291 | -+ | ! |
- })+ shiny::isolate({ |
292 | -+ | ! |
-
+ new_slices <- .self$all_slices() |
293 | -+ | ! |
- # Build snapshot table ----+ old_mapping <- attr(new_slices, "mapping") |
294 |
- # Create UI elements and server logic for the snapshot table.+ |
||
295 | -+ | ! |
- # Observers must be tracked to avoid duplication and excess reactivity.+ new_mapping <- if (.self$is_module_specific()) { |
296 | -+ | ! |
- # Remaining elements are tracked likewise for consistency and a slight speed margin.+ new_module_mapping <- setNames(nm = module_label, list(character(0))) |
297 | -69x | +! |
- observers <- reactiveValues()+ modifyList(old_mapping, new_module_mapping) |
298 | -69x | +! |
- handlers <- reactiveValues()+ } else if (missing(module_label)) { |
299 | -69x | +! |
- divs <- reactiveValues()+ lapply( |
300 | -+ | ! |
-
+ attr(.self$all_slices(), "mapping"), |
301 | -69x | +! |
- observeEvent(snapshot_history(), {+ function(x) character(0) |
302 | -59x | +
- logger::log_debug("srv_snapshot_manager: snapshot history modified, updating snapshot list")+ ) |
|
303 | -59x | +
- lapply(names(snapshot_history())[-1L], function(s) {+ } else { |
|
304 | ! |
- id_pickme <- sprintf("pickme_%s", make.names(s))+ old_mapping[[module_label]] <- character(0) |
|
305 | ! |
- id_saveme <- sprintf("saveme_%s", make.names(s))+ old_mapping |
|
306 | -! | +
- id_rowme <- sprintf("rowme_%s", make.names(s))+ } |
|
308 | -+ | ! |
- # Observer for restoring snapshot.+ if (!identical(new_mapping, old_mapping)) { |
309 | ! |
- if (!is.element(id_pickme, names(observers))) {+ logger::log_debug(".slicesGlobal@slices_deactivate_all: deactivating all slices.") |
|
310 | ! |
- observers[[id_pickme]] <- observeEvent(input[[id_pickme]], {+ attr(new_slices, "mapping") <- new_mapping |
|
311 | -+ | ! |
- ### Begin restore procedure. ###+ .self$all_slices(new_slices) |
312 | -! | +
- snapshot <- snapshot_history()[[s]]+ } |
|
313 | ! |
- snapshot_state <- as.teal_slices(snapshot)+ invisible(.self) |
|
314 |
-
+ }) |
||
315 | -! | +
- slices_global$slices_set(snapshot_state)+ }, |
|
316 | -! | +
- removeModal()+ slices_active = function(mapping_elem) { |
|
317 | -+ | 172x |
- ### End restore procedure. ###+ shiny::isolate({ |
318 | -+ | 172x |
- })+ if (.self$is_module_specific()) { |
319 | -+ | 32x |
- }+ new_mapping <- modifyList(attr(.self$all_slices(), "mapping"), mapping_elem) |
320 |
- # Create handler for downloading snapshot.+ } else { |
||
321 | -! | +140x |
- if (!is.element(id_saveme, names(handlers))) {+ new_mapping <- setNames(nm = "global_filters", list(unique(unlist(mapping_elem)))) |
322 | -! | +
- output[[id_saveme]] <- downloadHandler(+ } |
|
323 | -! | +
- filename = function() {+ |
|
324 | -! | +172x |
- sprintf("teal_snapshot_%s_%s.json", s, Sys.Date())+ if (!identical(new_mapping, attr(.self$all_slices(), "mapping"))) { |
325 | -+ | 115x |
- },+ mapping_modules <- toString(names(new_mapping)) |
326 | -! | +115x |
- content = function(file) {+ logger::log_debug(".slicesGlobal@slices_active: changing mapping for module(s): { mapping_modules }.") |
327 | -! | +115x |
- snapshot <- snapshot_history()[[s]]+ new_slices <- .self$all_slices() |
328 | -! | +115x |
- snapshot_state <- as.teal_slices(snapshot)+ attr(new_slices, "mapping") <- new_mapping |
329 | -! | +115x |
- slices_store(tss = snapshot_state, file = file)+ .self$all_slices(new_slices) |
330 |
- }+ } |
||
331 |
- )+ |
||
332 | -! | +172x |
- handlers[[id_saveme]] <- id_saveme+ invisible(.self) |
333 |
- }+ }) |
||
334 |
- # Create a row for the snapshot table.+ }, |
||
335 | -! | +
- if (!is.element(id_rowme, names(divs))) {+ # - only new filters are appended to the $all_slices |
|
336 | -! | +
- divs[[id_rowme]] <- tags$div(+ # - mapping is not updated here |
|
337 | -! | +
- class = "manager_table_row",+ slices_append = function(slices, activate = FALSE) { |
|
338 | -! | +172x |
- tags$span(tags$h5(s)),+ shiny::isolate({ |
339 | -! | +172x |
- actionLink(inputId = ns(id_pickme), label = icon("far fa-circle-check"), title = "select"),+ if (!is.teal_slices(slices)) { |
340 | ! |
- downloadLink(outputId = ns(id_saveme), label = icon("far fa-save"), title = "save to file")+ slices <- as.teal_slices(slices) |
|
341 |
- )+ } |
||
342 |
- }+ |
||
343 |
- })+ # to make sure that we don't unnecessary trigger $all_slices <reactiveVal> |
||
344 | -+ | 172x |
- })+ new_slices <- setdiff_teal_slices(slices, .self$all_slices()) |
345 | -+ | 172x |
-
+ old_mapping <- attr(.self$all_slices(), "mapping") |
346 | -+ | 172x |
- # Create table to display list of snapshots and their actions.+ if (length(new_slices)) { |
347 | -69x | +6x |
- output$snapshot_list <- renderUI({+ new_ids <- vapply(new_slices, `[[`, character(1L), "id") |
348 | -59x | +6x |
- rows <- rev(reactiveValuesToList(divs))+ logger::log_debug(".slicesGlobal@slices_append: appending new slice(s): { new_ids }.") |
349 | -59x | +6x |
- if (length(rows) == 0L) {+ slices_ids <- vapply(.self$all_slices(), `[[`, character(1L), "id") |
350 | -59x | +6x |
- tags$div(+ lapply(new_slices, function(slice) { |
351 | -59x | +
- class = "manager_placeholder",+ # In case the new state has the same id as an existing one, add a suffix |
|
352 | -59x | +6x |
- "Snapshots will appear here."+ if (slice$id %in% slices_ids) { |
353 | -+ | 1x |
- )+ slice$id <- utils::tail(make.unique(c(slices_ids, slice$id), sep = "_"), 1) |
354 |
- } else {+ } |
||
355 | -! | +
- rows+ }) |
|
356 |
- }+ |
||
357 | -+ | 6x |
- })+ new_slices_all <- c(.self$all_slices(), new_slices) |
358 | -+ | 6x |
-
+ attr(new_slices_all, "mapping") <- old_mapping |
359 | -69x | +6x |
- snapshot_history+ .self$all_slices(new_slices_all) |
360 |
- })+ } |
||
361 |
- }+ |
1 | -+ | ||
362 | +172x |
- #' Create a `tdata` object+ invisible(.self) |
|
2 | +363 |
- #'+ }) |
|
3 | +364 |
- #' @description `r lifecycle::badge("superseded")`+ }, |
|
4 | +365 |
- #'+ slices_get = function(module_label) { |
|
5 | -+ | ||
366 | +252x |
- #' Recent changes in `teal` cause modules to fail because modules expect a `tdata` object+ if (missing(module_label)) { |
|
6 | -+ | ||
367 | +! |
- #' to be passed to the `data` argument but instead they receive a `teal_data` object,+ .self$all_slices() |
|
7 | -- |
- #' which is additionally wrapped in a reactive expression in the server functions.- |
- |
8 | -- |
- #' In order to easily adapt such modules without a proper refactor,- |
- |
9 | -- |
- #' use this function to downgrade the `data` argument.- |
- |
10 | -- |
- #'- |
- |
11 | -- |
- #' @name tdata- |
- |
12 | -- |
- #' @param ... ignored- |
- |
13 | -- |
- #' @return nothing- |
- |
14 | -- |
- NULL- |
- |
15 | -- | - - | -|
16 | -- |
- #' @rdname tdata- |
- |
17 | -- |
- #' @export- |
- |
18 | +368 |
- new_tdata <- function(...) {- |
- |
19 | -! | -
- .deprecate_tdata_msg()+ } else { |
|
20 | -+ | ||
369 | +252x |
- }+ module_ids <- unlist(attr(.self$all_slices(), "mapping")[c(module_label, "global_filters")]) |
|
21 | -+ | ||
370 | +252x |
-
+ Filter( |
|
22 | -+ | ||
371 | +252x |
- #' @rdname tdata+ function(slice) slice$id %in% module_ids, |
|
23 | -+ | ||
372 | +252x |
- #' @export+ .self$all_slices() |
|
24 | +373 |
- tdata2env <- function(...) {- |
- |
25 | -! | -
- .deprecate_tdata_msg()+ ) |
|
26 | +374 |
- }+ } |
|
27 | +375 |
-
+ }, |
|
28 | +376 |
- #' @rdname tdata+ slices_set = function(slices) { |
|
29 | -+ | ||
377 | +7x |
- #' @export+ shiny::isolate({ |
|
30 | -+ | ||
378 | +7x |
- get_code_tdata <- function(...) {+ if (!is.teal_slices(slices)) { |
|
31 | +379 | ! |
- .deprecate_tdata_msg()- |
-
32 | -- |
- }- |
- |
33 | -- | - - | -|
34 | -- |
- #' @rdname tdata- |
- |
35 | -- |
- #' @export+ slices <- as.teal_slices(slices) |
|
36 | +380 |
- join_keys.tdata <- function(...) {- |
- |
37 | -! | -
- .deprecate_tdata_msg()+ } |
|
38 | -+ | ||
381 | +7x |
- }+ .self$all_slices(slices) |
|
39 | -+ | ||
382 | +7x |
-
+ invisible(.self) |
|
40 | +383 |
- #' @rdname tdata+ }) |
|
41 | +384 |
- #' @export+ }, |
|
42 | +385 |
- get_metadata <- function(...) {+ show = function() { |
|
43 | +386 | ! |
- .deprecate_tdata_msg()- |
-
44 | -- |
- }- |
- |
45 | -- | - - | -|
46 | -- |
- #' @rdname tdata- |
- |
47 | -- |
- #' @export- |
- |
48 | -- |
- as_tdata <- function(...) {+ shiny::isolate(print(.self$all_slices())) |
|
49 | +387 | ! |
- .deprecate_tdata_msg()- |
-
50 | -- |
- }- |
- |
51 | -- | - - | -|
52 | -- |
-
+ invisible(.self) |
|
53 | +388 |
- .deprecate_tdata_msg <- function() {- |
- |
54 | -! | -
- lifecycle::deprecate_stop(- |
- |
55 | -! | -
- when = "0.16",- |
- |
56 | -! | -
- what = "tdata()",- |
- |
57 | -! | -
- details = paste(- |
- |
58 | -! | -
- "tdata has been removed in favour of `teal_data`.\n",- |
- |
59 | -! | -
- "Please follow migration instructions https://github.com/insightsengineering/teal/discussions/987."+ } |
|
60 | +389 |
- )+ ) |
|
61 | +390 |
- )+ ) |
|
62 | +391 |
- }+ # todo: prevent any teal_slices attribute except mapping |
1 |
- #' Calls all `modules`+ #' App state management. |
||
3 |
- #' On the UI side each `teal_modules` is translated to a `tabsetPanel` and each `teal_module` is a+ #' @description |
||
4 |
- #' `tabPanel`. Both, UI and server are called recursively so that each tab is a separate module and+ #' `r lifecycle::badge("experimental")` |
||
5 |
- #' reflect nested structure of `modules` argument.+ #' |
||
6 |
- #'+ #' Capture and restore the global (app) input state. |
||
7 |
- #' @name module_teal_module+ #' |
||
8 |
- #'+ #' @details |
||
9 |
- #' @inheritParams module_teal+ #' This module introduces bookmarks into `teal` apps: the `shiny` bookmarking mechanism becomes enabled |
||
10 |
- #'+ #' and server-side bookmarks can be created. |
||
11 |
- #' @param data_rv (`reactive` returning `teal_data`)+ #' |
||
12 |
- #'+ #' The bookmark manager presents a button with the bookmark icon and is placed in the tab-bar. |
||
13 |
- #' @param slices_global (`reactiveVal` returning `modules_teal_slices`)+ #' When clicked, the button creates a bookmark and opens a modal which displays the bookmark URL. |
||
14 |
- #' see [`module_filter_manager`]+ #' |
||
15 |
- #'+ #' `teal` does not guarantee that all modules (`teal_module` objects) are bookmarkable. |
||
16 |
- #' @param depth (`integer(1)`)+ #' Those that are, have a `teal_bookmarkable` attribute set to `TRUE`. If any modules are not bookmarkable, |
||
17 |
- #' number which helps to determine depth of the modules nesting.+ #' the bookmark manager modal displays a warning and the bookmark button displays a flag. |
||
18 |
- #'+ #' In order to communicate that a external module is bookmarkable, the module developer |
||
19 |
- #' @param datasets (`reactive` returning `FilteredData` or `NULL`)+ #' should set the `teal_bookmarkable` attribute to `TRUE`. |
||
20 |
- #' When `datasets` is passed from the parent module (`srv_teal`) then `dataset` is a singleton+ #' |
||
21 |
- #' which implies in filter-panel to be "global". When `NULL` then filter-panel is "module-specific".+ #' @section Server logic: |
||
22 |
- #'+ #' A bookmark is a URL that contains the app address with a `/?_state_id_=<bookmark_dir>` suffix. |
||
23 |
- #' @return+ #' `<bookmark_dir>` is a directory created on the server, where the state of the application is saved. |
||
24 |
- #' output of currently active module.+ #' Accessing the bookmark URL opens a new session of the app that starts in the previously saved state. |
||
25 |
- #' - `srv_teal_module.teal_module` returns `reactiveVal` containing output of the called module.+ #' |
||
26 |
- #' - `srv_teal_module.teal_modules` returns output of module selected by `input$active_tab`.+ #' @section Note: |
||
27 |
- #'+ #' To enable bookmarking use either: |
||
28 |
- #' @keywords internal+ #' - `shiny` app by using `shinyApp(..., enableBookmarking = "server")` (not supported in `shinytest2`) |
||
29 |
- NULL+ #' - set `options(shiny.bookmarkStore = "server")` before running the app |
||
30 |
-
+ #' |
||
31 |
- #' @rdname module_teal_module+ #' |
||
32 |
- ui_teal_module <- function(id, modules, depth = 0L) {+ #' @inheritParams init |
||
33 | -! | +
- checkmate::assert_multi_class(modules, c("teal_modules", "teal_module", "shiny.tag"))+ #' |
|
34 | -! | +
- checkmate::assert_count(depth)+ #' @return Invisible `NULL`. |
|
35 | -! | +
- UseMethod("ui_teal_module", modules)+ #' |
|
36 |
- }+ #' @aliases bookmark bookmark_manager bookmark_manager_module |
||
37 |
-
+ #' |
||
38 |
- #' @rdname module_teal_module+ #' @name module_bookmark_manager |
||
39 |
- #' @export+ #' @rdname module_bookmark_manager |
||
40 |
- ui_teal_module.default <- function(id, modules, depth = 0L) {+ #' |
||
41 | -! | +
- stop("Modules class not supported: ", paste(class(modules), collapse = " "))+ #' @keywords internal |
|
42 |
- }+ #' |
||
43 |
-
+ NULL |
||
44 |
- #' @rdname module_teal_module+ |
||
45 |
- #' @export+ #' @rdname module_bookmark_manager |
||
46 |
- ui_teal_module.teal_modules <- function(id, modules, depth = 0L) {+ ui_bookmark_panel <- function(id, modules) { |
||
48 | -! | +
- do.call(+ |
|
49 | ! |
- tabsetPanel,+ bookmark_option <- get_bookmarking_option() |
|
50 | ! |
- c(+ is_unbookmarkable <- need_bookmarking(modules) |
|
51 | -+ | ! |
- # by giving an id, we can reactively respond to tab changes+ shinyOptions(bookmarkStore = bookmark_option) |
52 | -! | +
- list(+ |
|
53 | -! | +
- id = ns("active_tab"),+ # Render bookmark warnings count |
|
54 | ! |
- type = if (modules$label == "root") "pills" else "tabs"+ if (!all(is_unbookmarkable) && identical(bookmark_option, "server")) { |
|
55 | -+ | ! |
- ),+ tags$button( |
56 | ! |
- lapply(+ id = ns("do_bookmark"), |
|
57 | ! |
- names(modules$children),+ class = "btn action-button wunder_bar_button bookmark_manager_button", |
|
58 | ! |
- function(module_id) {+ title = "Add bookmark", |
|
59 | ! |
- module_label <- modules$children[[module_id]]$label+ tags$span( |
|
60 | ! |
- if (is.null(module_label)) {+ suppressMessages(icon("fas fa-bookmark")), |
|
61 | ! |
- module_label <- icon("fas fa-database")+ if (any(is_unbookmarkable)) { |
|
62 | -+ | ! |
- }+ tags$span( |
63 | ! |
- tabPanel(+ sum(is_unbookmarkable), |
|
64 | ! |
- title = module_label,+ class = "badge-warning badge-count text-white bg-danger" |
|
65 | -! | +
- value = module_id, # when clicked this tab value changes input$<tabset panel id>+ ) |
|
66 | -! | +
- ui_teal_module(+ } |
|
67 | -! | +
- id = ns(module_id),+ ) |
|
68 | -! | +
- modules = modules$children[[module_id]],+ ) |
|
69 | -! | +
- depth = depth + 1L+ } |
|
70 |
- )+ } |
||
71 |
- )+ |
||
72 |
- }+ #' @rdname module_bookmark_manager |
||
73 |
- )+ srv_bookmark_panel <- function(id, modules) { |
||
74 | -+ | 69x |
- )+ checkmate::assert_character(id) |
75 | -+ | 69x |
- )+ checkmate::assert_class(modules, "teal_modules") |
76 | -+ | 69x |
- }+ moduleServer(id, function(input, output, session) { |
77 | -+ | 69x |
-
+ logger::log_debug("bookmark_manager_srv initializing") |
78 | -+ | 69x |
- #' @rdname module_teal_module+ ns <- session$ns |
79 | -+ | 69x |
- #' @export+ bookmark_option <- get_bookmarking_option() |
80 | -+ | 69x |
- ui_teal_module.shiny.tag <- function(id, modules, depth = 0L) {+ is_unbookmarkable <- need_bookmarking(modules) |
81 | -! | +
- modules+ |
|
82 |
- }+ # Set up bookmarking callbacks ---- |
||
83 |
-
+ # Register bookmark exclusions: do_bookmark button to avoid re-bookmarking |
||
84 | -+ | 69x |
- #' @rdname module_teal_module+ setBookmarkExclude(c("do_bookmark")) |
85 |
- #' @export+ # This bookmark can only be used on the app session. |
||
86 | -+ | 69x |
- ui_teal_module.teal_module <- function(id, modules, depth = 0L) {+ app_session <- .subset2(session, "parent") |
87 | -! | +69x |
- ns <- NS(id)+ app_session$onBookmarked(function(url) { |
88 | ! |
- args <- c(list(id = ns("module")), modules$ui_args)+ logger::log_debug("bookmark_manager_srv@onBookmarked: bookmark button clicked, registering bookmark") |
|
89 | -+ | ! |
-
+ modal_content <- if (bookmark_option != "server") { |
90 | ! |
- ui_teal <- div(+ msg <- sprintf( |
|
91 | ! |
- div(+ "Bookmarking has been set to \"%s\".\n%s\n%s", |
|
92 | ! |
- class = "teal_validated",+ bookmark_option, |
|
93 | ! |
- ui_validate_reactive_teal_data(ns("validate_datanames"))+ "Only server-side bookmarking is supported.", |
|
94 | -+ | ! |
- ),+ "Please contact your app developer." |
95 | -! | +
- do.call(modules$ui, args)+ ) |
|
96 | -+ | ! |
- )+ tags$div( |
97 | -+ | ! |
-
+ tags$p(msg, class = "text-warning") |
98 | -! | +
- div(+ ) |
|
99 | -! | +
- id = id,+ } else { |
|
100 | ! |
- class = "teal_module",+ tags$div( |
|
101 | ! |
- uiOutput(ns("data_reactive"), inline = TRUE),+ tags$span( |
|
102 | ! |
- tagList(+ tags$pre(url) |
|
103 | -! | +
- if (depth >= 2L) tags$div(style = "mt-6"),+ ), |
|
104 | ! |
- if (!is.null(modules$datanames)) {+ if (any(is_unbookmarkable)) { |
|
105 | ! |
- fluidRow(+ bkmb_summary <- rapply2( |
|
106 | ! |
- column(width = 9, ui_teal, class = "teal_primary_col"),+ modules_bookmarkable(modules), |
|
107 | ! |
- column(+ function(x) { |
|
108 | ! |
- width = 3,+ if (isTRUE(x)) { |
|
109 | ! |
- ui_data_summary(ns("data_summary")),+ "\u2705" # check mark |
|
110 | ! |
- ui_filter_data(ns("filter_panel")),+ } else if (isFALSE(x)) { |
|
111 | ! |
- if (length(modules$transformers) > 0 && !isTRUE(attr(modules$transformers, "custom_ui"))) {+ "\u274C" # cross mark |
|
112 | -! | +
- ui_transform_data(ns("data_transform"), transforms = modules$transformers, class = "well")+ } else { |
|
113 | -+ | ! |
- },+ "\u2753" # question mark |
114 | -! | +
- class = "teal_secondary_col"+ } |
|
115 |
- )+ } |
||
116 |
- )+ ) |
||
117 | -+ | ! |
- } else {+ tags$div( |
118 | ! |
- ui_teal+ tags$p( |
|
119 | -+ | ! |
- }+ icon("fas fa-exclamation-triangle"), |
120 | -+ | ! |
- )+ "Some modules will not be restored when using this bookmark.", |
121 | -+ | ! |
- )+ tags$br(), |
122 | -+ | ! |
- }+ "Check the list below to see which modules are not bookmarkable.", |
123 | -+ | ! |
-
+ class = "text-warning" |
124 |
- #' @rdname module_teal_module+ ), |
||
125 | -+ | ! |
- srv_teal_module <- function(id,+ tags$pre(yaml::as.yaml(bkmb_summary)) |
126 |
- data_rv,+ ) |
||
127 |
- modules,+ } |
||
128 |
- datasets = NULL,+ ) |
||
129 |
- slices_global,+ } |
||
130 |
- reporter = teal.reporter::Reporter$new(),+ |
||
131 | -+ | ! |
- is_active = reactive(TRUE)) {+ showModal( |
132 | -165x | +! |
- checkmate::assert_string(id)+ modalDialog( |
133 | -165x | +! |
- assert_reactive(data_rv)+ id = ns("bookmark_modal"), |
134 | -165x | +! |
- checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"))+ title = "Bookmarked teal app url", |
135 | -165x | +! |
- assert_reactive(datasets, null.ok = TRUE)+ modal_content, |
136 | -165x | +! |
- checkmate::assert_class(slices_global, ".slicesGlobal")+ easyClose = TRUE |
137 | -165x | +
- checkmate::assert_class(reporter, "Reporter")+ ) |
|
138 | -165x | +
- UseMethod("srv_teal_module", modules)+ ) |
|
139 |
- }+ }) |
||
141 |
- #' @rdname module_teal_module+ # manually trigger bookmarking because of the problems reported on windows with bookmarkButton in teal |
||
142 | -+ | 69x |
- #' @export+ observeEvent(input$do_bookmark, { |
143 | -+ | ! |
- srv_teal_module.default <- function(id,+ logger::log_debug("bookmark_manager_srv@1 do_bookmark module clicked.") |
144 | -+ | ! |
- data_rv,+ session$doBookmark() |
145 |
- modules,+ }) |
||
146 |
- datasets = NULL,+ |
||
147 | -+ | 69x |
- slices_global,+ invisible(NULL) |
148 |
- reporter = teal.reporter::Reporter$new(),+ }) |
||
149 |
- is_active = reactive(TRUE)) {+ } |
||
150 | -! | +
- stop("Modules class not supported: ", paste(class(modules), collapse = " "))+ |
|
151 |
- }+ |
||
152 |
-
+ #' @rdname module_bookmark_manager |
||
153 |
- #' @rdname module_teal_module+ get_bookmarking_option <- function() { |
||
154 | -+ | 69x |
- #' @export+ bookmark_option <- getShinyOption("bookmarkStore") |
155 | -+ | 69x |
- srv_teal_module.teal_modules <- function(id,+ if (is.null(bookmark_option) && identical(getOption("shiny.bookmarkStore"), "server")) { |
156 | -+ | ! |
- data_rv,+ bookmark_option <- getOption("shiny.bookmarkStore") |
157 |
- modules,+ } |
||
158 | -+ | 69x |
- datasets = NULL,+ bookmark_option |
159 |
- slices_global,+ } |
||
160 |
- reporter = teal.reporter::Reporter$new(),+ |
||
161 |
- is_active = reactive(TRUE)) {+ #' @rdname module_bookmark_manager |
||
162 | -70x | +
- moduleServer(id = id, module = function(input, output, session) {+ need_bookmarking <- function(modules) { |
|
163 | -70x | +69x |
- logger::log_debug("srv_teal_module.teal_modules initializing the module { deparse1(modules$label) }.")+ unlist(rapply2( |
164 | -+ | 69x |
-
+ modules_bookmarkable(modules), |
165 | -70x | +69x |
- modules_output <- sapply(+ Negate(isTRUE) |
166 | -70x | +
- names(modules$children),+ )) |
|
167 | -70x | +
- function(module_id) {+ } |
|
168 | -95x | +
- srv_teal_module(+ |
|
169 | -95x | +
- id = module_id,+ |
|
170 | -95x | +
- data_rv = data_rv,+ # utilities ---- |
|
171 | -95x | +
- modules = modules$children[[module_id]],+ |
|
172 | -95x | +
- datasets = datasets,+ #' Restore value from bookmark. |
|
173 | -95x | +
- slices_global = slices_global,+ #' |
|
174 | -95x | +
- reporter = reporter,+ #' Get value from bookmark or return default. |
|
175 | -95x | +
- is_active = reactive(is_active() && input$active_tab == module_id)+ #' |
|
176 |
- )+ #' Bookmarks can store not only inputs but also arbitrary values. |
||
177 |
- },+ #' These values are stored by `onBookmark` callbacks and restored by `onBookmarked` callbacks, |
||
178 | -70x | +
- simplify = FALSE+ #' and they are placed in the `values` environment in the `session$restoreContext` field. |
|
179 |
- )+ #' Using `teal_data_module` makes it impossible to run the callbacks |
||
180 |
-
+ #' because the app becomes ready before modules execute and callbacks are registered. |
||
181 | -69x | +
- modules_output+ #' In those cases the stored values can still be recovered from the `session` object directly. |
|
182 |
- })+ #' |
||
183 |
- }+ #' Note that variable names in the `values` environment are prefixed with module name space names, |
||
184 |
-
+ #' therefore, when using this function in modules, `value` must be run through the name space function. |
||
185 |
- #' @rdname module_teal_module+ #' |
||
186 |
- #' @export+ #' @param value (`character(1)`) name of value to restore |
||
187 |
- srv_teal_module.teal_module <- function(id,+ #' @param default fallback value |
||
188 |
- data_rv,+ #' |
||
189 |
- modules,+ #' @return |
||
190 |
- datasets = NULL,+ #' In an application restored from a server-side bookmark, |
||
191 |
- slices_global,+ #' the variable specified by `value` from the `values` environment. |
||
192 |
- reporter = teal.reporter::Reporter$new(),+ #' Otherwise `default`. |
||
193 |
- is_active = reactive(TRUE)) {+ #' |
||
194 | -95x | +
- logger::log_debug("srv_teal_module.teal_module initializing the module: { deparse1(modules$label) }.")+ #' @keywords internal |
|
195 | -95x | +
- moduleServer(id = id, module = function(input, output, session) {+ #' |
|
196 | -95x | +
- active_datanames <- reactive(.resolve_module_datanames(data = data_rv(), modules = modules))+ restoreValue <- function(value, default) { # nolint: object_name. |
|
197 | -95x | +138x |
- if (is.null(datasets)) {+ checkmate::assert_character("value") |
198 | -18x | +138x |
- datasets <- eventReactive(data_rv(), {+ session_default <- shiny::getDefaultReactiveDomain() |
199 | -14x | +138x |
- if (!inherits(data_rv(), "teal_data")) {+ session_parent <- .subset2(session_default, "parent") |
200 | -! | +138x |
- stop("data_rv must be teal_data object.")+ session <- if (is.null(session_parent)) session_default else session_parent |
201 |
- }+ |
||
202 | -14x | +138x |
- logger::log_debug("srv_teal_module@1 initializing module-specific FilteredData")+ if (isTRUE(session$restoreContext$active) && exists(value, session$restoreContext$values, inherits = FALSE)) { |
203 | -+ | ! |
-
+ session$restoreContext$values[[value]] |
204 | -14x | +
- teal_data_to_filtered_data(data_rv(), datanames = active_datanames())+ } else { |
|
205 | -+ | 138x |
- })+ default |
206 |
- }+ } |
||
207 |
-
+ } |
||
208 |
- # manage module filters on the module level+ |
||
209 |
- # important:+ #' Compare bookmarks. |
||
210 |
- # filter_manager_module_srv needs to be called before filter_panel_srv+ #' |
||
211 |
- # Because available_teal_slices is used in FilteredData$srv_available_slices (via srv_filter_panel)+ #' Test if two bookmarks store identical state. |
||
212 |
- # and if it is not set, then it won't be available in the srv_filter_panel+ #' |
||
213 | -95x | +
- srv_module_filter_manager(modules$label, module_fd = datasets, slices_global = slices_global)+ #' `input` environments are compared one variable at a time and if not identical, |
|
214 | -95x | +
- filtered_teal_data <- srv_filter_data(+ #' values in both bookmarks are reported. States of `datatable`s are stripped |
|
215 | -95x | +
- "filter_panel",+ #' of the `time` element before comparing because the time stamp is always different. |
|
216 | -95x | +
- datasets = datasets,+ #' The contents themselves are not printed as they are large and the contents are not informative. |
|
217 | -95x | +
- active_datanames = active_datanames,+ #' Elements present in one bookmark and absent in the other are also reported. |
|
218 | -95x | +
- data_rv = data_rv,+ #' Differences are printed as messages. |
|
219 | -95x | +
- is_active = is_active+ #' |
|
220 |
- )+ #' `values` environments are compared with `all.equal`. |
||
221 |
-
+ #' |
||
222 | -95x | +
- transformed_teal_data <- srv_transform_data(+ #' @section How to use: |
|
223 | -95x | +
- "data_transform",+ #' Open an application, change relevant inputs (typically, all of them), and create a bookmark. |
|
224 | -95x | +
- data = filtered_teal_data,+ #' Then open that bookmark and immediately create a bookmark of that. |
|
225 | -95x | +
- transforms = modules$transformers,+ #' If restoring bookmarks occurred properly, the two bookmarks should store the same state. |
|
226 | -95x | +
- modules = modules+ #' |
|
227 |
- )+ #' |
||
228 |
-
+ #' @param book1,book2 bookmark directories stored in `shiny_bookmarks/`; |
||
229 | -94x | +
- module_teal_data <- reactive({+ #' default to the two most recently modified directories |
|
230 | -103x | +
- all_teal_data <- transformed_teal_data()+ #' |
|
231 | -78x | +
- module_datanames <- .resolve_module_datanames(data = all_teal_data, modules = modules)+ #' @return |
|
232 | -78x | +
- .subset_teal_data(all_teal_data, module_datanames)+ #' Invisible `NULL` if bookmarks are identical or if there are no bookmarks to test. |
|
233 |
- })+ #' `FALSE` if inconsistencies are detected. |
||
234 |
-
+ #' |
||
235 | -94x | +
- module_teal_data_validated <- srv_validate_reactive_teal_data(+ #' @keywords internal |
|
236 | -94x | +
- "validate_datanames",+ #' |
|
237 | -94x | +
- data = module_teal_data,+ bookmarks_identical <- function(book1, book2) { |
|
238 | -94x | +! |
- modules = modules+ if (!dir.exists("shiny_bookmarks")) { |
239 | -+ | ! |
- )+ message("no bookmark directory") |
240 | -+ | ! |
-
+ return(invisible(NULL)) |
241 | -94x | +
- summary_table <- srv_data_summary("data_summary", module_teal_data)+ } |
|
243 | -+ | ! |
- # Call modules.+ ans <- TRUE |
244 | -94x | +
- module_out <- reactiveVal(NULL)+ |
|
245 | -94x | +! |
- if (!inherits(modules, "teal_module_previewer")) {+ if (missing(book1) && missing(book2)) { |
246 | -94x | +! |
- obs_module <- observeEvent(+ dirs <- list.dirs("shiny_bookmarks", recursive = FALSE) |
247 | -+ | ! |
- # wait for module_teal_data() to be not NULL but only once:+ bookmarks_sorted <- basename(rev(dirs[order(file.mtime(dirs))])) |
248 | -94x | +! |
- ignoreNULL = TRUE,+ if (length(bookmarks_sorted) < 2L) { |
249 | -94x | +! |
- once = TRUE,+ message("no bookmarks to compare") |
250 | -94x | +! |
- eventExpr = module_teal_data_validated(),+ return(invisible(NULL)) |
251 | -94x | +
- handlerExpr = {+ } |
|
252 | -68x | +! |
- module_out(.call_teal_module(modules, datasets, module_teal_data_validated, reporter))+ book1 <- bookmarks_sorted[2L] |
253 | -+ | ! |
- }+ book2 <- bookmarks_sorted[1L] |
254 |
- )+ } else { |
||
255 | -+ | ! |
- } else {+ if (!dir.exists(file.path("shiny_bookmarks", book1))) stop(book1, " not found") |
256 | -+ | ! |
- # Report previewer must be initiated on app start for report cards to be included in bookmarks.+ if (!dir.exists(file.path("shiny_bookmarks", book2))) stop(book2, " not found") |
257 |
- # When previewer is delayed, cards are bookmarked only if previewer has been initiated (visited).+ } |
||
258 | -! | +
- module_out(.call_teal_module(modules, datasets, module_teal_data, reporter))+ |
|
259 | -+ | ! |
- }+ book1_input <- readRDS(file.path("shiny_bookmarks", book1, "input.rds")) |
260 | -+ | ! |
-
+ book2_input <- readRDS(file.path("shiny_bookmarks", book2, "input.rds")) |
261 |
- # todo: (feature request) add a ReporterCard to the reporter as an output from the teal_module+ |
||
262 | -+ | ! |
- # how to determine if module returns a ReporterCard so that reportPreviewer is needed?+ elements_common <- intersect(names(book1_input), names(book2_input)) |
263 | -+ | ! |
- # Should we insertUI of the ReportPreviewer then?+ dt_states <- grepl("_state$", elements_common) |
264 | -+ | ! |
- # What about attr(module, "reportable") - similar to attr(module, "bookmarkable")+ if (any(dt_states)) { |
265 | -94x | +! |
- if ("report" %in% names(module_out)) {+ for (el in elements_common[dt_states]) { |
266 | -+ | ! |
- # (reactively) add card to the reporter+ book1_input[[el]][["time"]] <- NULL |
267 | -+ | ! |
- }+ book2_input[[el]][["time"]] <- NULL |
268 |
-
+ } |
||
269 | -94x | +
- module_out+ } |
|
270 |
- })+ |
||
271 | -+ | ! |
- }+ identicals <- mapply(identical, book1_input[elements_common], book2_input[elements_common]) |
272 | -+ | ! |
-
+ non_identicals <- names(identicals[!identicals]) |
273 | -+ | ! |
- # This function calls a module server function.+ compares <- sprintf("$ %s:\t%s --- %s", non_identicals, book1_input[non_identicals], book2_input[non_identicals]) |
274 | -+ | ! |
- .call_teal_module <- function(modules, datasets, filtered_teal_data, reporter) {+ if (length(compares) != 0L) { |
275 | -+ | ! |
- # collect arguments to run teal_module+ message("common elements not identical: \n", paste(compares, collapse = "\n")) |
276 | -68x | +! |
- args <- c(list(id = "module"), modules$server_args)+ ans <- FALSE |
277 | -68x | +
- if (is_arg_used(modules$server, "reporter")) {+ } |
|
278 | -1x | +
- args <- c(args, list(reporter = reporter))+ |
|
279 | -+ | ! |
- }+ elements_boook1 <- setdiff(names(book1_input), names(book2_input)) |
280 | -+ | ! |
-
+ if (length(elements_boook1) != 0L) { |
281 | -68x | +! |
- if (is_arg_used(modules$server, "datasets")) {+ dt_states <- grepl("_state$", elements_boook1) |
282 | -1x | +! |
- args <- c(args, datasets = datasets())+ if (any(dt_states)) { |
283 | -1x | +! |
- warning("datasets argument is not reactive and therefore it won't be updated when data is refreshed.")+ for (el in elements_boook1[dt_states]) { |
284 | -+ | ! |
- }+ if (is.list(book1_input[[el]])) book1_input[[el]] <- "--- data table state ---" |
285 |
-
+ } |
||
286 | -68x | +
- if (is_arg_used(modules$server, "data")) {+ } |
|
287 | -64x | +! |
- args <- c(args, data = list(filtered_teal_data))+ excess1 <- sprintf("$ %s:\t%s", elements_boook1, book1_input[elements_boook1]) |
288 | -+ | ! |
- }+ message("elements only in book1: \n", paste(excess1, collapse = "\n")) |
289 | -+ | ! |
-
+ ans <- FALSE |
290 | -68x | +
- if (is_arg_used(modules$server, "filter_panel_api")) {+ } |
|
291 | -1x | +
- args <- c(args, filter_panel_api = teal.slice::FilterPanelAPI$new(datasets()))+ |
|
292 | -+ | ! |
- }+ elements_boook2 <- setdiff(names(book2_input), names(book1_input)) |
293 | -+ | ! |
-
+ if (length(elements_boook2) != 0L) { |
294 | -68x | +! |
- if (is_arg_used(modules$server, "id")) {+ dt_states <- grepl("_state$", elements_boook1) |
295 | -68x | +! |
- do.call(modules$server, args)+ if (any(dt_states)) { |
296 | -+ | ! |
- } else {+ for (el in elements_boook1[dt_states]) { |
297 | ! |
- do.call(callModule, c(args, list(module = modules$server)))+ if (is.list(book2_input[[el]])) book2_input[[el]] <- "--- data table state ---" |
|
298 |
- }+ } |
||
299 |
- }+ } |
||
300 | -+ | ! |
-
+ excess2 <- sprintf("$ %s:\t%s", elements_boook2, book2_input[elements_boook2]) |
301 | -+ | ! |
- .resolve_module_datanames <- function(data, modules) {+ message("elements only in book2: \n", paste(excess2, collapse = "\n")) |
302 | -153x | +! |
- stopifnot("data_rv must be teal_data object." = inherits(data, "teal_data"))+ ans <- FALSE |
303 | -153x | +
- if (is.null(modules$datanames) || identical(modules$datanames, "all")) {+ } |
|
304 | -131x | +
- .topologically_sort_datanames(ls(teal.code::get_env(data)), teal.data::join_keys(data))+ |
|
305 | -+ | ! |
- } else {+ book1_values <- readRDS(file.path("shiny_bookmarks", book1, "values.rds")) |
306 | -22x | +! |
- intersect(+ book2_values <- readRDS(file.path("shiny_bookmarks", book2, "values.rds")) |
307 | -22x | +
- .include_parent_datanames(modules$datanames, teal.data::join_keys(data)),+ |
|
308 | -22x | +! |
- ls(teal.code::get_env(data))+ if (!isTRUE(all.equal(book1_values, book2_values))) { |
309 | +! | +
+ message("different values detected")+ |
+ |
310 | +! | +
+ message("choices for numeric filters MAY be different, see RangeFilterState$set_choices")+ |
+ |
311 | +! | +
+ ans <- FALSE+ |
+ |
312 |
- )+ } |
||
310 | +313 | ++ | + + | +
314 | +! | +
+ if (ans) message("perfect!")+ |
+ |
315 | +! | +
+ invisible(NULL)+ |
+ |
316 | ++ |
+ }+ |
+ |
317 | ++ | + + | +|
318 | ++ | + + | +|
319 | ++ |
+ # Replacement for [base::rapply] which doesn't handle NULL values - skips the evaluation+ |
+ |
320 | ++ |
+ # of the function and returns NULL for given element.+ |
+ |
321 | ++ |
+ rapply2 <- function(x, f) {+ |
+ |
322 | +163x | +
+ if (inherits(x, "list")) {+ |
+ |
323 | +69x | +
+ lapply(x, rapply2, f = f)+ |
+ |
324 | ++ |
+ } else {+ |
+ |
325 | +94x | +
+ f(x)+ |
+ |
326 |
} |
||
311 | +327 |
}@@ -14624,14 +14232,14 @@ teal coverage - 57.64% |
1 |
- #' Manage multiple `FilteredData` objects+ #' Generate lockfile for application's environment reproducibility |
||
3 |
- #' @description+ #' @param lockfile_path (`character`) path to the lockfile. |
||
4 |
- #' Oversee filter states across the entire application.+ #' |
||
5 |
- #'+ #' @section Different ways of creating lockfile: |
||
6 |
- #' @section Slices global:+ #' `teal` leverages [renv::snapshot()], which offers multiple methods for lockfile creation. |
||
7 |
- #' The key role in maintaining the module-specific filter states is played by the `.slicesGlobal`+ #' |
||
8 |
- #' object. It is a reference class that holds the following fields:+ #' - **Working directory lockfile**: `teal`, by default, will create an `implicit` type lockfile that uses |
||
9 |
- #' - `all_slices` (`reactiveVal`) - reactive value containing all filters registered in an app.+ #' `renv::dependencies()` to detect all R packages in the current project's working directory. |
||
10 |
- #' - `module_slices_api` (`reactiveValues`) - reactive field containing references to each modules'+ #' - **`DESCRIPTION`-based lockfile**: To generate a lockfile based on a `DESCRIPTION` file in your working |
||
11 |
- #' `FilteredData` object methods. At this moment it is used only in `srv_filter_manager` to display+ #' directory, set `renv::settings$snapshot.type("explicit")`. The naming convention for `type` follows |
||
12 |
- #' the filter states in a table combining informations from `all_slices` and from+ #' `renv::snapshot()`. For the `"explicit"` type, refer to `renv::settings$package.dependency.fields()` for the |
||
13 |
- #' `FilteredData$get_available_teal_slices()`.+ #' `DESCRIPTION` fields included in the lockfile. |
||
14 |
- #'+ #' - **Custom files-based lockfile**: To specify custom files as the basis for the lockfile, set |
||
15 |
- #' During a session only new filters are added to `all_slices` unless [`module_snapshot_manager`] is+ #' `renv::settings$snapshot.type("custom")` and configure the `renv.snapshot.filter` option. |
||
16 |
- #' used to restore previous state. Filters from `all_slices` can be activated or deactivated in a+ #' |
||
17 |
- #' module which is linked (both ways) by `attr(, "mapping")` so that:+ #' @section lockfile usage: |
||
18 |
- #' - If module's filter is added or removed in its `FilteredData` object, this information is passed+ #' After creating the lockfile, you can restore the application's environment using `renv::restore()`. |
||
19 |
- #' to `SlicesGlobal` which updates `attr(, "mapping")` accordingly.+ #' |
||
20 |
- #' - When mapping changes in a `SlicesGlobal`, filters are set or removed from module's+ #' @seealso [renv::snapshot()], [renv::restore()]. |
||
21 |
- #' `FilteredData`.+ #' |
||
22 |
- #'+ #' @return `NULL` |
||
23 |
- #' @section Filter manager:+ #' |
||
24 |
- #' Filter-manager is split into two parts:+ #' @name module_teal_lockfile |
||
25 |
- #' 1. `ui/srv_filter_manager_panel` - Called once for the whole app. This module observes changes in+ #' @rdname module_teal_lockfile |
||
26 |
- #' the filters in `slices_global` and displays them in a table utilizing information from `mapping`:+ #' |
||
27 |
- #' - (`TRUE`) - filter is active in the module+ #' @keywords internal |
||
28 |
- #' - (`FALSE`) - filter is inactive in the module+ NULL |
||
29 |
- #' - (`NA`) - filter is not available in the module+ |
||
30 |
- #' 2. `ui/srv_module_filter_manager` - Called once for each `teal_module`. Handling filter states+ #' @rdname module_teal_lockfile |
||
31 |
- #' for of single module and keeping module `FilteredData` consistent with `slices_global`, so that+ ui_teal_lockfile <- function(id) { |
||
32 | -+ | ! |
- #' local filters are always reflected in the `slices_global` and its mapping and vice versa.+ ns <- NS(id) |
33 | -+ | ! |
- #'+ shiny::tagList( |
34 | -+ | ! |
- #'+ tags$span("", id = ns("lockFileStatus")), |
35 | -+ | ! |
- #' @param id (`character(1)`)+ shinyjs::disabled(downloadLink(ns("lockFileLink"), "Download lockfile")) |
36 |
- #' `shiny` module instance id.+ ) |
||
37 |
- #'+ } |
||
38 |
- #' @param slices_global (`reactiveVal`)+ |
||
39 |
- #' containing `teal_slices`.+ #' @rdname module_teal_lockfile |
||
40 |
- #'+ srv_teal_lockfile <- function(id) { |
||
41 | -+ | 71x |
- #' @param module_fd (`FilteredData`)+ moduleServer(id, function(input, output, session) { |
42 | -+ | 71x |
- #' Object containing the data to be filtered in a single `teal` module.+ logger::log_debug("Initialize srv_teal_lockfile.") |
43 | -+ | 71x |
- #'+ enable_lockfile_download <- function() { |
44 | -+ | ! |
- #' @return+ shinyjs::html("lockFileStatus", "Application lockfile ready.") |
45 | -+ | ! |
- #' Module returns a `slices_global` (`reactiveVal`) containing a `teal_slices` object with mapping.+ shinyjs::hide("lockFileStatus", anim = TRUE) |
46 | -+ | ! |
- #'+ shinyjs::enable("lockFileLink") |
47 | -+ | ! |
- #' @encoding UTF-8+ output$lockFileLink <- shiny::downloadHandler( |
48 | -+ | ! |
- #'+ filename = function() { |
49 | -+ | ! |
- #' @name module_filter_manager+ "renv.lock" |
50 |
- #' @rdname module_filter_manager+ }, |
||
51 | -+ | ! |
- #'+ content = function(file) { |
52 | -+ | ! |
- NULL+ file.copy(lockfile_path, file) |
53 | -+ | ! |
-
+ file |
54 |
- #' @rdname module_filter_manager+ }, |
||
55 | -+ | ! |
- ui_filter_manager_panel <- function(id) {+ contentType = "application/json" |
56 | -! | +
- ns <- NS(id)+ ) |
|
57 | -! | +
- tags$button(+ } |
|
58 | -! | +71x |
- id = ns("show_filter_manager"),+ disable_lockfile_download <- function() { |
59 | ! |
- class = "btn action-button wunder_bar_button",+ warning("Lockfile creation failed.", call. = FALSE) |
|
60 | ! |
- title = "View filter mapping",+ shinyjs::html("lockFileStatus", "Lockfile creation failed.") |
|
61 | ! |
- suppressMessages(icon("fas fa-grip"))+ shinyjs::hide("lockFileLink") |
|
62 |
- )+ } |
||
63 |
- }+ |
||
64 | -+ | 71x |
-
+ shiny::onStop(function() { |
65 | -+ | 71x |
- #' @rdname module_filter_manager+ if (file.exists(lockfile_path) && !shiny::isRunning()) { |
66 | -+ | 1x |
- #' @keywords internal+ logger::log_debug("Removing lockfile after shutting down the app") |
67 | -+ | 1x |
- srv_filter_manager_panel <- function(id, slices_global) {+ file.remove(lockfile_path) |
68 | -69x | +
- checkmate::assert_string(id)+ } |
|
69 | -69x | +
- checkmate::assert_class(slices_global, ".slicesGlobal")+ }) |
|
70 | -69x | +
- moduleServer(id, function(input, output, session) {+ |
|
71 | -69x | +71x |
- setBookmarkExclude(c("show_filter_manager"))+ lockfile_path <- "teal_app.lock" |
72 | -69x | +71x |
- observeEvent(input$show_filter_manager, {+ mode <- getOption("teal.lockfile.mode", default = "") |
73 | -! | +
- logger::log_debug("srv_filter_manager_panel@1 show_filter_manager button has been clicked.")+ |
|
74 | -! | +71x |
- showModal(+ if (!(mode %in% c("auto", "enabled", "disabled"))) { |
75 | ! |
- modalDialog(+ stop("'teal.lockfile.mode' option can only be one of \"auto\", \"disabled\" or \"disabled\". ") |
|
76 | -! | +
- ui_filter_manager(session$ns("filter_manager")),+ } |
|
77 | -! | +
- class = "filter_manager_modal",+ |
|
78 | -! | +71x |
- size = "l",+ if (mode == "disabled") { |
79 | -! | +1x |
- footer = NULL,+ logger::log_debug("'teal.lockfile.mode' option is set to 'disabled'. Hiding lockfile download button.") |
80 | -! | +1x |
- easyClose = TRUE+ shinyjs::hide("lockFileLink") |
81 | -+ | 1x |
- )+ return(NULL) |
82 |
- )+ } |
||
83 |
- })+ |
||
84 | -69x | +70x |
- srv_filter_manager("filter_manager", slices_global = slices_global)+ if (file.exists(lockfile_path)) { |
85 | -+ | ! |
- })+ logger::log_debug("Lockfile has already been created for this app - skipping automatic creation.") |
86 | -+ | ! |
- }+ enable_lockfile_download() |
87 | -+ | ! |
-
+ return(NULL) |
88 |
- #' @rdname module_filter_manager+ } |
||
89 |
- ui_filter_manager <- function(id) {+ |
||
90 | -! | +70x |
- ns <- NS(id)+ if (mode == "auto" && .is_disabled_lockfile_scenario()) { |
91 | -! | +69x |
- actionButton(ns("filter_manager"), NULL, icon = icon("fas fa-filter"))+ logger::log_debug( |
92 | -! | +69x |
- tags$div(+ "Automatic lockfile creation disabled. Execution scenario satisfies teal:::.is_disabled_lockfile_scenario()." |
93 | -! | +
- class = "filter_manager_content",+ ) |
|
94 | -! | +69x |
- tableOutput(ns("slices_table"))+ shinyjs::hide("lockFileLink") |
95 | -+ | 69x |
- )+ return(NULL) |
96 |
- }+ } |
||
98 | -+ | 1x |
- #' @rdname module_filter_manager+ if (!.is_lockfile_deps_installed()) { |
99 | -+ | ! |
- srv_filter_manager <- function(id, slices_global) {+ warning("Automatic lockfile creation disabled. `mirai` and `renv` packages must be installed.") |
100 | -69x | +! |
- checkmate::assert_string(id)+ shinyjs::hide("lockFileLink") |
101 | -69x | +! |
- checkmate::assert_class(slices_global, ".slicesGlobal")+ return(NULL) |
102 |
-
+ } |
||
103 | -69x | +
- moduleServer(id, function(input, output, session) {+ |
|
104 | -69x | +
- logger::log_debug("filter_manager_srv initializing.")+ # - Will be run only if the lockfile doesn't exist (see the if-s above) |
|
105 |
-
+ # - We render to the tempfile because the process might last after session is closed and we don't |
||
106 |
- # Bookmark slices global with mapping.+ # want to make a "teal_app.renv" then. This is why we copy only during active session. |
||
107 | -69x | +1x |
- session$onBookmark(function(state) {+ process <- .teal_lockfile_process_invoke(lockfile_path) |
108 | -! | +1x |
- logger::log_debug("filter_manager_srv@onBookmark: storing filter state")+ observeEvent(process$status(), { |
109 | ! |
- state$values$filter_state_on_bookmark <- as.list(+ if (process$status() %in% c("initial", "running")) { |
|
110 | ! |
- slices_global$all_slices(),+ shinyjs::html("lockFileStatus", "Creating lockfile...") |
|
111 | ! |
- recursive = TRUE+ } else if (process$status() == "success") { |
|
112 | -+ | ! |
- )+ result <- process$result() |
113 | -+ | ! |
- })+ if (any(grepl("Lockfile written to", result$out))) { |
114 | -+ | ! |
-
+ logger::log_debug("Lockfile containing { length(result$res$Packages) } packages created.") |
115 | -69x | +! |
- bookmarked_slices <- restoreValue(session$ns("filter_state_on_bookmark"), NULL)+ if (any(grepl("(WARNING|ERROR):", result$out))) { |
116 | -69x | +! |
- if (!is.null(bookmarked_slices)) {+ warning("Lockfile created with warning(s) or error(s):", call. = FALSE) |
117 | ! |
- logger::log_debug("filter_manager_srv: restoring filter state from bookmark.")+ for (i in result$out) { |
|
118 | ! |
- slices_global$slices_set(bookmarked_slices)+ warning(i, call. = FALSE) |
|
119 |
- }+ } |
||
120 |
-
+ } |
||
121 | -69x | +! |
- mapping_table <- reactive({+ enable_lockfile_download() |
122 |
- # We want this to be reactive on slices_global$all_slices() only as get_available_teal_slices()+ } else { |
||
123 | -+ | ! |
- # is dependent on slices_global$all_slices().+ disable_lockfile_download() |
124 | -77x | +
- module_labels <- setdiff(+ } |
|
125 | -77x | +! |
- names(attr(slices_global$all_slices(), "mapping")),+ } else if (process$status() == "error") { |
126 | -77x | +! |
- "Report previewer"+ disable_lockfile_download() |
127 |
- )+ } |
||
128 | -77x | +
- isolate({+ }) |
|
129 | -77x | +
- mm <- as.data.frame(+ |
|
130 | -77x | +1x |
- sapply(+ NULL |
131 | -77x | +
- module_labels,+ }) |
|
132 | -77x | +
- simplify = FALSE,+ } |
|
133 | -77x | +
- function(module_label) {+ |
|
134 | -90x | +
- available_slices <- slices_global$module_slices_api[[module_label]]$get_available_teal_slices()+ utils::globalVariables(c("opts", "sysenv", "libpaths", "wd", "lockfilepath", "run")) # needed for mirai call |
|
135 | -83x | +
- global_ids <- sapply(slices_global$all_slices(), `[[`, "id", simplify = FALSE)+ #' @rdname module_teal_lockfile |
|
136 | -83x | +
- module_ids <- sapply(slices_global$slices_get(module_label), `[[`, "id", simplify = FALSE)+ .teal_lockfile_process_invoke <- function(lockfile_path) { |
|
137 | -83x | +1x |
- allowed_ids <- vapply(available_slices, `[[`, character(1L), "id")+ mirai_obj <- NULL |
138 | -83x | +1x |
- active_ids <- global_ids %in% module_ids+ process <- shiny::ExtendedTask$new(function() { |
139 | -83x | +1x |
- setNames(nm = global_ids, ifelse(global_ids %in% allowed_ids, active_ids, NA))+ m <- mirai::mirai( |
140 |
- }+ { |
||
141 | -+ | 1x |
- ),+ options(opts) |
142 | -77x | +1x |
- check.names = FALSE+ do.call(Sys.setenv, sysenv) |
143 | -+ | 1x |
- )+ .libPaths(libpaths) |
144 | -70x | +1x |
- colnames(mm)[colnames(mm) == "global_filters"] <- "Global filters"+ setwd(wd) |
145 | -+ | 1x |
-
+ run(lockfile_path = lockfile_path) |
146 | -70x | +
- mm+ }, |
|
147 | -+ | 1x |
- })+ run = .renv_snapshot, |
148 | -+ | 1x |
- })+ lockfile_path = lockfile_path, |
149 | -+ | 1x |
-
+ opts = options(), |
150 | -69x | +1x |
- output$slices_table <- renderTable(+ libpaths = .libPaths(), |
151 | -69x | +1x |
- expr = {+ sysenv = as.list(Sys.getenv()), |
152 | -77x | +1x |
- logger::log_debug("filter_manager_srv@1 rendering slices_table.")+ wd = getwd() |
153 | -77x | +
- mm <- mapping_table()+ ) |
|
154 | -+ | 1x |
-
+ mirai_obj <<- m |
155 | -+ | 1x |
- # Display logical values as UTF characters.+ m |
156 | -70x | +
- mm[] <- lapply(mm, ifelse, yes = intToUtf8(9989), no = intToUtf8(10060))+ }) |
|
157 | -70x | +
- mm[] <- lapply(mm, function(x) ifelse(is.na(x), intToUtf8(128306), x))+ |
|
158 | -+ | 1x |
-
+ shiny::onStop(function() { |
159 | -+ | 1x |
- # Display placeholder if no filters defined.+ if (mirai::unresolved(mirai_obj)) { |
160 | -70x | +! |
- if (nrow(mm) == 0L) {+ logger::log_debug("Terminating a running lockfile process...") |
161 | -46x | +! |
- mm <- data.frame(`Filter manager` = "No filters specified.", check.names = FALSE)+ mirai::stop_mirai(mirai_obj) # this doesn't stop running - renv will be created even if session is closed |
162 | -46x | +
- rownames(mm) <- ""+ } |
|
163 |
- }+ }) |
||
164 | -70x | +
- mm+ |
|
165 | -+ | 1x |
- },+ suppressWarnings({ # 'package:stats' may not be available when loading |
166 | -69x | +1x |
- rownames = TRUE+ process$invoke() |
167 |
- )+ }) |
||
169 | -69x | +1x |
- mapping_table # for testing purpose+ logger::log_debug("Lockfile creation started based on { getwd() }.") |
170 |
- })+ |
||
171 | -+ | 1x |
- }+ process |
172 |
-
+ } |
||
173 |
- #' @rdname module_filter_manager+ |
||
174 |
- srv_module_filter_manager <- function(id, module_fd, slices_global) {+ #' @rdname module_teal_lockfile |
||
175 | -95x | +
- checkmate::assert_string(id)+ .renv_snapshot <- function(lockfile_path) { |
|
176 | -95x | +1x |
- assert_reactive(module_fd)+ out <- utils::capture.output( |
177 | -95x | +1x |
- checkmate::assert_class(slices_global, ".slicesGlobal")+ res <- renv::snapshot( |
178 | -+ | 1x |
-
+ lockfile = lockfile_path, |
179 | -95x | +1x |
- moduleServer(id, function(input, output, session) {+ prompt = FALSE, |
180 | -95x | +1x |
- logger::log_debug("srv_module_filter_manager initializing for module: { id }.")+ force = TRUE, |
181 | -+ | 1x |
- # Track filter global and local states.+ type = renv::settings$snapshot.type() # see the section "Different ways of creating lockfile" above here |
182 | -95x | +
- slices_global_module <- reactive({+ ) |
|
183 | -169x | +
- slices_global$slices_get(module_label = id)+ ) |
|
184 |
- })+ |
||
185 | -95x | +1x |
- slices_module <- reactive(req(module_fd())$get_filter_state())+ list(out = out, res = res) |
186 |
-
+ } |
||
187 | -95x | +
- module_fd_previous <- reactiveVal(NULL)+ |
|
188 |
-
+ #' @rdname module_teal_lockfile |
||
189 |
- # Set (reactively) available filters for the module.+ .is_lockfile_deps_installed <- function() { |
||
190 | -95x | +1x |
- obs1 <- observeEvent(module_fd(), priority = 1, {+ requireNamespace("mirai", quietly = TRUE) && requireNamespace("renv", quietly = TRUE) |
191 | -75x | +
- logger::log_debug("srv_module_filter_manager@1 setting initial slices for module: { id }.")+ } |
|
192 |
- # Filters relevant for the module in module-specific app.+ |
||
193 | -75x | +
- slices <- slices_global_module()+ #' @rdname module_teal_lockfile |
|
194 |
-
+ .is_disabled_lockfile_scenario <- function() { |
||
195 | -+ | 69x |
- # Clean up previous filter states and refresh cache of previous module_fd with current+ identical(Sys.getenv("CALLR_IS_RUNNING"), "true") || # inside callr process |
196 | -3x | +69x |
- if (!is.null(module_fd_previous())) module_fd_previous()$finalize()+ identical(Sys.getenv("TESTTHAT"), "true") || # inside devtools::test |
197 | -75x | +69x |
- module_fd_previous(module_fd())+ !identical(Sys.getenv("QUARTO_PROJECT_ROOT"), "") || # inside Quarto process |
198 |
-
+ ( |
||
199 | -+ | 69x |
- # Setting filter states from slices_global:+ ("CheckExEnv" %in% search()) || any(c("_R_CHECK_TIMINGS_", "_R_CHECK_LICENSE_") %in% names(Sys.getenv())) |
200 | -+ | 69x |
- # 1. when app initializes slices_global set to initial filters (specified by app developer)+ ) # inside R CMD CHECK |
201 |
- # 2. when data reinitializes slices_global reflects latest filter states- |
- ||
202 | -- | - - | -|
203 | -75x | -
- module_fd()$set_filter_state(slices)+ } |
204 | +1 |
-
+ #' Filter state snapshot management |
||
205 | +2 |
- # irrelevant filters are discarded in FilteredData$set_available_teal_slices+ #' |
||
206 | +3 |
- # it means we don't need to subset slices_global$all_slices() from filters refering to irrelevant datasets+ #' Capture and restore snapshots of the global (app) filter state. |
||
207 | -75x | +|||
4 | +
- module_fd()$set_available_teal_slices(slices_global$all_slices)+ #' |
|||
208 | +5 |
-
+ #' This module introduces snapshots: stored descriptions of the filter state of the entire application. |
||
209 | +6 |
- # this needed in filter_manager_srv+ #' Snapshots allow the user to save the current filter state of the application for later use in the session, |
||
210 | -75x | +|||
7 | +
- slices_global$module_slices_api_set(+ #' as well as to save it to file in order to share it with an app developer or other users, |
|||
211 | -75x | +|||
8 | +
- id,+ #' who in turn can upload it to their own session. |
|||
212 | -75x | +|||
9 | +
- list(+ #' |
|||
213 | -75x | +|||
10 | +
- get_available_teal_slices = module_fd()$get_available_teal_slices(),+ #' The snapshot manager is accessed with the camera icon in the tabset bar. |
|||
214 | -75x | +|||
11 | +
- set_filter_state = module_fd()$set_filter_state, # for testing purpose+ #' At the beginning of a session it presents three icons: a camera, an upload, and an circular arrow. |
|||
215 | -75x | +|||
12 | +
- get_filter_state = module_fd()$get_filter_state # for testing purpose+ #' Clicking the camera captures a snapshot, clicking the upload adds a snapshot from a file |
|||
216 | +13 |
- )+ #' and applies the filter states therein, and clicking the arrow resets initial application state. |
||
217 | +14 |
- )+ #' As snapshots are added, they will show up as rows in a table and each will have a select button and a save button. |
||
218 | +15 |
- })+ #' |
||
219 | +16 |
-
+ #' @section Server logic: |
||
220 | +17 |
- # Update global state and mapping matrix when module filters change.+ #' Snapshots are basically `teal_slices` objects, however, since each module is served by a separate instance |
||
221 | -95x | +|||
18 | +
- obs2 <- observeEvent(slices_module(), priority = 0, {+ #' of `FilteredData` and these objects require shared state, `teal_slice` is a `reactiveVal` so `teal_slices` |
|||
222 | -99x | +|||
19 | +
- this_slices <- slices_module()+ #' cannot be stored as is. Therefore, `teal_slices` are reversibly converted to a list of lists representation |
|||
223 | -99x | +|||
20 | +
- slices_global$slices_append(this_slices) # append new slices to the all_slices list+ #' (attributes are maintained). |
|||
224 | -99x | +|||
21 | +
- mapping_elem <- setNames(nm = id, list(vapply(this_slices, `[[`, character(1L), "id")))+ #' |
|||
225 | -99x | +|||
22 | +
- slices_global$slices_active(mapping_elem)+ #' Snapshots are stored in a `reactiveVal` as a named list. |
|||
226 | +23 |
- })+ #' The first snapshot is the initial state of the application and the user can add a snapshot whenever they see fit. |
||
227 | +24 |
-
+ #' |
||
228 | -95x | +|||
25 | +
- obs3 <- observeEvent(slices_global_module(), {+ #' For every snapshot except the initial one, a piece of UI is generated that contains |
|||
229 | -116x | +|||
26 | +
- global_vs_module <- setdiff_teal_slices(slices_global_module(), slices_module())+ #' the snapshot name, a select button to restore that snapshot, and a save button to save it to a file. |
|||
230 | -116x | +|||
27 | +
- module_vs_global <- setdiff_teal_slices(slices_module(), slices_global_module())+ #' The initial snapshot is restored by a separate "reset" button. |
|||
231 | -108x | +|||
28 | +
- if (length(global_vs_module) || length(module_vs_global)) {+ #' It cannot be saved directly but a user is welcome to capture the initial state as a snapshot and save that. |
|||
232 | +29 |
- # Comment: (Nota Bene) Normally new filters for a module are added through module-filter-panel, and slices+ #' |
||
233 | +30 |
- # global are updated automatically so slices_module -> slices_global_module are equal.+ #' @section Snapshot mechanics: |
||
234 | +31 |
- # this if is valid only when a change is made on the global level so the change needs to be propagated down+ #' When a snapshot is captured, the user is prompted to name it. |
||
235 | +32 |
- # to the module (for example through snapshot manager). If it happens both slices are different+ #' Names are displayed as is but since they are used to create button ids, |
||
236 | -13x | +|||
33 | +
- logger::log_debug("srv_module_filter_manager@3 (N.B.) global state has changed for a module:{ id }.")+ #' under the hood they are converted to syntactically valid strings. |
|||
237 | -13x | +|||
34 | +
- module_fd()$clear_filter_states()+ #' New snapshot names are validated so that their valid versions are unique. |
|||
238 | -13x | +|||
35 | +
- module_fd()$set_filter_state(slices_global_module())+ #' Leading and trailing white space is trimmed. |
|||
239 | +36 |
- }+ #' |
||
240 | +37 |
- })+ #' The module can read the global state of the application from `slices_global` and `mapping_matrix`. |
||
241 | +38 |
-
+ #' The former provides a list of all existing `teal_slice`s and the latter says which slice is active in which module. |
||
242 | -95x | +|||
39 | +
- slices_module # returned for testing purpose+ #' Once a name has been accepted, `slices_global` is converted to a list of lists - a snapshot. |
|||
243 | +40 |
- })+ #' The snapshot contains the `mapping` attribute of the initial application state |
||
244 | +41 |
- }+ #' (or one that has been restored), which may not reflect the current one, |
||
245 | +42 |
-
+ #' so `mapping_matrix` is transformed to obtain the current mapping, i.e. a list that, |
||
246 | +43 |
- #' @importFrom shiny reactiveVal reactiveValues+ #' when passed to the `mapping` argument of [teal_slices()], would result in the current mapping. |
||
247 | +44 |
- methods::setOldClass("reactiveVal")+ #' This is substituted as the snapshot's `mapping` attribute and the snapshot is added to the snapshot list. |
||
248 | +45 |
- methods::setOldClass("reactivevalues")+ #' |
||
249 | +46 |
-
+ #' To restore app state, a snapshot is retrieved from storage and rebuilt into a `teal_slices` object. |
||
250 | +47 |
- #' @importFrom methods new+ #' Then state of all `FilteredData` objects (provided in `datasets`) is cleared |
||
251 | +48 |
- #' @rdname module_filter_manager+ #' and set anew according to the `mapping` attribute of the snapshot. |
||
252 | +49 |
- .slicesGlobal <- methods::setRefClass(".slicesGlobal", # nolint: object_name.+ #' The snapshot is then set as the current content of `slices_global`. |
||
253 | +50 |
- fields = list(+ #' |
||
254 | +51 |
- all_slices = "reactiveVal",+ #' To save a snapshot, the snapshot is retrieved and reassembled just like for restoring, |
||
255 | +52 |
- module_slices_api = "reactivevalues"+ #' and then saved to file with [slices_store()]. |
||
256 | +53 |
- ),+ #' |
||
257 | +54 |
- methods = list(+ #' When a snapshot is uploaded, it will first be added to storage just like a newly created one, |
||
258 | +55 |
- initialize = function(slices = teal_slices(), module_labels) {+ #' and then used to restore app state much like a snapshot taken from storage. |
||
259 | -70x | +|||
56 | +
- shiny::isolate({+ #' Upon clicking the upload icon the user will be prompted for a file to upload |
|||
260 | -70x | +|||
57 | +
- checkmate::assert_class(slices, "teal_slices")+ #' and may choose to name the new snapshot. The name defaults to the name of the file (the extension is dropped) |
|||
261 | +58 |
- # needed on init to not mix "global_filters" with module-specific-slots+ #' and normal naming rules apply. Loading the file yields a `teal_slices` object, |
||
262 | -70x | +|||
59 | +
- if (isTRUE(attr(slices, "module_specific"))) {+ #' which is disassembled for storage and used directly for restoring app state. |
|||
263 | -9x | +|||
60 | +
- old_mapping <- attr(slices, "mapping")+ #' |
|||
264 | -9x | +|||
61 | +
- new_mapping <- sapply(module_labels, simplify = FALSE, function(module_label) {+ #' @section Transferring snapshots: |
|||
265 | -18x | +|||
62 | +
- unique(unlist(old_mapping[c(module_label, "global_filters")]))+ #' Snapshots uploaded from disk should only be used in the same application they come from, |
|||
266 | +63 |
- })+ #' _i.e._ an application that uses the same data and the same modules. |
||
267 | -9x | +|||
64 | +
- attr(slices, "mapping") <- new_mapping+ #' To ensure this is the case, `init` stamps `teal_slices` with an app id that is stored in the `app_id` attribute of |
|||
268 | +65 |
- }+ #' a `teal_slices` object. When a snapshot is restored from file, its `app_id` is compared to that |
||
269 | -70x | +|||
66 | +
- .self$all_slices <<- shiny::reactiveVal(slices)+ #' of the current app state and only if the match is the snapshot admitted to the session. |
|||
270 | -70x | +|||
67 | +
- .self$module_slices_api <<- shiny::reactiveValues()+ #' |
|||
271 | -70x | +|||
68 | +
- .self$slices_append(slices)+ #' @section Bookmarks: |
|||
272 | -70x | +|||
69 | +
- .self$slices_active(attr(slices, "mapping"))+ #' An `onBookmark` callback creates a snapshot of the current filter state. |
|||
273 | -70x | +|||
70 | +
- invisible(.self)+ #' This is done on the app session, not the module session. |
|||
274 | +71 |
- })+ #' (The snapshot will be retrieved by `module_teal` in order to set initial app state in a restored app.) |
||
275 | +72 |
- },+ #' Then that snapshot, and the previous snapshot history are dumped into the `values.rds` file in `<bookmark_dir>`. |
||
276 | +73 |
- is_module_specific = function() {+ #' |
||
277 | -247x | +|||
74 | +
- isTRUE(attr(.self$all_slices(), "module_specific"))+ #' @param id (`character(1)`) `shiny` module instance id. |
|||
278 | +75 |
- },+ #' @param slices_global (`reactiveVal`) that contains a `teal_slices` object |
||
279 | +76 |
- module_slices_api_set = function(module_label, functions_list) {+ #' containing all `teal_slice`s existing in the app, both active and inactive. |
||
280 | -75x | +|||
77 | +
- shiny::isolate({+ #' |
|||
281 | -75x | +|||
78 | +
- if (!.self$is_module_specific()) {+ #' @return `list` containing the snapshot history, where each element is an unlisted `teal_slices` object. |
|||
282 | -61x | +|||
79 | +
- module_label <- "global_filters"+ #' |
|||
283 | +80 |
- }+ #' @name module_snapshot_manager |
||
284 | -75x | +|||
81 | +
- if (!identical(.self$module_slices_api[[module_label]], functions_list)) {+ #' @rdname module_snapshot_manager |
|||
285 | -75x | +|||
82 | +
- .self$module_slices_api[[module_label]] <- functions_list+ #' |
|||
286 | +83 |
- }+ #' @author Aleksander Chlebowski |
||
287 | -75x | +|||
84 | +
- invisible(.self)+ #' @keywords internal |
|||
288 | +85 |
- })+ NULL |
||
289 | +86 |
- },+ |
||
290 | +87 |
- slices_deactivate_all = function(module_label) {+ #' @rdname module_snapshot_manager |
||
291 | -! | +|||
88 | +
- shiny::isolate({+ ui_snapshot_manager_panel <- function(id) { |
|||
292 | +89 | ! |
- new_slices <- .self$all_slices()+ ns <- NS(id) |
|
293 | +90 | ! |
- old_mapping <- attr(new_slices, "mapping")- |
- |
294 | -- |
-
+ tags$button( |
||
295 | +91 | ! |
- new_mapping <- if (.self$is_module_specific()) {+ id = ns("show_snapshot_manager"), |
|
296 | +92 | ! |
- new_module_mapping <- setNames(nm = module_label, list(character(0)))+ class = "btn action-button wunder_bar_button", |
|
297 | +93 | ! |
- modifyList(old_mapping, new_module_mapping)+ title = "View filter mapping", |
|
298 | +94 | ! |
- } else if (missing(module_label)) {+ suppressMessages(icon("fas fa-camera")) |
|
299 | -! | +|||
95 | +
- lapply(+ ) |
|||
300 | -! | +|||
96 | +
- attr(.self$all_slices(), "mapping"),+ } |
|||
301 | -! | +|||
97 | +
- function(x) character(0)+ |
|||
302 | +98 |
- )+ #' @rdname module_snapshot_manager |
||
303 | +99 |
- } else {+ srv_snapshot_manager_panel <- function(id, slices_global) { |
||
304 | -! | +|||
100 | +69x |
- old_mapping[[module_label]] <- character(0)+ moduleServer(id, function(input, output, session) { |
||
305 | -! | +|||
101 | +69x |
- old_mapping+ logger::log_debug("srv_snapshot_manager_panel initializing") |
||
306 | -+ | |||
102 | +69x |
- }+ setBookmarkExclude(c("show_snapshot_manager")) |
||
307 | -+ | |||
103 | +69x |
-
+ observeEvent(input$show_snapshot_manager, { |
||
308 | +104 | ! |
- if (!identical(new_mapping, old_mapping)) {+ logger::log_debug("srv_snapshot_manager_panel@1 show_snapshot_manager button has been clicked.") |
|
309 | +105 | ! |
- logger::log_debug(".slicesGlobal@slices_deactivate_all: deactivating all slices.")+ showModal( |
|
310 | +106 | ! |
- attr(new_slices, "mapping") <- new_mapping+ modalDialog( |
|
311 | +107 | ! |
- .self$all_slices(new_slices)+ ui_snapshot_manager(session$ns("module")), |
|
312 | -+ | |||
108 | +! |
- }+ class = "snapshot_manager_modal", |
||
313 | +109 | ! |
- invisible(.self)+ size = "m", |
|
314 | -+ | |||
110 | +! |
- })+ footer = NULL,+ |
+ ||
111 | +! | +
+ easyClose = TRUE |
||
315 | +112 |
- },+ ) |
||
316 | +113 |
- slices_active = function(mapping_elem) {+ ) |
||
317 | -172x | +|||
114 | +
- shiny::isolate({+ }) |
|||
318 | -172x | +115 | +69x |
- if (.self$is_module_specific()) {+ srv_snapshot_manager("module", slices_global = slices_global) |
319 | -32x | +|||
116 | +
- new_mapping <- modifyList(attr(.self$all_slices(), "mapping"), mapping_elem)+ }) |
|||
320 | +117 |
- } else {+ } |
||
321 | -140x | +|||
118 | +
- new_mapping <- setNames(nm = "global_filters", list(unique(unlist(mapping_elem))))+ |
|||
322 | +119 |
- }+ #' @rdname module_snapshot_manager |
||
323 | +120 |
-
+ ui_snapshot_manager <- function(id) { |
||
324 | -172x | +|||
121 | +! |
- if (!identical(new_mapping, attr(.self$all_slices(), "mapping"))) {+ ns <- NS(id) |
||
325 | -115x | +|||
122 | +! |
- mapping_modules <- toString(names(new_mapping))+ tags$div( |
||
326 | -115x | +|||
123 | +! |
- logger::log_debug(".slicesGlobal@slices_active: changing mapping for module(s): { mapping_modules }.")+ class = "manager_content", |
||
327 | -115x | +|||
124 | +! |
- new_slices <- .self$all_slices()+ tags$div( |
||
328 | -115x | +|||
125 | +! |
- attr(new_slices, "mapping") <- new_mapping+ class = "manager_table_row", |
||
329 | -115x | +|||
126 | +! |
- .self$all_slices(new_slices)+ tags$span(tags$b("Snapshot manager")), |
||
330 | -+ | |||
127 | +! |
- }+ actionLink(ns("snapshot_add"), label = NULL, icon = icon("fas fa-camera"), title = "add snapshot"),+ |
+ ||
128 | +! | +
+ actionLink(ns("snapshot_load"), label = NULL, icon = icon("fas fa-upload"), title = "upload snapshot"),+ |
+ ||
129 | +! | +
+ actionLink(ns("snapshot_reset"), label = NULL, icon = icon("fas fa-undo"), title = "reset initial state"),+ |
+ ||
130 | +! | +
+ NULL |
||
331 | +131 |
-
+ ), |
||
332 | -172x | +|||
132 | +! |
- invisible(.self)+ uiOutput(ns("snapshot_list")) |
||
333 | +133 |
- })+ ) |
||
334 | +134 |
- },+ } |
||
335 | +135 |
- # - only new filters are appended to the $all_slices+ |
||
336 | +136 |
- # - mapping is not updated here+ #' @rdname module_snapshot_manager |
||
337 | +137 |
- slices_append = function(slices, activate = FALSE) {+ srv_snapshot_manager <- function(id, slices_global) { |
||
338 | -172x | +138 | +69x |
- shiny::isolate({+ checkmate::assert_character(id)+ |
+
139 | ++ | + | ||
339 | -172x | +140 | +69x |
- if (!is.teal_slices(slices)) {+ moduleServer(id, function(input, output, session) { |
340 | -! | +|||
141 | +69x |
- slices <- as.teal_slices(slices)+ logger::log_debug("srv_snapshot_manager initializing") |
||
341 | +142 |
- }+ |
||
342 | +143 |
-
+ # Set up bookmarking callbacks ---- |
||
343 | +144 |
- # to make sure that we don't unnecessary trigger $all_slices <reactiveVal>+ # Register bookmark exclusions (all buttons and text fields). |
||
344 | -172x | +145 | +69x |
- new_slices <- setdiff_teal_slices(slices, .self$all_slices())+ setBookmarkExclude(c( |
345 | -172x | +146 | +69x |
- old_mapping <- attr(.self$all_slices(), "mapping")+ "snapshot_add", "snapshot_load", "snapshot_reset", |
346 | -172x | +147 | +69x |
- if (length(new_slices)) {+ "snapshot_name_accept", "snaphot_file_accept", |
347 | -6x | +148 | +69x |
- new_ids <- vapply(new_slices, `[[`, character(1L), "id")+ "snapshot_name", "snapshot_file" |
348 | -6x | +|||
149 | +
- logger::log_debug(".slicesGlobal@slices_append: appending new slice(s): { new_ids }.")+ )) |
|||
349 | -6x | +|||
150 | +
- slices_ids <- vapply(.self$all_slices(), `[[`, character(1L), "id")+ # Add snapshot history to bookmark. |
|||
350 | -6x | +151 | +69x |
- lapply(new_slices, function(slice) {+ session$onBookmark(function(state) {+ |
+
152 | +! | +
+ logger::log_debug("srv_snapshot_manager@onBookmark: storing snapshot and bookmark history")+ |
+ ||
153 | +! | +
+ state$values$snapshot_history <- snapshot_history() # isolate this? |
||
351 | +154 |
- # In case the new state has the same id as an existing one, add a suffix+ }) |
||
352 | -6x | +|||
155 | +
- if (slice$id %in% slices_ids) {+ |
|||
353 | -1x | +156 | +69x |
- slice$id <- utils::tail(make.unique(c(slices_ids, slice$id), sep = "_"), 1)+ ns <- session$ns |
354 | +157 |
- }+ |
||
355 | +158 |
- })+ # Track global filter states ----+ |
+ ||
159 | +69x | +
+ snapshot_history <- reactiveVal({ |
||
356 | +160 |
-
+ # Restore directly from bookmarked state, if applicable. |
||
357 | -6x | +161 | +69x |
- new_slices_all <- c(.self$all_slices(), new_slices)+ restoreValue( |
358 | -6x | +162 | +69x |
- attr(new_slices_all, "mapping") <- old_mapping+ ns("snapshot_history"), |
359 | -6x | +163 | +69x |
- .self$all_slices(new_slices_all)+ list("Initial application state" = shiny::isolate(as.list(slices_global$all_slices(), recursive = TRUE))) |
360 | +164 |
- }+ ) |
||
361 | +165 | - - | -||
362 | -172x | -
- invisible(.self)+ }) |
||
363 | +166 |
- })+ |
||
364 | +167 |
- },+ # Snapshot current application state ---- |
||
365 | +168 |
- slices_get = function(module_label) {+ # Name snaphsot. |
||
366 | -252x | +169 | +69x |
- if (missing(module_label)) {+ observeEvent(input$snapshot_add, { |
367 | +170 | ! |
- .self$all_slices()+ logger::log_debug("srv_snapshot_manager: snapshot_add button clicked") |
|
368 | -+ | |||
171 | +! |
- } else {+ showModal( |
||
369 | -252x | +|||
172 | +! |
- module_ids <- unlist(attr(.self$all_slices(), "mapping")[c(module_label, "global_filters")])+ modalDialog( |
||
370 | -252x | +|||
173 | +! |
- Filter(+ textInput(ns("snapshot_name"), "Name the snapshot", width = "100%", placeholder = "Meaningful, unique name"), |
||
371 | -252x | +|||
174 | +! |
- function(slice) slice$id %in% module_ids,+ footer = tagList( |
||
372 | -252x | +|||
175 | +! |
- .self$all_slices()+ actionButton(ns("snapshot_name_accept"), "Accept", icon = icon("far fa-thumbs-up")),+ |
+ ||
176 | +! | +
+ modalButton(label = "Cancel", icon = icon("far fa-thumbs-down")) |
||
373 | +177 | ++ |
+ ),+ |
+ |
178 | +! | +
+ size = "s"+ |
+ ||
179 |
) |
|||
374 | +180 |
- }+ ) |
||
375 | +181 |
- },+ }) |
||
376 | +182 |
- slices_set = function(slices) {+ # Store snaphsot. |
||
377 | -7x | +183 | +69x |
- shiny::isolate({+ observeEvent(input$snapshot_name_accept, { |
378 | -7x | +|||
184 | +! |
- if (!is.teal_slices(slices)) {+ logger::log_debug("srv_snapshot_manager: snapshot_name_accept button clicked") |
||
379 | +185 | ! |
- slices <- as.teal_slices(slices)+ snapshot_name <- trimws(input$snapshot_name) |
|
380 | -+ | |||
186 | +! |
- }+ if (identical(snapshot_name, "")) { |
||
381 | -7x | +|||
187 | +! |
- .self$all_slices(slices)+ logger::log_debug("srv_snapshot_manager: snapshot name rejected") |
||
382 | -7x | +|||
188 | +! |
- invisible(.self)+ showNotification( |
||
383 | -+ | |||
189 | +! |
- })+ "Please name the snapshot.", |
||
384 | -+ | |||
190 | +! |
- },+ type = "message" |
||
385 | +191 |
- show = function() {+ ) |
||
386 | +192 | ! |
- shiny::isolate(print(.self$all_slices()))+ updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name") |
|
387 | +193 | ! |
- invisible(.self)- |
- |
388 | -- |
- }+ } else if (is.element(make.names(snapshot_name), make.names(names(snapshot_history())))) { |
||
389 | -+ | |||
194 | +! |
- )+ logger::log_debug("srv_snapshot_manager: snapshot name rejected") |
||
390 | -+ | |||
195 | +! |
- )+ showNotification( |
||
391 | -+ | |||
196 | +! |
- # todo: prevent any teal_slices attribute except mapping+ "This name is in conflict with other snapshot names. Please choose a different one.", |
1 | -+ | |||
197 | +! |
- #' Data Module for teal+ type = "message" |
||
2 | +198 |
- #'+ ) |
||
3 | -+ | |||
199 | +! |
- #' This module manages the `data` argument for `srv_teal`. The `teal` framework uses [teal_data()],+ updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name") |
||
4 | +200 |
- #' which can be provided in various ways:+ } else { |
||
5 | -+ | |||
201 | +! |
- #' 1. Directly as a [teal.data::teal_data()] object. This will automatically convert it into a `reactive` `teal_data`.+ logger::log_debug("srv_snapshot_manager: snapshot name accepted, adding snapshot") |
||
6 | -+ | |||
202 | +! |
- #' 2. As a `reactive` object that returns a [teal.data::teal_data()] object.+ snapshot <- as.list(slices_global$all_slices(), recursive = TRUE) |
||
7 | -+ | |||
203 | +! |
- #'+ snapshot_update <- c(snapshot_history(), list(snapshot)) |
||
8 | -+ | |||
204 | +! |
- #' @details+ names(snapshot_update)[length(snapshot_update)] <- snapshot_name |
||
9 | -+ | |||
205 | +! |
- #' ## Reactive `teal_data`:+ snapshot_history(snapshot_update) |
||
10 | -+ | |||
206 | +! |
- #'+ removeModal() |
||
11 | +207 |
- #' The data in the application can be reactively updated, prompting [srv_teal()] to rebuild the+ # Reopen filter manager modal by clicking button in the main application. |
||
12 | -+ | |||
208 | +! |
- #' content accordingly. There are two methods for creating interactive `teal_data`:+ shinyjs::click(id = "teal-wunder_bar-show_snapshot_manager", asis = TRUE) |
||
13 | +209 |
- #' 1. Using a `reactive` object provided from outside the `teal` application. In this scenario,+ } |
||
14 | +210 |
- #' reactivity is controlled by an external module, and `srv_teal` responds to changes.+ }) |
||
15 | +211 |
- #' 2. Using [teal_data_module()], which is embedded within the `teal` application, allowing data to+ |
||
16 | +212 |
- #' be resubmitted by the user as needed.+ # Upload a snapshot file ---- |
||
17 | +213 |
- #'+ # Select file. |
||
18 | -+ | |||
214 | +69x |
- #' Since the server of [teal_data_module()] must return a `reactive` `teal_data` object, both+ observeEvent(input$snapshot_load, { |
||
19 | -+ | |||
215 | +! |
- #' methods (1 and 2) produce the same reactive behavior within a `teal` application. The distinction+ logger::log_debug("srv_snapshot_manager: snapshot_load button clicked") |
||
20 | -+ | |||
216 | +! |
- #' lies in data control: the first method involves external control, while the second method+ showModal( |
||
21 | -+ | |||
217 | +! |
- #' involves control from a custom module within the app.+ modalDialog( |
||
22 | -+ | |||
218 | +! |
- #'+ fileInput(ns("snapshot_file"), "Choose snapshot file", accept = ".json", width = "100%"), |
||
23 | -+ | |||
219 | +! |
- #' For more details, see [`module_teal_data`].+ textInput( |
||
24 | -+ | |||
220 | +! |
- #'+ ns("snapshot_name"), |
||
25 | -+ | |||
221 | +! |
- #' @inheritParams init+ "Name the snapshot (optional)", |
||
26 | -+ | |||
222 | +! |
- #'+ width = "100%", |
||
27 | -+ | |||
223 | +! |
- #' @param data (`teal_data`, `teal_data_module`, or `reactive` returning `teal_data`)+ placeholder = "Meaningful, unique name" |
||
28 | +224 |
- #' The `ui` component of this module does not require `data` if `teal_data_module` is not provided.+ ), |
||
29 | -+ | |||
225 | +! |
- #' The `data` argument in the `ui` is included solely for the `$ui` function of the+ footer = tagList( |
||
30 | -+ | |||
226 | +! |
- #' `teal_data_module`. Otherwise, it can be disregarded, ensuring that `ui_teal` does not depend on+ actionButton(ns("snaphot_file_accept"), "Accept", icon = icon("far fa-thumbs-up")), |
||
31 | -+ | |||
227 | +! |
- #' the reactive data of the enclosing application.+ modalButton(label = "Cancel", icon = icon("far fa-thumbs-down")) |
||
32 | +228 |
- #'+ ) |
||
33 | +229 |
- #' @return A `reactive` object that returns:+ ) |
||
34 | +230 |
- #' - `teal_data` when the object is validated+ ) |
||
35 | +231 |
- #' - `shiny.silent.error` when not validated.+ }) |
||
36 | +232 |
- #'+ # Store new snapshot to list and restore filter states. |
||
37 | -+ | |||
233 | +69x |
- #' @rdname module_init_data+ observeEvent(input$snaphot_file_accept, { |
||
38 | -+ | |||
234 | +! |
- #' @name module_init_data+ logger::log_debug("srv_snapshot_manager: snapshot_file_accept button clicked") |
||
39 | -+ | |||
235 | +! |
- #' @keywords internal+ snapshot_name <- trimws(input$snapshot_name) |
||
40 | -+ | |||
236 | +! |
- NULL+ if (identical(snapshot_name, "")) { |
||
41 | -+ | |||
237 | +! |
-
+ logger::log_debug("srv_snapshot_manager: no snapshot name provided, naming after file") |
||
42 | -+ | |||
238 | +! |
- #' @rdname module_init_data+ snapshot_name <- tools::file_path_sans_ext(input$snapshot_file$name) |
||
43 | +239 |
- ui_init_data <- function(id, data) {- |
- ||
44 | -! | -
- ns <- shiny::NS(id)+ } |
||
45 | +240 | ! |
- shiny::div(+ if (is.element(make.names(snapshot_name), make.names(names(snapshot_history())))) { |
|
46 | +241 | ! |
- id = ns("content"),+ logger::log_debug("srv_snapshot_manager: snapshot name rejected") |
|
47 | +242 | ! |
- style = "display: inline-block;",+ showNotification( |
|
48 | +243 | ! |
- if (inherits(data, "teal_data_module")) {+ "This name is in conflict with other snapshot names. Please choose a different one.", |
|
49 | +244 | ! |
- ui_teal_data(ns("teal_data_module"), data_module = data)+ type = "message" |
|
50 | +245 |
- } else {+ ) |
||
51 | +246 | ! |
- NULL+ updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name") |
|
52 | +247 |
- }+ } else { |
||
53 | +248 |
- )+ # Restore snapshot and verify app compatibility. |
||
54 | -+ | |||
249 | +! |
- }+ logger::log_debug("srv_snapshot_manager: snapshot name accepted, loading snapshot") |
||
55 | -+ | |||
250 | +! |
-
+ snapshot_state <- try(slices_restore(input$snapshot_file$datapath)) |
||
56 | -- |
- #' @rdname module_init_data- |
- ||
57 | -+ | |||
251 | +! |
- srv_init_data <- function(id, data, modules, filter = teal_slices()) {+ if (!inherits(snapshot_state, "modules_teal_slices")) { |
||
58 | -71x | +|||
252 | +! |
- checkmate::assert_character(id, max.len = 1, any.missing = FALSE)+ logger::log_debug("srv_snapshot_manager: snapshot file corrupt") |
||
59 | -71x | +|||
253 | +! |
- checkmate::assert_multi_class(data, c("teal_data", "teal_data_module", "reactive", "reactiveVal"))+ showNotification( |
||
60 | -71x | +|||
254 | +! |
- checkmate::assert_class(modules, "teal_modules")+ "File appears to be corrupt.", |
||
61 | -71x | +|||
255 | +! |
- checkmate::assert_class(filter, "teal_slices")+ type = "error" |
||
62 | +256 | - - | -||
63 | -71x | -
- moduleServer(id, function(input, output, session) {+ ) |
||
64 | -71x | +|||
257 | +! |
- logger::log_debug("srv_data initializing.")+ } else if (!identical(attr(snapshot_state, "app_id"), attr(slices_global$all_slices(), "app_id"))) { |
||
65 | -+ | |||
258 | +! |
-
+ logger::log_debug("srv_snapshot_manager: snapshot not compatible with app") |
||
66 | -71x | +|||
259 | +! |
- if (getOption("teal.show_js_log", default = FALSE)) {+ showNotification( |
||
67 | +260 | ! |
- shinyjs::showLog()+ "This snapshot file is not compatible with the app and cannot be loaded.", |
|
68 | -+ | |||
261 | +! |
- }+ type = "warning" |
||
69 | +262 |
-
+ ) |
||
70 | +263 |
- # data_rv contains teal_data object+ } else { |
||
71 | +264 |
- # either passed to teal::init or returned from teal_data_module- |
- ||
72 | -71x | -
- data_validated <- if (inherits(data, "teal_data_module")) {- |
- ||
73 | -9x | -
- srv_teal_data(+ # Add to snapshot history. |
||
74 | -9x | +|||
265 | +! |
- "teal_data_module",+ logger::log_debug("srv_snapshot_manager: snapshot loaded, adding to history") |
||
75 | -9x | +|||
266 | +! |
- data = reactive(req(FALSE)), # to .fallback_on_failure to shiny.silent.error+ snapshot <- as.list(slices_global$all_slices(), recursive = TRUE) |
||
76 | -9x | +|||
267 | +! |
- data_module = data,+ snapshot_update <- c(snapshot_history(), list(snapshot)) |
||
77 | -9x | +|||
268 | +! |
- modules = modules,+ names(snapshot_update)[length(snapshot_update)] <- snapshot_name |
||
78 | -9x | +|||
269 | +! |
- validate_shiny_silent_error = FALSE+ snapshot_history(snapshot_update) |
||
79 | +270 |
- )- |
- ||
80 | -71x | -
- } else if (inherits(data, "teal_data")) {+ ### Begin simplified restore procedure. ### |
||
81 | -35x | +|||
271 | +! |
- reactiveVal(data)+ logger::log_debug("srv_snapshot_manager: restoring snapshot") |
||
82 | -71x | +|||
272 | +! |
- } else if (test_reactive(data)) {+ slices_global$slices_set(snapshot_state) |
||
83 | -27x | +|||
273 | +! |
- .fallback_on_failure(this = data, that = reactive(req(FALSE)), label = "Reactive data")+ removeModal() |
||
84 | +274 |
- }+ ### End simplified restore procedure. ### |
||
85 | +275 |
-
+ } |
||
86 | -70x | +|||
276 | +
- if (inherits(data, "teal_data_module")) {+ } |
|||
87 | -8x | +|||
277 | +
- shinyjs::disable(selector = sprintf(".teal-body:has('#%s') .nav li a", session$ns("content")))+ }) |
|||
88 | +278 |
- }+ # Apply newly added snapshot. |
||
89 | +279 | |||
90 | -70x | +|||
280 | +
- observeEvent(data_validated(), {+ # Restore initial state ---- |
|||
91 | -57x | +281 | +69x |
- showNotification("Data loaded successfully.", duration = 5)+ observeEvent(input$snapshot_reset, { |
92 | -57x | +282 | +2x |
- shinyjs::enable(selector = sprintf(".teal-body:has('#%s') .nav li a", session$ns("content")))+ logger::log_debug("srv_snapshot_manager: snapshot_reset button clicked, restoring snapshot") |
93 | -57x | +283 | +2x |
- if (isTRUE(attr(data, "once"))) {+ s <- "Initial application state" |
94 | +284 |
- # Hiding the data module tab.+ ### Begin restore procedure. ### |
||
95 | -5x | +285 | +2x |
- shinyjs::hide(+ snapshot <- snapshot_history()[[s]] |
96 | -5x | +|||
286 | +
- selector = sprintf(+ # todo: as.teal_slices looses module-mapping if is not global |
|||
97 | -5x | +287 | +2x |
- ".teal-body:has('#%s') a[data-value='teal_data_module']",+ snapshot_state <- as.teal_slices(snapshot) |
98 | -5x | +288 | +2x |
- session$ns("content")+ slices_global$slices_set(snapshot_state) |
99 | -+ | |||
289 | +2x |
- )+ removeModal() |
||
100 | +290 |
- )+ ### End restore procedure. ### |
||
101 | +291 |
- # Clicking the second tab, which is the first module.- |
- ||
102 | -5x | -
- shinyjs::runjs(- |
- ||
103 | -5x | -
- sprintf(- |
- ||
104 | -5x | -
- "document.querySelector('.teal-body:has(#%s) .nav li:nth-child(2) a').click();",+ }) |
||
105 | -5x | +|||
292 | +
- session$ns("content")+ |
|||
106 | +293 |
- )+ # Build snapshot table ---- |
||
107 | +294 |
- )+ # Create UI elements and server logic for the snapshot table. |
||
108 | +295 |
- }+ # Observers must be tracked to avoid duplication and excess reactivity. |
||
109 | +296 |
-
+ # Remaining elements are tracked likewise for consistency and a slight speed margin. |
||
110 | -57x | +297 | +69x |
- is_filter_ok <- check_filter_datanames(filter, ls(teal.code::get_env(data_validated())))+ observers <- reactiveValues() |
111 | -57x | +298 | +69x |
- if (!isTRUE(is_filter_ok)) {+ handlers <- reactiveValues() |
112 | -2x | +299 | +69x |
- showNotification(+ divs <- reactiveValues() |
113 | -2x | +|||
300 | +
- "Some filters were not applied because of incompatibility with data. Contact app developer.",+ |
|||
114 | -2x | +301 | +69x |
- type = "warning",+ observeEvent(snapshot_history(), { |
115 | -2x | +302 | +59x |
- duration = 10+ logger::log_debug("srv_snapshot_manager: snapshot history modified, updating snapshot list") |
116 | -+ | |||
303 | +59x |
- )+ lapply(names(snapshot_history())[-1L], function(s) { |
||
117 | -2x | +|||
304 | +! |
- warning(is_filter_ok)+ id_pickme <- sprintf("pickme_%s", make.names(s)) |
||
118 | -+ | |||
305 | +! |
- }+ id_saveme <- sprintf("saveme_%s", make.names(s)) |
||
119 | -+ | |||
306 | +! |
- })+ id_rowme <- sprintf("rowme_%s", make.names(s)) |
||
120 | +307 | |||
121 | -70x | -
- observeEvent(data_validated(), once = TRUE, {- |
- ||
122 | +308 |
- # Excluding the ids from teal_data_module using full namespace and global shiny app session.- |
- ||
123 | -55x | -
- app_session <- .subset2(shiny::getDefaultReactiveDomain(), "parent")- |
- ||
124 | -55x | -
- setBookmarkExclude(+ # Observer for restoring snapshot. |
||
125 | -55x | +|||
309 | +! |
- session$ns(+ if (!is.element(id_pickme, names(observers))) { |
||
126 | -55x | +|||
310 | +! |
- grep(+ observers[[id_pickme]] <- observeEvent(input[[id_pickme]], { |
||
127 | -55x | +|||
311 | +
- pattern = "teal_data_module-",+ ### Begin restore procedure. ### |
|||
128 | -55x | +|||
312 | +! |
- x = names(reactiveValuesToList(input)),+ snapshot <- snapshot_history()[[s]] |
||
129 | -55x | +|||
313 | +! |
- value = TRUE+ snapshot_state <- as.teal_slices(snapshot) |
||
130 | +314 |
- )+ |
||
131 | -+ | |||
315 | +! |
- ),+ slices_global$slices_set(snapshot_state) |
||
132 | -55x | +|||
316 | +! |
- session = app_session+ removeModal() |
||
133 | +317 |
- )+ ### End restore procedure. ### |
||
134 | +318 |
- })+ }) |
||
135 | +319 |
-
+ } |
||
136 | +320 |
- # Adds signature protection to the datanames in the data+ # Create handler for downloading snapshot. |
||
137 | -70x | +|||
321 | +! |
- reactive({+ if (!is.element(id_saveme, names(handlers))) { |
||
138 | -62x | +|||
322 | +! |
- req(data_validated())+ output[[id_saveme]] <- downloadHandler( |
||
139 | -58x | +|||
323 | +! |
- .add_signature_to_data(data_validated())+ filename = function() { |
||
140 | -+ | |||
324 | +! |
- })+ sprintf("teal_snapshot_%s_%s.json", s, Sys.Date()) |
||
141 | +325 |
- })+ }, |
||
142 | -+ | |||
326 | +! |
- }+ content = function(file) { |
||
143 | -- | - - | -||
144 | -- |
- #' Adds signature protection to the `datanames` in the data- |
- ||
145 | -- |
- #' @param data (`teal_data`)- |
- ||
146 | -- |
- #' @return `teal_data` with additional code that has signature of the `datanames`- |
- ||
147 | -+ | |||
327 | +! |
- #' @keywords internal+ snapshot <- snapshot_history()[[s]] |
||
148 | -+ | |||
328 | +! |
- .add_signature_to_data <- function(data) {+ snapshot_state <- as.teal_slices(snapshot) |
||
149 | -58x | +|||
329 | +! |
- hashes <- .get_hashes_code(data)+ slices_store(tss = snapshot_state, file = file) |
||
150 | +330 | - - | -||
151 | -58x | -
- tdata <- do.call(- |
- ||
152 | -58x | -
- teal.data::teal_data,- |
- ||
153 | -58x | -
- c(- |
- ||
154 | -58x | -
- list(code = trimws(c(teal.code::get_code(data), hashes), which = "right")),- |
- ||
155 | -58x | -
- list(join_keys = teal.data::join_keys(data)),- |
- ||
156 | -58x | -
- sapply(- |
- ||
157 | -58x | -
- ls(teal.code::get_env(data)),- |
- ||
158 | -58x | -
- teal.code::get_var,- |
- ||
159 | -58x | -
- object = data,- |
- ||
160 | -58x | -
- simplify = FALSE+ } |
||
161 | +331 |
- )+ ) |
||
162 | -+ | |||
332 | +! |
- )+ handlers[[id_saveme]] <- id_saveme |
||
163 | +333 |
- )+ } |
||
164 | +334 | - - | -||
165 | -58x | -
- tdata@verified <- data@verified+ # Create a row for the snapshot table. |
||
166 | -58x | +|||
335 | +! |
- tdata+ if (!is.element(id_rowme, names(divs))) { |
||
167 | -+ | |||
336 | +! |
- }+ divs[[id_rowme]] <- tags$div( |
||
168 | -+ | |||
337 | +! |
-
+ class = "manager_table_row", |
||
169 | -+ | |||
338 | +! |
- #' Get code that tests the integrity of the reproducible data+ tags$span(tags$h5(s)), |
||
170 | -+ | |||
339 | +! |
- #'+ actionLink(inputId = ns(id_pickme), label = icon("far fa-circle-check"), title = "select"), |
||
171 | -+ | |||
340 | +! |
- #' @param data (`teal_data`) object holding the data+ downloadLink(outputId = ns(id_saveme), label = icon("far fa-save"), title = "save to file") |
||
172 | +341 |
- #' @param datanames (`character`) names of `datasets`+ ) |
||
173 | +342 |
- #'+ } |
||
174 | +343 |
- #' @return A character vector with the code lines.+ }) |
||
175 | +344 |
- #' @keywords internal+ }) |
||
176 | +345 |
- #'+ |
||
177 | +346 |
- .get_hashes_code <- function(data, datanames = ls(teal.code::get_env(data))) {+ # Create table to display list of snapshots and their actions. |
||
178 | -58x | +347 | +69x |
- vapply(+ output$snapshot_list <- renderUI({ |
179 | -58x | +348 | +59x |
- datanames,+ rows <- rev(reactiveValuesToList(divs)) |
180 | -58x | +349 | +59x |
- function(dataname, datasets) {+ if (length(rows) == 0L) { |
181 | -105x | +350 | +59x |
- hash <- rlang::hash(data[[dataname]])+ tags$div( |
182 | -105x | +351 | +59x |
- sprintf(+ class = "manager_placeholder", |
183 | -105x | +352 | +59x |
- "stopifnot(%s == %s) # @linksto %s",+ "Snapshots will appear here." |
184 | -105x | +|||
353 | +
- deparse1(bquote(rlang::hash(.(as.name(dataname))))),+ ) |
|||
185 | -105x | +|||
354 | +
- deparse1(hash),+ } else { |
|||
186 | -105x | +|||
355 | +! |
- dataname+ rows |
||
187 | +356 |
- )+ } |
||
188 | +357 |
- },+ }) |
||
189 | -58x | +|||
358 | +
- character(1L),+ |
|||
190 | -58x | +359 | +69x |
- USE.NAMES = TRUE+ snapshot_history |
191 | +360 |
- )+ }) |
||
192 | +361 |
}@@ -18717,56 +18178,56 @@ teal coverage - 57.64% |
1 |
- #' @title `TealReportCard`+ #' Calls all `modules` |
||
2 |
- #' @description `r lifecycle::badge("experimental")`+ #' |
||
3 |
- #' Child class of [`ReportCard`] that is used for `teal` specific applications.+ #' On the UI side each `teal_modules` is translated to a `tabsetPanel` and each `teal_module` is a |
||
4 |
- #' In addition to the parent methods, it supports rendering `teal` specific elements such as+ #' `tabPanel`. Both, UI and server are called recursively so that each tab is a separate module and |
||
5 |
- #' the source code, the encodings panel content and the filter panel content as part of the+ #' reflect nested structure of `modules` argument. |
||
6 |
- #' meta data.+ #' |
||
7 |
- #' @export+ #' @name module_teal_module |
||
9 |
- TealReportCard <- R6::R6Class( # nolint: object_name.+ #' @inheritParams module_teal |
||
10 |
- classname = "TealReportCard",+ #' |
||
11 |
- inherit = teal.reporter::ReportCard,+ #' @param data_rv (`reactive` returning `teal_data`) |
||
12 |
- public = list(+ #' |
||
13 |
- #' @description Appends the source code to the `content` meta data of this `TealReportCard`.+ #' @param slices_global (`reactiveVal` returning `modules_teal_slices`) |
||
14 |
- #'+ #' see [`module_filter_manager`] |
||
15 |
- #' @param src (`character(1)`) code as text.+ #' |
||
16 |
- #' @param ... any `rmarkdown` `R` chunk parameter and its value.+ #' @param depth (`integer(1)`) |
||
17 |
- #' But `eval` parameter is always set to `FALSE`.+ #' number which helps to determine depth of the modules nesting. |
||
18 |
- #' @return Object of class `TealReportCard`, invisibly.+ #' |
||
19 |
- #' @examples+ #' @param datasets (`reactive` returning `FilteredData` or `NULL`) |
||
20 |
- #' card <- TealReportCard$new()$append_src(+ #' When `datasets` is passed from the parent module (`srv_teal`) then `dataset` is a singleton |
||
21 |
- #' "plot(iris)"+ #' which implies in filter-panel to be "global". When `NULL` then filter-panel is "module-specific". |
||
22 |
- #' )+ #' |
||
23 |
- #' card$get_content()[[1]]$get_content()+ #' @param data_load_status (`reactive` returning `character`) |
||
24 |
- append_src = function(src, ...) {+ #' Determines action dependent on a data loading status: |
||
25 | -4x | +
- checkmate::assert_character(src, min.len = 0, max.len = 1)+ #' - `"ok"` when `teal_data` is returned from the data loading. |
|
26 | -4x | +
- params <- list(...)+ #' - `"teal_data_module failed"` when [teal_data_module()] didn't return `teal_data`. Disables tabs buttons. |
|
27 | -4x | +
- params$eval <- FALSE+ #' - `"external failed"` when a `reactive` passed to `srv_teal(data)` didn't return `teal_data`. Hides the whole tab |
|
28 | -4x | +
- rblock <- RcodeBlock$new(src)+ #' panel. |
|
29 | -4x | +
- rblock$set_params(params)+ #' |
|
30 | -4x | +
- self$append_content(rblock)+ #' @return |
|
31 | -4x | +
- self$append_metadata("SRC", src)+ #' output of currently active module. |
|
32 | -4x | +
- invisible(self)+ #' - `srv_teal_module.teal_module` returns `reactiveVal` containing output of the called module. |
|
33 |
- },+ #' - `srv_teal_module.teal_modules` returns output of module selected by `input$active_tab`. |
||
34 |
- #' @description Appends the filter state list to the `content` and `metadata` of this `TealReportCard`.+ #' |
||
35 |
- #' If the filter state list has an attribute named `formatted`, it appends it to the card otherwise it uses+ #' @keywords internal |
||
36 |
- #' the default `yaml::as.yaml` to format the list.+ NULL |
||
37 |
- #' If the filter state list is empty, nothing is appended to the `content`.+ |
||
38 |
- #'+ #' @rdname module_teal_module |
||
39 |
- #' @param fs (`teal_slices`) object returned from [teal_slices()] function.+ ui_teal_module <- function(id, modules, depth = 0L) { |
||
40 | -+ | ! |
- #' @return `self`, invisibly.+ checkmate::assert_multi_class(modules, c("teal_modules", "teal_module", "shiny.tag")) |
41 | -+ | ! |
- append_fs = function(fs) {+ checkmate::assert_count(depth) |
42 | -5x | +! |
- checkmate::assert_class(fs, "teal_slices")+ UseMethod("ui_teal_module", modules) |
43 | -4x | +
- self$append_text("Filter State", "header3")+ } |
|
44 | -4x | +
- if (length(fs)) {+ |
|
45 | -3x | +
- self$append_content(TealSlicesBlock$new(fs))+ #' @rdname module_teal_module |
|
46 |
- } else {+ #' @export |
||
47 | -1x | +
- self$append_text("No filters specified.")+ ui_teal_module.default <- function(id, modules, depth = 0L) { |
|
48 | -+ | ! |
- }+ stop("Modules class not supported: ", paste(class(modules), collapse = " ")) |
49 | -4x | +
- invisible(self)+ } |
|
50 |
- },+ |
||
51 |
- #' @description Appends the encodings list to the `content` and `metadata` of this `TealReportCard`.+ #' @rdname module_teal_module |
||
52 |
- #'+ #' @export |
||
53 |
- #' @param encodings (`list`) list of encodings selections of the `teal` app.+ ui_teal_module.teal_modules <- function(id, modules, depth = 0L) { |
||
54 | -+ | ! |
- #' @return `self`, invisibly.+ ns <- NS(id) |
55 | -+ | ! |
- #' @examples+ tags$div( |
56 | -+ | ! |
- #' card <- TealReportCard$new()$append_encodings(list(variable1 = "X"))+ id = ns("wrapper"), |
57 | -+ | ! |
- #' card$get_content()[[1]]$get_content()+ do.call( |
58 | -+ | ! |
- #'+ tabsetPanel, |
59 | -+ | ! |
- append_encodings = function(encodings) {+ c( |
60 | -4x | +
- checkmate::assert_list(encodings)+ # by giving an id, we can reactively respond to tab changes |
|
61 | -4x | +! |
- self$append_text("Selected Options", "header3")+ list( |
62 | -4x | +! |
- if (requireNamespace("yaml", quietly = TRUE)) {+ id = ns("active_tab"), |
63 | -4x | +! |
- self$append_text(yaml::as.yaml(encodings, handlers = list(+ type = if (modules$label == "root") "pills" else "tabs" |
64 | -4x | +
- POSIXct = function(x) format(x, "%Y-%m-%d"),+ ), |
|
65 | -4x | +! |
- POSIXlt = function(x) format(x, "%Y-%m-%d"),+ lapply( |
66 | -4x | +! |
- Date = function(x) format(x, "%Y-%m-%d")+ names(modules$children), |
67 | -4x | +! |
- )), "verbatim")+ function(module_id) { |
68 | -+ | ! |
- } else {+ module_label <- modules$children[[module_id]]$label |
69 | ! |
- stop("yaml package is required to format the encodings list")+ if (is.null(module_label)) { |
|
70 | -+ | ! |
- }+ module_label <- icon("fas fa-database") |
71 | -4x | +
- self$append_metadata("Encodings", encodings)+ } |
|
72 | -4x | +! |
- invisible(self)+ tabPanel( |
73 | -+ | ! |
- }+ title = module_label, |
74 | -+ | ! |
- ),+ value = module_id, # when clicked this tab value changes input$<tabset panel id> |
75 | -+ | ! |
- private = list(+ ui_teal_module( |
76 | -+ | ! |
- dispatch_block = function(block_class) {+ id = ns(module_id), |
77 | ! |
- eval(str2lang(block_class))+ modules = modules$children[[module_id]], |
|
78 | -+ | ! |
- }+ depth = depth + 1L |
79 |
- )+ ) |
||
80 |
- )+ ) |
||
81 |
-
+ } |
||
82 |
- #' @title `TealSlicesBlock`+ ) |
||
83 |
- #' @docType class+ ) |
||
84 |
- #' @description+ ) |
||
85 |
- #' Specialized `TealSlicesBlock` block for managing filter panel content in reports.+ ) |
||
86 |
- #' @keywords internal+ } |
||
87 |
- TealSlicesBlock <- R6::R6Class( # nolint: object_name_linter.+ |
||
88 |
- classname = "TealSlicesBlock",+ #' @rdname module_teal_module |
||
89 |
- inherit = teal.reporter:::TextBlock,+ #' @export |
||
90 |
- public = list(+ ui_teal_module.teal_module <- function(id, modules, depth = 0L) { |
||
91 | -+ | ! |
- #' @description Returns a `TealSlicesBlock` object.+ ns <- NS(id) |
92 | -+ | ! |
- #'+ args <- c(list(id = ns("module")), modules$ui_args) |
93 |
- #' @details Returns a `TealSlicesBlock` object with no content and no parameters.+ |
||
94 | -+ | ! |
- #'+ ui_teal <- tagList( |
95 | -+ | ! |
- #' @param content (`teal_slices`) object returned from [teal_slices()] function.+ div( |
96 | -+ | ! |
- #' @param style (`character(1)`) string specifying style to apply.+ id = ns("validate_datanames"), |
97 | -+ | ! |
- #'+ ui_validate_reactive_teal_data(ns("validate_datanames")) |
98 |
- #' @return Object of class `TealSlicesBlock`, invisibly.+ ), |
||
99 | -+ | ! |
- #'+ shinyjs::hidden( |
100 | -+ | ! |
- initialize = function(content = teal_slices(), style = "verbatim") {+ tags$div( |
101 | -9x | +! |
- self$set_content(content)+ id = ns("transformer_failure_info"), |
102 | -8x | +! |
- self$set_style(style)+ class = "teal_validated", |
103 | -8x | +! |
- invisible(self)+ div( |
104 | -+ | ! |
- },+ class = "teal-output-warning", |
105 | -+ | ! |
-
+ "One of transformers failed. Please fix and continue." |
106 |
- #' @description Sets content of this `TealSlicesBlock`.+ ) |
||
107 |
- #' Sets content as `YAML` text which represents a list generated from `teal_slices`.+ ) |
||
108 |
- #' The list displays limited number of fields from `teal_slice` objects, but this list is+ ), |
||
109 | -+ | ! |
- #' sufficient to conclude which filters were applied.+ tags$div( |
110 | -+ | ! |
- #' When `selected` field in `teal_slice` object is a range, then it is displayed as a "min"+ id = ns("teal_module_ui"), |
111 | -+ | ! |
- #'+ do.call(modules$ui, args) |
112 |
- #'+ ) |
||
113 |
- #' @param content (`teal_slices`) object returned from [teal_slices()] function.+ ) |
||
114 |
- #' @return `self`, invisibly.+ |
||
115 | -+ | ! |
- set_content = function(content) {+ div( |
116 | -9x | +! |
- checkmate::assert_class(content, "teal_slices")+ id = id, |
117 | -8x | +! |
- if (length(content) != 0) {+ class = "teal_module", |
118 | -6x | +! |
- states_list <- lapply(content, function(x) {+ uiOutput(ns("data_reactive"), inline = TRUE), |
119 | -6x | +! |
- x_list <- shiny::isolate(as.list(x))+ tagList( |
120 | -6x | +! |
- if (+ if (depth >= 2L) tags$div(style = "mt-6"), |
121 | -6x | +! |
- inherits(x_list$choices, c("integer", "numeric", "Date", "POSIXct", "POSIXlt")) &&+ if (!is.null(modules$datanames)) { |
122 | -6x | +! |
- length(x_list$choices) == 2 &&+ fluidRow( |
123 | -6x | +! |
- length(x_list$selected) == 2+ column(width = 9, ui_teal, class = "teal_primary_col"), |
124 | -+ | ! |
- ) {+ column( |
125 | ! |
- x_list$range <- paste(x_list$selected, collapse = " - ")+ width = 3, |
|
126 | ! |
- x_list["selected"] <- NULL+ ui_data_summary(ns("data_summary")), |
|
127 | -+ | ! |
- }+ ui_filter_data(ns("filter_panel")), |
128 | -6x | +! |
- if (!is.null(x_list$arg)) {+ if (length(modules$transformers) > 0 && !isTRUE(attr(modules$transformers, "custom_ui"))) { |
129 | ! |
- x_list$arg <- if (x_list$arg == "subset") "Genes" else "Samples"+ ui_transform_data(ns("data_transform"), transforms = modules$transformers, class = "well") |
|
130 |
- }+ }, |
||
131 | -+ | ! |
-
+ class = "teal_secondary_col" |
132 | -6x | +
- x_list <- x_list[+ ) |
|
133 | -6x | +
- c("dataname", "varname", "experiment", "arg", "expr", "selected", "range", "keep_na", "keep_inf")+ ) |
|
134 |
- ]+ } else { |
||
135 | -6x | +! |
- names(x_list) <- c(+ div( |
136 | -6x | +! |
- "Dataset name", "Variable name", "Experiment", "Filtering by", "Applied expression",+ div( |
137 | -6x | +! |
- "Selected Values", "Selected range", "Include NA values", "Include Inf values"+ class = "teal_validated", |
138 | -+ | ! |
- )+ uiOutput(ns("data_input_error")) |
139 |
-
+ ), |
||
140 | -6x | +! |
- Filter(Negate(is.null), x_list)+ ui_teal |
141 |
- })+ ) |
||
142 |
-
+ } |
||
143 | -6x | +
- if (requireNamespace("yaml", quietly = TRUE)) {+ ) |
|
144 | -6x | +
- super$set_content(yaml::as.yaml(states_list))+ ) |
|
145 |
- } else {+ } |
||
146 | -! | +
- stop("yaml package is required to format the filter state list")+ |
|
147 |
- }+ #' @rdname module_teal_module |
||
148 |
- }+ srv_teal_module <- function(id, |
||
149 | -8x | +
- private$teal_slices <- content+ data_rv, |
|
150 | -8x | +
- invisible(self)+ modules, |
|
151 |
- },+ datasets = NULL, |
||
152 |
- #' @description Create the `TealSlicesBlock` from a list.+ slices_global, |
||
153 |
- #'+ reporter = teal.reporter::Reporter$new(), |
||
154 |
- #' @param x (`named list`) with two fields `text` and `style`.+ data_load_status = reactive("ok"), |
||
155 |
- #' Use the `get_available_styles` method to get all possible styles.+ is_active = reactive(TRUE)) { |
||
156 | -+ | 165x |
- #'+ checkmate::assert_string(id) |
157 | -+ | 165x |
- #' @return `self`, invisibly.+ assert_reactive(data_rv) |
158 | -+ | 165x |
- #' @examples+ checkmate::assert_multi_class(modules, c("teal_modules", "teal_module")) |
159 | -+ | 165x |
- #' TealSlicesBlock <- getFromNamespace("TealSlicesBlock", "teal")+ assert_reactive(datasets, null.ok = TRUE) |
160 | -+ | 165x |
- #' block <- TealSlicesBlock$new()+ checkmate::assert_class(slices_global, ".slicesGlobal") |
161 | -+ | 165x |
- #' block$from_list(list(text = "sth", style = "default"))+ checkmate::assert_class(reporter, "Reporter") |
162 | -+ | 165x |
- #'+ assert_reactive(data_load_status) |
163 | -+ | 165x |
- from_list = function(x) {+ UseMethod("srv_teal_module", modules) |
164 | -1x | +
- checkmate::assert_list(x)+ } |
|
165 | -1x | +
- checkmate::assert_names(names(x), must.include = c("text", "style"))+ |
|
166 | -1x | +
- super$set_content(x$text)+ #' @rdname module_teal_module |
|
167 | -1x | +
- super$set_style(x$style)+ #' @export |
|
168 | -1x | +
- invisible(self)+ srv_teal_module.default <- function(id, |
|
169 |
- },+ data_rv, |
||
170 |
- #' @description Convert the `TealSlicesBlock` to a list.+ modules, |
||
171 |
- #'+ datasets = NULL, |
||
172 |
- #' @return `named list` with a text and style.+ slices_global, |
||
173 |
- #' @examples+ reporter = teal.reporter::Reporter$new(), |
||
174 |
- #' TealSlicesBlock <- getFromNamespace("TealSlicesBlock", "teal")+ data_load_status = reactive("ok"), |
||
175 |
- #' block <- TealSlicesBlock$new()+ is_active = reactive(TRUE)) { |
||
176 | -+ | ! |
- #' block$to_list()+ stop("Modules class not supported: ", paste(class(modules), collapse = " ")) |
177 |
- #'+ } |
||
178 |
- to_list = function() {+ |
||
179 | -2x | +
- content <- self$get_content()+ #' @rdname module_teal_module |
|
180 | -2x | +
- list(+ #' @export |
|
181 | -2x | +
- text = if (length(content)) content else "",+ srv_teal_module.teal_modules <- function(id, |
|
182 | -2x | +
- style = self$get_style()+ data_rv, |
|
183 |
- )+ modules, |
||
184 |
- }+ datasets = NULL, |
||
185 |
- ),+ slices_global, |
||
186 |
- private = list(+ reporter = teal.reporter::Reporter$new(), |
||
187 |
- style = "verbatim",+ data_load_status = reactive("ok"), |
||
188 |
- teal_slices = NULL # teal_slices+ is_active = reactive(TRUE)) { |
||
189 | -+ | 70x |
- )+ moduleServer(id = id, module = function(input, output, session) { |
190 | -+ | 70x |
- )+ logger::log_debug("srv_teal_module.teal_modules initializing the module { deparse1(modules$label) }.") |
1 | +191 |
- #' Filter settings for `teal` applications+ |
||
2 | -+ | |||
192 | +70x |
- #'+ observeEvent(data_load_status(), { |
||
3 | -+ | |||
193 | +61x |
- #' Specify initial filter states and filtering settings for a `teal` app.+ tabs_selector <- sprintf("#%s li a", session$ns("active_tab")) |
||
4 | -+ | |||
194 | +61x |
- #'+ if (identical(data_load_status(), "ok")) { |
||
5 | -+ | |||
195 | +57x |
- #' Produces a `teal_slices` object.+ logger::log_debug("srv_teal_module@1 enabling modules tabs.") |
||
6 | -+ | |||
196 | +57x |
- #' The `teal_slice` components will specify filter states that will be active when the app starts.+ shinyjs::show("wrapper") |
||
7 | -+ | |||
197 | +57x |
- #' Attributes (created with the named arguments) will configure the way the app applies filters.+ shinyjs::enable(selector = tabs_selector) |
||
8 | -+ | |||
198 | +4x |
- #' See argument descriptions for details.+ } else if (identical(data_load_status(), "teal_data_module failed")) { |
||
9 | -+ | |||
199 | +4x |
- #'+ logger::log_debug("srv_teal_module@1 disabling modules tabs.") |
||
10 | -+ | |||
200 | +4x |
- #' @inheritParams teal.slice::teal_slices+ shinyjs::disable(selector = tabs_selector) |
||
11 | -+ | |||
201 | +! |
- #'+ } else if (identical(data_load_status(), "external failed")) { |
||
12 | -+ | |||
202 | +! |
- #' @param module_specific (`logical(1)`) optional,+ logger::log_debug("srv_teal_module@1 hiding modules tabs.") |
||
13 | -+ | |||
203 | +! |
- #' - `FALSE` (default) when one filter panel applied to all modules.+ shinyjs::hide("wrapper") |
||
14 | +204 |
- #' All filters will be shared by all modules.+ } |
||
15 | +205 |
- #' - `TRUE` when filter panel module-specific.+ }) |
||
16 | +206 |
- #' Modules can have different set of filters specified - see `mapping` argument.+ |
||
17 | -+ | |||
207 | +70x |
- #' @param mapping `r lifecycle::badge("experimental")`+ modules_output <- sapply( |
||
18 | -+ | |||
208 | +70x |
- #' _This is a new feature. Do kindly share your opinions on+ names(modules$children), |
||
19 | -+ | |||
209 | +70x |
- #' [`teal`'s GitHub repository](https://github.com/insightsengineering/teal/)._+ function(module_id) { |
||
20 | -+ | |||
210 | +95x |
- #'+ srv_teal_module( |
||
21 | -+ | |||
211 | +95x |
- #' (named `list`) specifies which filters will be active in which modules on app start.+ id = module_id, |
||
22 | -+ | |||
212 | +95x |
- #' Elements should contain character vector of `teal_slice` `id`s (see [`teal.slice::teal_slice`]).+ data_rv = data_rv, |
||
23 | -+ | |||
213 | +95x |
- #' Names of the list should correspond to `teal_module` `label` set in [module()] function.+ modules = modules$children[[module_id]], |
||
24 | -+ | |||
214 | +95x |
- #' - `id`s listed under `"global_filters` will be active in all modules.+ datasets = datasets, |
||
25 | -+ | |||
215 | +95x |
- #' - If missing, all filters will be applied to all modules.+ slices_global = slices_global, |
||
26 | -+ | |||
216 | +95x |
- #' - If empty list, all filters will be available to all modules but will start inactive.+ reporter = reporter, |
||
27 | -+ | |||
217 | +95x |
- #' - If `module_specific` is `FALSE`, only `global_filters` will be active on start.+ is_active = reactive(is_active() && input$active_tab == module_id) |
||
28 | +218 |
- #' @param app_id (`character(1)`)+ ) |
||
29 | +219 |
- #' For internal use only, do not set manually.+ }, |
||
30 | -+ | |||
220 | +70x |
- #' Added by `init` so that a `teal_slices` can be matched to the app in which it was used.+ simplify = FALSE |
||
31 | +221 |
- #' Used for verifying snapshots uploaded from file. See `snapshot`.+ ) |
||
32 | +222 |
- #'+ |
||
33 | -+ | |||
223 | +69x |
- #' @param x (`list`) of lists to convert to `teal_slices`+ modules_output |
||
34 | +224 |
- #'+ }) |
||
35 | +225 |
- #' @return+ } |
||
36 | +226 |
- #' A `teal_slices` object.+ |
||
37 | +227 |
- #'+ #' @rdname module_teal_module |
||
38 | +228 |
- #' @seealso [`teal.slice::teal_slices`], [`teal.slice::teal_slice`], [slices_store()]+ #' @export |
||
39 | +229 |
- #'+ srv_teal_module.teal_module <- function(id, |
||
40 | +230 |
- #' @examples+ data_rv, |
||
41 | +231 |
- #' filter <- teal_slices(+ modules, |
||
42 | +232 |
- #' teal_slice(dataname = "iris", varname = "Species", id = "species"),+ datasets = NULL, |
||
43 | +233 |
- #' teal_slice(dataname = "iris", varname = "Sepal.Length", id = "sepal_length"),+ slices_global, |
||
44 | +234 |
- #' teal_slice(+ reporter = teal.reporter::Reporter$new(), |
||
45 | +235 |
- #' dataname = "iris", id = "long_petals", title = "Long petals", expr = "Petal.Length > 5"+ data_load_status = reactive("ok"), |
||
46 | +236 |
- #' ),+ is_active = reactive(TRUE)) { |
||
47 | -+ | |||
237 | +95x |
- #' teal_slice(dataname = "mtcars", varname = "mpg", id = "mtcars_mpg"),+ logger::log_debug("srv_teal_module.teal_module initializing the module: { deparse1(modules$label) }.") |
||
48 | -+ | |||
238 | +95x |
- #' mapping = list(+ moduleServer(id = id, module = function(input, output, session) { |
||
49 | -+ | |||
239 | +95x |
- #' module1 = c("species", "sepal_length"),+ active_datanames <- reactive({ |
||
50 | -+ | |||
240 | +75x |
- #' module2 = c("mtcars_mpg"),+ .resolve_module_datanames(data = data_rv(), modules = modules) |
||
51 | +241 |
- #' global_filters = "long_petals"+ }) |
||
52 | -+ | |||
242 | +95x |
- #' )+ if (is.null(datasets)) { |
||
53 | -+ | |||
243 | +18x |
- #' )+ datasets <- eventReactive(data_rv(), { |
||
54 | -+ | |||
244 | +14x |
- #'+ req(inherits(data_rv(), "teal_data")) |
||
55 | -+ | |||
245 | +14x |
- #' app <- init(+ logger::log_debug("srv_teal_module@1 initializing module-specific FilteredData") |
||
56 | -+ | |||
246 | +14x |
- #' data = teal_data(iris = iris, mtcars = mtcars),+ teal_data_to_filtered_data(data_rv(), datanames = active_datanames()) |
||
57 | +247 |
- #' modules = list(+ }) |
||
58 | +248 |
- #' module("module1"),+ } |
||
59 | +249 |
- #' module("module2")+ |
||
60 | +250 |
- #' ),+ # manage module filters on the module level |
||
61 | +251 |
- #' filter = filter+ # important: |
||
62 | +252 |
- #' )+ # filter_manager_module_srv needs to be called before filter_panel_srv |
||
63 | +253 |
- #'+ # Because available_teal_slices is used in FilteredData$srv_available_slices (via srv_filter_panel) |
||
64 | +254 |
- #' if (interactive()) {+ # and if it is not set, then it won't be available in the srv_filter_panel |
||
65 | -+ | |||
255 | +95x |
- #' shinyApp(app$ui, app$server)+ srv_module_filter_manager(modules$label, module_fd = datasets, slices_global = slices_global) |
||
66 | -+ | |||
256 | +95x |
- #' }+ filtered_teal_data <- srv_filter_data( |
||
67 | -+ | |||
257 | +95x |
- #'+ "filter_panel", |
||
68 | -+ | |||
258 | +95x |
- #' @export+ datasets = datasets, |
||
69 | -+ | |||
259 | +95x |
- teal_slices <- function(...,+ active_datanames = active_datanames, |
||
70 | -+ | |||
260 | +95x |
- exclude_varnames = NULL,+ data_rv = data_rv, |
||
71 | -+ | |||
261 | +95x |
- include_varnames = NULL,+ is_active = is_active |
||
72 | +262 |
- count_type = NULL,+ ) |
||
73 | +263 |
- allow_add = TRUE,+ |
||
74 | -+ | |||
264 | +95x |
- module_specific = FALSE,+ is_transformer_failed <- reactiveValues() |
||
75 | -+ | |||
265 | +95x |
- mapping,+ transformed_teal_data <- srv_transform_data( |
||
76 | -+ | |||
266 | +95x |
- app_id = NULL) {+ "data_transform", |
||
77 | -147x | +267 | +95x |
- shiny::isolate({+ data = filtered_teal_data, |
78 | -147x | +268 | +95x |
- checkmate::assert_flag(allow_add)+ transforms = modules$transformers, |
79 | -147x | +269 | +95x |
- checkmate::assert_flag(module_specific)+ modules = modules, |
80 | -50x | -
- if (!missing(mapping)) checkmate::assert_list(mapping, types = c("character", "NULL"), names = "named")- |
- ||
81 | -144x | +270 | +95x |
- checkmate::assert_string(app_id, null.ok = TRUE)+ is_transformer_failed = is_transformer_failed |
82 | +271 |
-
+ ) |
||
83 | -144x | +272 | +94x |
- slices <- list(...)+ any_transformer_failed <- reactive({ |
84 | -144x | +273 | +80x |
- all_slice_id <- vapply(slices, `[[`, character(1L), "id")+ any(unlist(reactiveValuesToList(is_transformer_failed))) |
85 | +274 |
-
+ }) |
||
86 | -144x | +275 | +94x |
- if (missing(mapping)) {+ observeEvent(any_transformer_failed(), { |
87 | -97x | +276 | +80x |
- mapping <- if (length(all_slice_id)) {+ if (isTRUE(any_transformer_failed())) { |
88 | -26x | +277 | +4x |
- list(global_filters = all_slice_id)+ shinyjs::hide("teal_module_ui")+ |
+
278 | +4x | +
+ shinyjs::hide("validate_datanames")+ |
+ ||
279 | +4x | +
+ shinyjs::show("transformer_failure_info") |
||
89 | +280 |
} else { |
||
90 | -71x | +281 | +76x |
- list()+ shinyjs::show("teal_module_ui")+ |
+
282 | +76x | +
+ shinyjs::show("validate_datanames")+ |
+ ||
283 | +76x | +
+ shinyjs::hide("transformer_failure_info") |
||
91 | +284 |
} |
||
92 | +285 |
- }+ }) |
||
93 | +286 | |||
94 | -144x | +287 | +94x |
- if (!module_specific) {+ module_teal_data <- reactive({ |
95 | -127x | +288 | +101x |
- mapping[setdiff(names(mapping), "global_filters")] <- NULL+ req(inherits(transformed_teal_data(), "teal_data")) |
96 | -+ | |||
289 | +72x |
- }+ all_teal_data <- transformed_teal_data() |
||
97 | -+ | |||
290 | +72x |
-
+ module_datanames <- .resolve_module_datanames(data = all_teal_data, modules = modules) |
||
98 | -144x | +291 | +72x |
- failed_slice_id <- setdiff(unlist(mapping), all_slice_id)+ .subset_teal_data(all_teal_data, module_datanames) |
99 | -144x | +|||
292 | +
- if (length(failed_slice_id)) {+ }) |
|||
100 | -1x | +|||
293 | +
- stop(sprintf(+ |
|||
101 | -1x | +294 | +94x |
- "Filters in mapping don't match any available filter.\n %s not in %s",+ srv_validate_reactive_teal_data( |
102 | -1x | +295 | +94x |
- toString(failed_slice_id),+ "validate_datanames", |
103 | -1x | +296 | +94x |
- toString(all_slice_id)+ data = module_teal_data, |
104 | -+ | |||
297 | +94x |
- ))+ modules = modules |
||
105 | +298 |
- }+ ) |
||
106 | +299 | |||
107 | -143x | +300 | +94x |
- tss <- teal.slice::teal_slices(+ summary_table <- srv_data_summary("data_summary", module_teal_data) |
108 | +301 |
- ...,+ |
||
109 | -143x | +|||
302 | +
- exclude_varnames = exclude_varnames,+ # Call modules. |
|||
110 | -143x | +303 | +94x |
- include_varnames = include_varnames,+ module_out <- reactiveVal(NULL) |
111 | -143x | +304 | +94x |
- count_type = count_type,+ if (!inherits(modules, "teal_module_previewer")) { |
112 | -143x | +305 | +94x |
- allow_add = allow_add+ obs_module <- observeEvent( |
113 | +306 |
- )+ # wait for module_teal_data() to be not NULL but only once: |
||
114 | -143x | +307 | +94x |
- attr(tss, "mapping") <- mapping+ ignoreNULL = TRUE, |
115 | -143x | +308 | +94x |
- attr(tss, "module_specific") <- module_specific+ once = TRUE, |
116 | -143x | +309 | +94x |
- attr(tss, "app_id") <- app_id+ eventExpr = module_teal_data(), |
117 | -143x | +310 | +94x |
- class(tss) <- c("modules_teal_slices", class(tss))+ handlerExpr = { |
118 | -143x | +311 | +64x |
- tss+ module_out(.call_teal_module(modules, datasets, module_teal_data, reporter)) |
119 | +312 |
- })+ } |
||
120 | +313 |
- }+ ) |
||
121 | +314 |
-
+ } else { |
||
122 | +315 |
-
+ # Report previewer must be initiated on app start for report cards to be included in bookmarks. |
||
123 | +316 |
- #' @rdname teal_slices+ # When previewer is delayed, cards are bookmarked only if previewer has been initiated (visited). |
||
124 | -+ | |||
317 | +! |
- #' @export+ module_out(.call_teal_module(modules, datasets, module_teal_data, reporter)) |
||
125 | +318 |
- #' @keywords internal+ } |
||
126 | +319 |
- #'+ |
||
127 | +320 |
- as.teal_slices <- function(x) { # nolint: object_name.- |
- ||
128 | -12x | -
- checkmate::assert_list(x)- |
- ||
129 | -12x | -
- lapply(x, checkmate::assert_list, names = "named", .var.name = "list element")+ # todo: (feature request) add a ReporterCard to the reporter as an output from the teal_module |
||
130 | +321 |
-
+ # how to determine if module returns a ReporterCard so that reportPreviewer is needed? |
||
131 | -12x | +|||
322 | +
- attrs <- attributes(unclass(x))+ # Should we insertUI of the ReportPreviewer then? |
|||
132 | -12x | +|||
323 | +
- ans <- lapply(x, function(x) if (is.teal_slice(x)) x else as.teal_slice(x))+ # What about attr(module, "reportable") - similar to attr(module, "bookmarkable") |
|||
133 | -12x | +324 | +94x |
- do.call(teal_slices, c(ans, attrs))+ if ("report" %in% names(module_out)) { |
134 | +325 |
- }+ # (reactively) add card to the reporter |
||
135 | +326 |
-
+ } |
||
136 | +327 | |||
137 | -+ | |||
328 | +94x |
- #' @rdname teal_slices+ module_out |
||
138 | +329 |
- #' @export+ }) |
||
139 | +330 |
- #' @keywords internal+ } |
||
140 | +331 |
- #'+ |
||
141 | +332 |
- c.teal_slices <- function(...) {- |
- ||
142 | -6x | -
- x <- list(...)+ # This function calls a module server function. |
||
143 | -6x | +|||
333 | +
- checkmate::assert_true(all(vapply(x, is.teal_slices, logical(1L))), .var.name = "all arguments are teal_slices")+ .call_teal_module <- function(modules, datasets, filtered_teal_data, reporter) { |
|||
144 | +334 |
-
+ # collect arguments to run teal_module |
||
145 | -6x | +335 | +64x |
- all_attributes <- lapply(x, attributes)+ args <- c(list(id = "module"), modules$server_args) |
146 | -6x | +336 | +64x |
- all_attributes <- coalesce_r(all_attributes)+ if (is_arg_used(modules$server, "reporter")) { |
147 | -6x | +337 | +1x |
- all_attributes <- all_attributes[names(all_attributes) != "class"]+ args <- c(args, list(reporter = reporter)) |
148 | +338 |
-
+ } |
||
149 | -6x | +|||
339 | +
- do.call(+ |
|||
150 | -6x | +340 | +64x |
- teal_slices,+ if (is_arg_used(modules$server, "datasets")) { |
151 | -6x | +341 | +1x |
- c(+ args <- c(args, datasets = datasets()) |
152 | -6x | +342 | +1x |
- unique(unlist(x, recursive = FALSE)),+ warning("datasets argument is not reactive and therefore it won't be updated when data is refreshed.") |
153 | -6x | +|||
343 | +
- all_attributes+ } |
|||
154 | +344 |
- )+ |
||
155 | -+ | |||
345 | +64x |
- )+ if (is_arg_used(modules$server, "data")) { |
||
156 | -+ | |||
346 | +60x |
- }+ args <- c(args, data = list(filtered_teal_data)) |
||
157 | +347 |
-
+ } |
||
158 | +348 | |||
159 | -+ | |||
349 | +64x |
- #' Deep copy `teal_slices`+ if (is_arg_used(modules$server, "filter_panel_api")) { |
||
160 | -+ | |||
350 | +1x |
- #'+ args <- c(args, filter_panel_api = teal.slice::FilterPanelAPI$new(datasets())) |
||
161 | +351 |
- #' it's important to create a new copy of `teal_slices` when+ } |
||
162 | +352 |
- #' starting a new `shiny` session. Otherwise, object will be shared+ |
||
163 | -+ | |||
353 | +64x |
- #' by multiple users as it is created in global environment before+ if (is_arg_used(modules$server, "id")) {+ |
+ ||
354 | +64x | +
+ do.call(modules$server, args) |
||
164 | +355 |
- #' `shiny` session starts.+ } else {+ |
+ ||
356 | +! | +
+ do.call(callModule, c(args, list(module = modules$server))) |
||
165 | +357 |
- #' @param filter (`teal_slices`)+ } |
||
166 | +358 |
- #' @return `teal_slices`+ } |
||
167 | +359 |
- #' @keywords internal+ |
||
168 | +360 |
- deep_copy_filter <- function(filter) {+ .resolve_module_datanames <- function(data, modules) { |
||
169 | -1x | +361 | +147x |
- checkmate::assert_class(filter, "teal_slices")+ stopifnot("data_rv must be teal_data object." = inherits(data, "teal_data")) |
170 | -1x | +362 | +147x |
- shiny::isolate({+ if (is.null(modules$datanames) || identical(modules$datanames, "all")) { |
171 | -1x | +363 | +125x |
- filter_copy <- lapply(filter, function(slice) {+ .topologically_sort_datanames(ls(teal.code::get_env(data)), teal.data::join_keys(data)) |
172 | -2x | +|||
364 | +
- teal.slice::as.teal_slice(as.list(slice))+ } else { |
|||
173 | -+ | |||
365 | +22x |
- })+ intersect( |
||
174 | -1x | +366 | +22x |
- attributes(filter_copy) <- attributes(filter)+ .include_parent_datanames(modules$datanames, teal.data::join_keys(data)), |
175 | -1x | +367 | +22x |
- filter_copy+ ls(teal.code::get_env(data)) |
176 | +368 |
- })+ ) |
||
177 | +369 | ++ |
+ }+ |
+ |
370 |
}@@ -21298,11198 +20774,11829 @@ teal coverage - 57.64% |
1 |
- setOldClass("teal_module")+ #' @title `TealReportCard` |
||
2 |
- setOldClass("teal_modules")+ #' @description `r lifecycle::badge("experimental")` |
||
3 |
-
+ #' Child class of [`ReportCard`] that is used for `teal` specific applications. |
||
4 |
- #' Create `teal_module` and `teal_modules` objects+ #' In addition to the parent methods, it supports rendering `teal` specific elements such as |
||
5 |
- #'+ #' the source code, the encodings panel content and the filter panel content as part of the |
||
6 |
- #' @description+ #' meta data. |
||
7 |
- #' `r lifecycle::badge("stable")`+ #' @export |
||
8 |
- #' Create a nested tab structure to embed modules in a `teal` application.+ #' |
||
9 |
- #'+ TealReportCard <- R6::R6Class( # nolint: object_name. |
||
10 |
- #' @details+ classname = "TealReportCard", |
||
11 |
- #' `module()` creates an instance of a `teal_module` that can be placed in a `teal` application.+ inherit = teal.reporter::ReportCard, |
||
12 |
- #' `modules()` shapes the structure of a the application by organizing `teal_module` within the navigation panel.+ public = list( |
||
13 |
- #' It wraps `teal_module` and `teal_modules` objects in a `teal_modules` object,+ #' @description Appends the source code to the `content` meta data of this `TealReportCard`. |
||
14 |
- #' which results in a nested structure corresponding to the nested tabs in the final application.+ #' |
||
15 |
- #'+ #' @param src (`character(1)`) code as text. |
||
16 |
- #' Note that for `modules()` `label` comes after `...`, so it must be passed as a named argument,+ #' @param ... any `rmarkdown` `R` chunk parameter and its value. |
||
17 |
- #' otherwise it will be captured by `...`.+ #' But `eval` parameter is always set to `FALSE`. |
||
18 |
- #'+ #' @return Object of class `TealReportCard`, invisibly. |
||
19 |
- #' The labels `"global_filters"` and `"Report previewer"` are reserved+ #' @examples |
||
20 |
- #' because they are used by the `mapping` argument of [teal_slices()]+ #' card <- TealReportCard$new()$append_src( |
||
21 |
- #' and the report previewer module [reporter_previewer_module()], respectively.+ #' "plot(iris)" |
||
22 |
- #'+ #' ) |
||
23 |
- #' @param label (`character(1)`) Label shown in the navigation item for the module or module group.+ #' card$get_content()[[1]]$get_content() |
||
24 |
- #' For `modules()` defaults to `"root"`. See `Details`.+ append_src = function(src, ...) { |
||
25 | -+ | 4x |
- #' @param server (`function`) `shiny` module with following arguments:+ checkmate::assert_character(src, min.len = 0, max.len = 1) |
26 | -+ | 4x |
- #' - `id` - `teal` will set proper `shiny` namespace for this module (see [shiny::moduleServer()]).+ params <- list(...) |
27 | -+ | 4x |
- #' - `input`, `output`, `session` - (optional; not recommended) When provided, then [shiny::callModule()]+ params$eval <- FALSE |
28 | -+ | 4x |
- #' will be used to call a module. From `shiny` 1.5.0, the recommended way is to use+ rblock <- RcodeBlock$new(src) |
29 | -+ | 4x |
- #' [shiny::moduleServer()] instead which doesn't require these arguments.+ rblock$set_params(params) |
30 | -+ | 4x |
- #' - `data` (optional) When provided, the module will be called with `teal_data` object (i.e. a list of+ self$append_content(rblock) |
31 | -+ | 4x |
- #' reactive (filtered) data specified in the `filters` argument) as the value of this argument.+ self$append_metadata("SRC", src) |
32 | -+ | 4x |
- #' - `datasets` (optional) When provided, the module will be called with `FilteredData` object as the+ invisible(self) |
33 |
- #' value of this argument. (See [`teal.slice::FilteredData`]).+ }, |
||
34 |
- #' - `reporter` (optional) When provided, the module will be called with `Reporter` object as the value+ #' @description Appends the filter state list to the `content` and `metadata` of this `TealReportCard`. |
||
35 |
- #' of this argument. (See [`teal.reporter::Reporter`]).+ #' If the filter state list has an attribute named `formatted`, it appends it to the card otherwise it uses |
||
36 |
- #' - `filter_panel_api` (optional) When provided, the module will be called with `FilterPanelAPI` object+ #' the default `yaml::as.yaml` to format the list. |
||
37 |
- #' as the value of this argument. (See [`teal.slice::FilterPanelAPI`]).+ #' If the filter state list is empty, nothing is appended to the `content`. |
||
38 |
- #' - `...` (optional) When provided, `server_args` elements will be passed to the module named argument+ #' |
||
39 |
- #' or to the `...`.+ #' @param fs (`teal_slices`) object returned from [teal_slices()] function. |
||
40 |
- #' @param ui (`function`) `shiny` UI module function with following arguments:+ #' @return `self`, invisibly. |
||
41 |
- #' - `id` - `teal` will set proper `shiny` namespace for this module.+ append_fs = function(fs) { |
||
42 | -+ | 5x |
- #' - `...` (optional) When provided, `ui_args` elements will be passed to the module named argument+ checkmate::assert_class(fs, "teal_slices") |
43 | -+ | 4x |
- #' or to the `...`.+ self$append_text("Filter State", "header3") |
44 | -+ | 4x |
- #' @param filters (`character`) Deprecated. Use `datanames` instead.+ if (length(fs)) { |
45 | -+ | 3x |
- #' @param datanames (`character`) Names of the datasets that are relevant for the item.+ self$append_content(TealSlicesBlock$new(fs)) |
46 |
- #' The keyword `"all"` provides all datasets available in `data` passed to `teal` application.+ } else { |
||
47 | -+ | 1x |
- #' `NULL` will hide the filter panel.+ self$append_text("No filters specified.") |
48 |
- #' @param server_args (named `list`) with additional arguments passed on to the server function.+ } |
||
49 | -+ | 4x |
- #' @param ui_args (named `list`) with additional arguments passed on to the UI function.+ invisible(self) |
50 |
- #' @param x (`teal_module` or `teal_modules`) Object to format/print.+ }, |
||
51 |
- #' @param indent (`integer(1)`) Indention level; each nested element is indented one level more.+ #' @description Appends the encodings list to the `content` and `metadata` of this `TealReportCard`. |
||
52 |
- #' @param transformers (`list` of `teal_data_module`) that will be applied to transform the data.+ #' |
||
53 |
- #' Each transform module UI will appear in the `teal` application, unless the `custom_ui` attribute is set on the list.+ #' @param encodings (`list`) list of encodings selections of the `teal` app. |
||
54 |
- #' If so, the module developer is responsible to display the UI in the module itself. `datanames` of the `transformers`+ #' @return `self`, invisibly. |
||
55 |
- #' will be added to the `datanames`.+ #' @examples |
||
56 |
- #'+ #' card <- TealReportCard$new()$append_encodings(list(variable1 = "X")) |
||
57 |
- #' When the transformation does not have sufficient input data, the resulting data will fallback+ #' card$get_content()[[1]]$get_content() |
||
58 |
- #' to the last successful transform or, in case there are none, to the filtered data.+ #' |
||
59 |
- #' @param ...+ append_encodings = function(encodings) { |
||
60 | -+ | 4x |
- #' - For `modules()`: (`teal_module` or `teal_modules`) Objects to wrap into a tab.+ checkmate::assert_list(encodings) |
61 | -+ | 4x |
- #' - For `format()` and `print()`: Arguments passed to other methods.+ self$append_text("Selected Options", "header3") |
62 | -+ | 4x |
- #'+ if (requireNamespace("yaml", quietly = TRUE)) { |
63 | -+ | 4x |
- #' @section `datanames`:+ self$append_text(yaml::as.yaml(encodings, handlers = list( |
64 | -+ | 4x |
- #' The module's `datanames` argument determines a subset of datasets from the `data` object, as specified in the+ POSIXct = function(x) format(x, "%Y-%m-%d"), |
65 | -+ | 4x |
- #' server function argument, to be presented in the module. Datasets displayed in the filter panel will be limited+ POSIXlt = function(x) format(x, "%Y-%m-%d"), |
66 | -+ | 4x |
- #' to this subset.+ Date = function(x) format(x, "%Y-%m-%d") |
67 | -+ | 4x |
- #' When `datanames` is set to `"all"`, all available datasets in the `data` object are considered relevant for the+ )), "verbatim") |
68 |
- #' module. However, setting `datanames` argument to `"all"` might include datasets that are irrelevant for the module,+ } else { |
||
69 | -+ | ! |
- #' for example:+ stop("yaml package is required to format the encodings list") |
70 |
- #' - Proxy variables used for modifying columns.+ } |
||
71 | -+ | 4x |
- #' - Modified copies of datasets used to create a final dataset.+ self$append_metadata("Encodings", encodings) |
72 | -+ | 4x |
- #' - Connection objects.+ invisible(self) |
73 |
- #' To prevent these irrelevant datasets from appearing in the module, use the [set_datanames()] function on the+ } |
||
74 |
- #' [module] or [modules()] to change the `datanames` from `"all"` to specific dataset names. Attempting to change+ ), |
||
75 |
- #' `datanames` values that was not set to `"all"` using [set_datanames()] will be ignored with a warning.+ private = list( |
||
76 |
- #'+ dispatch_block = function(block_class) { |
||
77 | -+ | ! |
- #' Additionally, datasets with names starting with `.` are ignored when `datanames` is set to `"all"`.+ eval(str2lang(block_class)) |
78 |
- #'+ } |
||
79 |
- #' @return+ ) |
||
80 |
- #' `module()` returns an object of class `teal_module`.+ ) |
||
81 |
- #'+ |
||
82 |
- #' `modules()` returns a `teal_modules` object which contains following fields:+ #' @title `TealSlicesBlock` |
||
83 |
- #' - `label`: taken from the `label` argument.+ #' @docType class |
||
84 |
- #' - `children`: a list containing objects passed in `...`. List elements are named after+ #' @description |
||
85 |
- #' their `label` attribute converted to a valid `shiny` id.+ #' Specialized `TealSlicesBlock` block for managing filter panel content in reports. |
||
86 |
- #'+ #' @keywords internal |
||
87 |
- #' @name teal_modules+ TealSlicesBlock <- R6::R6Class( # nolint: object_name_linter. |
||
88 |
- #' @aliases teal_module+ classname = "TealSlicesBlock", |
||
89 |
- #'+ inherit = teal.reporter:::TextBlock, |
||
90 |
- #' @examples+ public = list( |
||
91 |
- #' library(shiny)+ #' @description Returns a `TealSlicesBlock` object. |
||
92 |
- #'+ #' |
||
93 |
- #' module_1 <- module(+ #' @details Returns a `TealSlicesBlock` object with no content and no parameters. |
||
94 |
- #' label = "a module",+ #' |
||
95 |
- #' server = function(id, data) {+ #' @param content (`teal_slices`) object returned from [teal_slices()] function. |
||
96 |
- #' moduleServer(+ #' @param style (`character(1)`) string specifying style to apply. |
||
97 |
- #' id,+ #' |
||
98 |
- #' module = function(input, output, session) {+ #' @return Object of class `TealSlicesBlock`, invisibly. |
||
99 |
- #' output$data <- renderDataTable(data()[["iris"]])+ #' |
||
100 |
- #' }+ initialize = function(content = teal_slices(), style = "verbatim") { |
||
101 | -+ | 9x |
- #' )+ self$set_content(content) |
102 | -+ | 8x |
- #' },+ self$set_style(style) |
103 | -+ | 8x |
- #' ui = function(id) {+ invisible(self) |
104 |
- #' ns <- NS(id)+ }, |
||
105 |
- #' tagList(dataTableOutput(ns("data")))+ |
||
106 |
- #' },+ #' @description Sets content of this `TealSlicesBlock`. |
||
107 |
- #' datanames = "all"+ #' Sets content as `YAML` text which represents a list generated from `teal_slices`. |
||
108 |
- #' )+ #' The list displays limited number of fields from `teal_slice` objects, but this list is |
||
109 |
- #'+ #' sufficient to conclude which filters were applied. |
||
110 |
- #' module_2 <- module(+ #' When `selected` field in `teal_slice` object is a range, then it is displayed as a "min" |
||
111 |
- #' label = "another module",+ #' |
||
112 |
- #' server = function(id) {+ #' |
||
113 |
- #' moduleServer(+ #' @param content (`teal_slices`) object returned from [teal_slices()] function. |
||
114 |
- #' id,+ #' @return `self`, invisibly. |
||
115 |
- #' module = function(input, output, session) {+ set_content = function(content) { |
||
116 | -+ | 9x |
- #' output$text <- renderText("Another Module")+ checkmate::assert_class(content, "teal_slices") |
117 | -+ | 8x |
- #' }+ if (length(content) != 0) { |
118 | -+ | 6x |
- #' )+ states_list <- lapply(content, function(x) { |
119 | -+ | 6x |
- #' },+ x_list <- shiny::isolate(as.list(x)) |
120 | -+ | 6x |
- #' ui = function(id) {+ if ( |
121 | -+ | 6x |
- #' ns <- NS(id)+ inherits(x_list$choices, c("integer", "numeric", "Date", "POSIXct", "POSIXlt")) && |
122 | -+ | 6x |
- #' tagList(textOutput(ns("text")))+ length(x_list$choices) == 2 && |
123 | -+ | 6x |
- #' },+ length(x_list$selected) == 2 |
124 |
- #' datanames = NULL+ ) { |
||
125 | -+ | ! |
- #' )+ x_list$range <- paste(x_list$selected, collapse = " - ") |
126 | -+ | ! |
- #'+ x_list["selected"] <- NULL |
127 |
- #' modules <- modules(+ } |
||
128 | -+ | 6x |
- #' label = "modules",+ if (!is.null(x_list$arg)) { |
129 | -+ | ! |
- #' modules(+ x_list$arg <- if (x_list$arg == "subset") "Genes" else "Samples" |
130 |
- #' label = "nested modules",+ } |
||
131 |
- #' module_1+ |
||
132 | -+ | 6x |
- #' ),+ x_list <- x_list[ |
133 | -+ | 6x |
- #' module_2+ c("dataname", "varname", "experiment", "arg", "expr", "selected", "range", "keep_na", "keep_inf") |
134 |
- #' )+ ] |
||
135 | -+ | 6x |
- #'+ names(x_list) <- c( |
136 | -+ | 6x |
- #' app <- init(+ "Dataset name", "Variable name", "Experiment", "Filtering by", "Applied expression", |
137 | -+ | 6x |
- #' data = teal_data(iris = iris),+ "Selected Values", "Selected range", "Include NA values", "Include Inf values" |
138 |
- #' modules = modules+ ) |
||
139 |
- #' )+ |
||
140 | -+ | 6x |
- #'+ Filter(Negate(is.null), x_list) |
141 |
- #' if (interactive()) {+ }) |
||
142 |
- #' shinyApp(app$ui, app$server)+ |
||
143 | -+ | 6x |
- #' }+ if (requireNamespace("yaml", quietly = TRUE)) { |
144 | -+ | 6x |
- #' @rdname teal_modules+ super$set_content(yaml::as.yaml(states_list)) |
145 |
- #' @export+ } else { |
||
146 | -+ | ! |
- #'+ stop("yaml package is required to format the filter state list") |
147 |
- module <- function(label = "module",+ } |
||
148 |
- server = function(id, data, ...) moduleServer(id, function(input, output, session) NULL),+ } |
||
149 | -+ | 8x |
- ui = function(id, ...) tags$p(paste0("This module has no UI (id: ", id, " )")),+ private$teal_slices <- content |
150 | -+ | 8x |
- filters,+ invisible(self) |
151 |
- datanames = "all",+ }, |
||
152 |
- server_args = NULL,+ #' @description Create the `TealSlicesBlock` from a list. |
||
153 |
- ui_args = NULL,+ #' |
||
154 |
- transformers = list()) {+ #' @param x (`named list`) with two fields `text` and `style`. |
||
155 |
- # argument checking (independent)+ #' Use the `get_available_styles` method to get all possible styles. |
||
156 |
- ## `label`+ #' |
||
157 | -201x | +
- checkmate::assert_string(label)+ #' @return `self`, invisibly. |
|
158 | -198x | +
- if (label == "global_filters") {+ #' @examples |
|
159 | -1x | +
- stop(+ #' TealSlicesBlock <- getFromNamespace("TealSlicesBlock", "teal") |
|
160 | -1x | +
- sprintf("module(label = \"%s\", ...\n ", label),+ #' block <- TealSlicesBlock$new() |
|
161 | -1x | +
- "Label 'global_filters' is reserved in teal. Please change to something else.",+ #' block$from_list(list(text = "sth", style = "default")) |
|
162 | -1x | +
- call. = FALSE+ #' |
|
163 |
- )+ from_list = function(x) { |
||
164 | -+ | 1x |
- }+ checkmate::assert_list(x) |
165 | -197x | +1x |
- if (label == "Report previewer") {+ checkmate::assert_names(names(x), must.include = c("text", "style")) |
166 | -! | +1x |
- stop(+ super$set_content(x$text) |
167 | -! | +1x |
- sprintf("module(label = \"%s\", ...\n ", label),+ super$set_style(x$style) |
168 | -! | +1x |
- "Label 'Report previewer' is reserved in teal. Please change to something else.",+ invisible(self) |
169 | -! | +
- call. = FALSE+ }, |
|
170 |
- )+ #' @description Convert the `TealSlicesBlock` to a list. |
||
171 |
- }+ #' |
||
172 |
-
+ #' @return `named list` with a text and style. |
||
173 |
- ## server+ #' @examples |
||
174 | -197x | +
- checkmate::assert_function(server)+ #' TealSlicesBlock <- getFromNamespace("TealSlicesBlock", "teal") |
|
175 | -197x | +
- server_formals <- names(formals(server))+ #' block <- TealSlicesBlock$new() |
|
176 | -197x | +
- if (!(+ #' block$to_list() |
|
177 | -197x | +
- "id" %in% server_formals ||+ #' |
|
178 | -197x | +
- all(c("input", "output", "session") %in% server_formals)+ to_list = function() { |
|
179 | -+ | 2x |
- )) {+ content <- self$get_content() |
180 | 2x |
- stop(+ list( |
|
181 | 2x |
- "\nmodule() `server` argument requires a function with following arguments:",+ text = if (length(content)) content else "", |
|
182 | 2x |
- "\n - id - `teal` will set proper `shiny` namespace for this module.",+ style = self$get_style() |
|
183 | -2x | +
- "\n - input, output, session (not recommended) - then `shiny::callModule` will be used to call a module.",+ ) |
|
184 | -2x | +
- "\n\nFollowing arguments can be used optionaly:",+ } |
|
185 | -2x | +
- "\n - `data` - module will receive list of reactive (filtered) data specified in the `filters` argument",+ ), |
|
186 | -2x | +
- "\n - `datasets` - module will receive `FilteredData`. See `help(teal.slice::FilteredData)`",+ private = list( |
|
187 | -2x | +
- "\n - `reporter` - module will receive `Reporter`. See `help(teal.reporter::Reporter)`",+ style = "verbatim", |
|
188 | -2x | +
- "\n - `filter_panel_api` - module will receive `FilterPanelAPI`. (See [teal.slice::FilterPanelAPI]).",+ teal_slices = NULL # teal_slices |
|
189 | -2x | +
- "\n - `...` server_args elements will be passed to the module named argument or to the `...`"+ ) |
|
190 |
- )+ ) |
191 | +1 |
- }+ #' Get client timezone |
||
192 | -195x | +|||
2 | +
- if ("datasets" %in% server_formals) {+ #' |
|||
193 | -2x | +|||
3 | +
- warning(+ #' User timezone in the browser may be different to the one on the server. |
|||
194 | -2x | +|||
4 | +
- sprintf("Called from module(label = \"%s\", ...)\n ", label),+ #' This script can be run to register a `shiny` input which contains information about the timezone in the browser. |
|||
195 | -2x | +|||
5 | +
- "`datasets` argument in the server is deprecated and will be removed in the next release. ",+ #' |
|||
196 | -2x | +|||
6 | +
- "Please use `data` instead.",+ #' @param ns (`function`) namespace function passed from the `session` object in the `shiny` server. |
|||
197 | -2x | +|||
7 | +
- call. = FALSE+ #' For `shiny` modules this will allow for proper name spacing of the registered input. |
|||
198 | +8 |
- )+ #' |
||
199 | +9 |
- }+ #' @return `NULL`, invisibly. |
||
200 | +10 |
-
+ #' |
||
201 | +11 |
-
+ #' @keywords internal |
||
202 | +12 |
- ## UI+ #' |
||
203 | -195x | +|||
13 | +
- checkmate::assert_function(ui)+ get_client_timezone <- function(ns) { |
|||
204 | -195x | +14 | +71x |
- ui_formals <- names(formals(ui))+ script <- sprintf( |
205 | -195x | +15 | +71x |
- if (!"id" %in% ui_formals) {+ "Shiny.setInputValue(`%s`, Intl.DateTimeFormat().resolvedOptions().timeZone)", |
206 | -1x | +16 | +71x |
- stop(+ ns("timezone") |
207 | -1x | +|||
17 | +
- "\nmodule() `ui` argument requires a function with following arguments:",+ ) |
|||
208 | -1x | +18 | +71x |
- "\n - id - `teal` will set proper `shiny` namespace for this module.",+ shinyjs::runjs(script) # function does not return anything |
209 | -1x | +19 | +71x |
- "\n\nFollowing arguments can be used optionally:",+ invisible(NULL) |
210 | -1x | +|||
20 | +
- "\n - `...` ui_args elements will be passed to the module argument of the same name or to the `...`"+ } |
|||
211 | +21 |
- )+ |
||
212 | +22 |
- }+ #' Resolve the expected bootstrap theme |
||
213 | -194x | +|||
23 | +
- if (any(c("data", "datasets") %in% ui_formals)) {+ #' @noRd |
|||
214 | -2x | +|||
24 | +
- stop(+ #' @keywords internal |
|||
215 | -2x | +|||
25 | +
- sprintf("Called from module(label = \"%s\", ...)\n ", label),+ get_teal_bs_theme <- function() { |
|||
216 | -2x | +26 | +4x |
- "UI with `data` or `datasets` argument is no longer accepted.\n ",+ bs_theme <- getOption("teal.bs_theme") |
217 | -2x | +|||
27 | +
- "If some UI inputs depend on data, please move the logic to your server instead.\n ",+ |
|||
218 | -2x | +28 | +4x |
- "Possible solutions are renderUI() or updateXyzInput() functions."+ if (is.null(bs_theme)) { |
219 | -+ | |||
29 | +1x |
- )+ return(NULL) |
||
220 | +30 |
} |
||
221 | +31 | |||
222 | -+ | |||
32 | +3x |
-
+ if (!checkmate::test_class(bs_theme, "bs_theme")) { |
||
223 | -+ | |||
33 | +2x |
- ## `filters`+ warning( |
||
224 | -192x | +34 | +2x |
- if (!missing(filters)) {+ "Assertion on 'teal.bs_theme' option value failed: ", |
225 | -! | +|||
35 | +2x |
- datanames <- filters+ checkmate::check_class(bs_theme, "bs_theme"), |
||
226 | -! | +|||
36 | +2x |
- msg <-+ ". The default Shiny Bootstrap theme will be used." |
||
227 | -! | +|||
37 | +
- "The `filters` argument is deprecated and will be removed in the next release. Please use `datanames` instead."+ ) |
|||
228 | -! | +|||
38 | +2x |
- warning(msg)+ return(NULL) |
||
229 | +39 |
} |
||
230 | +40 | |||
41 | +1x | +
+ bs_theme+ |
+ ||
231 | +42 |
- ## `datanames` (also including deprecated `filters`)+ } |
||
232 | +43 |
- # please note a race condition between datanames set when filters is not missing and data arg in server function+ |
||
233 | -192x | +|||
44 | +
- if (!is.element("data", server_formals) && !is.null(datanames)) {- |
- |||
234 | -12x | -
- message(sprintf("module \"%s\" server function takes no data so \"datanames\" will be ignored", label))- |
- ||
235 | -12x | -
- datanames <- NULL+ #' Return parentnames along with datanames. |
||
236 | +45 |
- }- |
- ||
237 | -192x | -
- checkmate::assert_character(datanames, min.len = 1, null.ok = TRUE, any.missing = FALSE)+ #' @noRd |
||
238 | +46 |
-
+ #' @keywords internal |
||
239 | +47 |
- ## `server_args`+ .include_parent_datanames <- function(datanames, join_keys) { |
||
240 | -191x | +48 | +147x |
- checkmate::assert_list(server_args, null.ok = TRUE, names = "named")+ ordered_datanames <- datanames |
241 | -189x | +49 | +147x |
- srv_extra_args <- setdiff(names(server_args), server_formals)+ for (i in datanames) { |
242 | -189x | +50 | +264x |
- if (length(srv_extra_args) > 0 && !"..." %in% server_formals) {+ parents <- character(0) |
243 | -1x | +51 | +264x |
- stop(+ while (length(i) > 0) { |
244 | -1x | +52 | +277x |
- "\nFollowing `server_args` elements have no equivalent in the formals of the server:\n",+ parent_i <- teal.data::parent(join_keys, i) |
245 | -1x | +53 | +277x |
- paste(paste(" -", srv_extra_args), collapse = "\n"),+ parents <- c(parent_i, parents) |
246 | -1x | -
- "\n\nUpdate the server arguments by including above or add `...`"- |
- ||
247 | -+ | 54 | +277x |
- )+ i <- parent_i |
248 | +55 |
- }+ } |
||
249 | -+ | |||
56 | +264x |
-
+ ordered_datanames <- c(parents, ordered_datanames) |
||
250 | +57 |
- ## `ui_args`- |
- ||
251 | -188x | -
- checkmate::assert_list(ui_args, null.ok = TRUE, names = "named")- |
- ||
252 | -186x | -
- ui_extra_args <- setdiff(names(ui_args), ui_formals)- |
- ||
253 | -186x | -
- if (length(ui_extra_args) > 0 && !"..." %in% ui_formals) {- |
- ||
254 | -1x | -
- stop(+ } |
||
255 | -1x | +58 | +147x |
- "\nFollowing `ui_args` elements have no equivalent in the formals of UI:\n",+ unique(ordered_datanames) |
256 | -1x | +|||
59 | +
- paste(paste(" -", ui_extra_args), collapse = "\n"),+ } |
|||
257 | -1x | +|||
60 | +
- "\n\nUpdate the UI arguments by including above or add `...`"+ |
|||
258 | +61 |
- )+ #' Return topologicaly sorted datanames |
||
259 | +62 |
- }+ #' @noRd |
||
260 | +63 |
-
+ #' @keywords internal |
||
261 | +64 |
- ## `transformers`+ .topologically_sort_datanames <- function(datanames, join_keys) { |
||
262 | -185x | +65 | +125x |
- if (inherits(transformers, "teal_transform_module")) {+ datanames_with_parents <- .include_parent_datanames(datanames, join_keys) |
263 | -1x | +66 | +125x |
- transformers <- list(transformers)+ intersect(datanames, datanames_with_parents) |
264 | +67 |
- }+ } |
||
265 | -185x | +|||
68 | +
- checkmate::assert_list(transformers, types = "teal_transform_module")+ |
|||
266 | -185x | +|||
69 | +
- transformer_datanames <- unlist(lapply(transformers, attr, "datanames"))+ #' Create a `FilteredData` |
|||
267 | -185x | +|||
70 | +
- combined_datanames <- if (identical(datanames, "all") || any(sapply(transformer_datanames, identical, "all"))) {+ #' |
|||
268 | -139x | +|||
71 | +
- "all"+ #' Create a `FilteredData` object from a `teal_data` object. |
|||
269 | +72 |
- } else {+ #' |
||
270 | -46x | +|||
73 | +
- union(datanames, transformer_datanames)+ #' @param x (`teal_data`) object |
|||
271 | +74 |
- }+ #' @param datanames (`character`) vector of data set names to include; must be subset of `datanames(x)` |
||
272 | +75 |
-
+ #' @return A `FilteredData` object. |
||
273 | -185x | +|||
76 | +
- structure(+ #' @keywords internal |
|||
274 | -185x | +|||
77 | +
- list(+ teal_data_to_filtered_data <- function(x, datanames = ls(teal.code::get_env(x))) { |
|||
275 | -185x | +78 | +65x |
- label = label,+ checkmate::assert_class(x, "teal_data") |
276 | -185x | +79 | +65x |
- server = server,+ checkmate::assert_character(datanames, min.chars = 1L, any.missing = FALSE) |
277 | -185x | +|||
80 | +
- ui = ui,+ # Otherwise, FilteredData will be created in the modules' scope later |
|||
278 | -185x | +81 | +65x |
- datanames = combined_datanames,+ teal.slice::init_filtered_data( |
279 | -185x | +82 | +65x |
- server_args = server_args,+ x = Filter( |
280 | -185x | +83 | +65x |
- ui_args = ui_args,+ length, |
281 | -185x | +84 | +65x |
- transformers = transformers+ sapply(datanames, function(dn) x[[dn]], simplify = FALSE) |
282 | +85 |
), |
||
283 | -185x | +86 | +65x |
- class = "teal_module"+ join_keys = teal.data::join_keys(x) |
284 | +87 |
) |
||
285 | +88 |
} |
||
286 | +89 | |||
287 | +90 |
- #' @rdname teal_modules+ |
||
288 | +91 |
- #' @export+ #' Template function for `TealReportCard` creation and customization |
||
289 | +92 |
#' |
||
290 | +93 |
- modules <- function(..., label = "root") {+ #' This function generates a report card with a title, |
||
291 | -124x | +|||
94 | +
- checkmate::assert_string(label)+ #' an optional description, and the option to append the filter state list. |
|||
292 | -122x | +|||
95 | +
- submodules <- list(...)+ #' |
|||
293 | -122x | +|||
96 | +
- if (any(vapply(submodules, is.character, FUN.VALUE = logical(1)))) {+ #' @param title (`character(1)`) title of the card (unless overwritten by label) |
|||
294 | -2x | +|||
97 | +
- stop(+ #' @param label (`character(1)`) label provided by the user when adding the card |
|||
295 | -2x | +|||
98 | +
- "The only character argument to modules() must be 'label' and it must be named, ",+ #' @param description (`character(1)`) optional, additional description |
|||
296 | -2x | +|||
99 | +
- "change modules('lab', ...) to modules(label = 'lab', ...)"+ #' @param with_filter (`logical(1)`) flag indicating to add filter state |
|||
297 | +100 |
- )+ #' @param filter_panel_api (`FilterPanelAPI`) object with API that allows the generation |
||
298 | +101 |
- }+ #' of the filter state in the report |
||
299 | +102 |
-
+ #' |
||
300 | -120x | +|||
103 | +
- checkmate::assert_list(submodules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules"))+ #' @return (`TealReportCard`) populated with a title, description and filter state. |
|||
301 | +104 |
- # name them so we can more easily access the children+ #' |
||
302 | +105 |
- # beware however that the label of the submodules should not be changed as it must be kept synced+ #' @export |
||
303 | -117x | +|||
106 | +
- labels <- vapply(submodules, function(submodule) submodule$label, character(1))+ report_card_template <- function(title, label, description = NULL, with_filter, filter_panel_api) { |
|||
304 | -117x | +107 | +2x |
- names(submodules) <- get_unique_labels(labels)+ checkmate::assert_string(title) |
305 | -117x | +108 | +2x |
- structure(+ checkmate::assert_string(label) |
306 | -117x | +109 | +2x |
- list(+ checkmate::assert_string(description, null.ok = TRUE) |
307 | -117x | +110 | +2x |
- label = label,+ checkmate::assert_flag(with_filter) |
308 | -117x | +111 | +2x |
- children = submodules+ checkmate::assert_class(filter_panel_api, classes = "FilterPanelAPI") |
309 | +112 |
- ),+ |
||
310 | -117x | +113 | +2x |
- class = "teal_modules"+ card <- teal::TealReportCard$new() |
311 | -+ | |||
114 | +2x |
- )+ title <- if (label == "") title else label |
||
312 | -+ | |||
115 | +2x |
- }+ card$set_name(title) |
||
313 | -+ | |||
116 | +2x |
-
+ card$append_text(title, "header2") |
||
314 | -+ | |||
117 | +1x |
- # printing methods ----+ if (!is.null(description)) card$append_text(description, "header3") |
||
315 | -+ | |||
118 | +1x |
-
+ if (with_filter) card$append_fs(filter_panel_api$get_filter_state()) |
||
316 | -+ | |||
119 | +2x |
- #' @rdname teal_modules+ card |
||
317 | +120 |
- #' @export+ } |
||
318 | +121 |
- format.teal_module <- function(x, indent = 0, ...) {- |
- ||
319 | -3x | -
- paste0(paste(rep(" ", indent), collapse = ""), "+ ", x$label, "\n", collapse = "")+ |
||
320 | +122 |
- }+ |
||
321 | +123 |
-
+ #' Check `datanames` in modules |
||
322 | +124 |
-
+ #' |
||
323 | +125 |
- #' @rdname teal_modules+ #' This function ensures specified `datanames` in modules match those in the data object, |
||
324 | +126 |
- #' @export+ #' returning error messages or `TRUE` for successful validation. |
||
325 | +127 |
- print.teal_module <- function(x, ...) {- |
- ||
326 | -! | -
- cat(format(x, ...))- |
- ||
327 | -! | -
- invisible(x)+ #' |
||
328 | +128 |
- }+ #' @param modules (`teal_modules`) object |
||
329 | +129 |
-
+ #' @param datanames (`character`) names of datasets available in the `data` object |
||
330 | +130 |
-
+ #' |
||
331 | +131 |
- #' @rdname teal_modules+ #' @return A `character(1)` containing error message or `TRUE` if validation passes. |
||
332 | +132 |
- #' @export+ #' @keywords internal |
||
333 | +133 |
- format.teal_modules <- function(x, indent = 0, ...) {- |
- ||
334 | -1x | -
- paste(- |
- ||
335 | -1x | -
- c(+ check_modules_datanames <- function(modules, datanames) { |
||
336 | -1x | +134 | +152x |
- paste0(rep(" ", indent), "+ ", x$label, "\n"),+ checkmate::assert_multi_class(modules, c("teal_modules", "teal_module")) |
337 | -1x | +135 | +152x |
- unlist(lapply(x$children, format, indent = indent + 1, ...))+ checkmate::assert_character(datanames) |
338 | +136 |
- ),+ |
||
339 | -1x | +137 | +152x |
- collapse = ""+ recursive_check_datanames <- function(modules, datanames) { |
340 | +138 |
- )+ # check teal_modules against datanames |
||
341 | -+ | |||
139 | +237x |
- }+ if (inherits(modules, "teal_modules")) { |
||
342 | -+ | |||
140 | +65x |
-
+ result <- lapply(modules$children, function(module) recursive_check_datanames(module, datanames = datanames)) |
||
343 | -+ | |||
141 | +65x |
- #' @param modules (`teal_module` or `teal_modules`)+ result <- result[vapply(result, Negate(is.null), logical(1L))] |
||
344 | -+ | |||
142 | +65x |
- #' @rdname teal_modules+ if (length(result) == 0) { |
||
345 | -+ | |||
143 | +58x |
- #' @examples+ return(NULL) |
||
346 | +144 |
- #' # change the module's datanames+ } |
||
347 | -+ | |||
145 | +7x |
- #' set_datanames(module(datanames = "all"), "a")+ list( |
||
348 | -+ | |||
146 | +7x |
- #'+ string = do.call(c, as.list(unname(sapply(result, function(x) x$string)))), |
||
349 | -+ | |||
147 | +7x |
- #' # change modules' datanames+ html = function(with_module_name = TRUE) { |
||
350 | -+ | |||
148 | +6x |
- #' set_datanames(+ tagList( |
||
351 | -+ | |||
149 | +6x |
- #' modules(+ lapply( |
||
352 | -+ | |||
150 | +6x |
- #' module(datanames = "all"),+ result, |
||
353 | -+ | |||
151 | +6x |
- #' module(datanames = "a")+ function(x) x$html(with_module_name = with_module_name) |
||
354 | +152 |
- #' ),+ ) |
||
355 | +153 |
- #' "b"+ ) |
||
356 | +154 |
- #' )+ } |
||
357 | +155 |
- #' @export+ ) |
||
358 | +156 |
- set_datanames <- function(modules, datanames) {- |
- ||
359 | -! | -
- checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"))+ } else { |
||
360 | -! | +|||
157 | +172x |
- if (inherits(modules, "teal_modules")) {+ extra_datanames <- setdiff(modules$datanames, c("all", datanames)) |
||
361 | -! | +|||
158 | +172x |
- modules$children <- lapply(modules$children, set_datanames, datanames)+ if (length(extra_datanames)) { |
||
362 | -+ | |||
159 | +10x |
- } else {+ list( |
||
363 | -! | +|||
160 | +10x |
- if (identical(modules$datanames, "all")) {+ string = build_datanames_error_message( |
||
364 | -! | +|||
161 | +10x |
- modules$datanames <- datanames+ modules$label, |
||
365 | -+ | |||
162 | +10x |
- } else {+ datanames, |
||
366 | -! | +|||
163 | +10x |
- warning(+ extra_datanames, |
||
367 | -! | +|||
164 | +10x |
- "Not possible to modify datanames of the module ", modules$label,+ tags = list( |
||
368 | -! | +|||
165 | +10x |
- ". set_datanames() can only change datanames if it was set to \"all\".",+ span = function(..., .noWS = NULL) { # nolint: object_name |
||
369 | -! | +|||
166 | +71x |
- call. = FALSE+ trimws(paste(..., sep = ifelse(is.null(.noWS), " ", ""), collapse = " ")) |
||
370 | +167 |
- )+ }, |
||
371 | -+ | |||
168 | +10x |
- }+ code = function(x) toString(dQuote(x, q = FALSE)) |
||
372 | +169 |
- }+ ), |
||
373 | -! | +|||
170 | +10x |
- modules+ tagList = function(...) trimws(paste(...)) |
||
374 | +171 |
- }+ ), |
||
375 | +172 |
-
+ # Build HTML representation of the error message with <pre> formatting |
||
376 | -+ | |||
173 | +10x |
- #' @rdname teal_modules+ html = function(with_module_name = TRUE) { |
||
377 | -+ | |||
174 | +9x |
- #' @export+ tagList( |
||
378 | -+ | |||
175 | +9x |
- print.teal_modules <- print.teal_module+ build_datanames_error_message( |
||
379 | -+ | |||
176 | +9x |
-
+ if (with_module_name) modules$label, |
||
380 | -+ | |||
177 | +9x |
-
+ datanames, |
||
381 | -+ | |||
178 | +9x |
- # utilities ----+ extra_datanames |
||
382 | +179 |
- ## subset or modify modules ----+ ), |
||
383 | -+ | |||
180 | +9x |
-
+ tags$br(.noWS = "before") |
||
384 | +181 |
- #' Append a `teal_module` to `children` of a `teal_modules` object+ ) |
||
385 | +182 |
- #' @keywords internal+ } |
||
386 | +183 |
- #' @param modules (`teal_modules`)+ ) |
||
387 | +184 |
- #' @param module (`teal_module`) object to be appended onto the children of `modules`+ } |
||
388 | +185 |
- #' @return A `teal_modules` object with `module` appended.+ } |
||
389 | +186 |
- append_module <- function(modules, module) {+ } |
||
390 | -8x | +187 | +152x |
- checkmate::assert_class(modules, "teal_modules")+ check_datanames <- recursive_check_datanames(modules, datanames) |
391 | -6x | +188 | +152x |
- checkmate::assert_class(module, "teal_module")+ if (length(check_datanames)) { |
392 | -4x | +189 | +10x |
- modules$children <- c(modules$children, list(module))+ check_datanames |
393 | -4x | +|||
190 | +
- labels <- vapply(modules$children, function(submodule) submodule$label, character(1))+ } else { |
|||
394 | -4x | +191 | +142x |
- names(modules$children) <- get_unique_labels(labels)+ TRUE |
395 | -4x | +|||
192 | +
- modules+ } |
|||
396 | +193 |
} |
||
397 | +194 | |||
398 | +195 |
- #' Extract/Remove module(s) of specific class+ #' Check `datanames` in filters |
||
399 | +196 |
#' |
||
400 | +197 |
- #' Given a `teal_module` or a `teal_modules`, return the elements of the structure according to `class`.+ #' This function checks whether `datanames` in filters correspond to those in `data`, |
||
401 | +198 |
- #'+ #' returning character vector with error messages or `TRUE` if all checks pass. |
||
402 | +199 |
- #' @param modules (`teal_modules`)+ #' |
||
403 | +200 |
- #' @param class The class name of `teal_module` to be extracted or dropped.+ #' @param filters (`teal_slices`) object |
||
404 | +201 |
- #' @keywords internal+ #' @param datanames (`character`) names of datasets available in the `data` object |
||
405 | +202 |
- #' @return+ #' |
||
406 | +203 |
- #' - For `extract_module`, a `teal_module` of class `class` or `teal_modules` containing modules of class `class`.+ #' @return A `character(1)` containing error message or TRUE if validation passes. |
||
407 | +204 |
- #' - For `drop_module`, the opposite, which is all `teal_modules` of class other than `class`.+ #' @keywords internal |
||
408 | +205 |
- #' @rdname module_management+ check_filter_datanames <- function(filters, datanames) { |
||
409 | -+ | |||
206 | +65x |
- extract_module <- function(modules, class) {+ checkmate::assert_class(filters, "teal_slices") |
||
410 | -22x | +207 | +65x |
- if (inherits(modules, class)) {+ checkmate::assert_character(datanames) |
411 | -! | +|||
208 | +
- modules+ |
|||
412 | -22x | -
- } else if (inherits(modules, "teal_module")) {+ | ||
209 | ++ |
+ # check teal_slices against datanames |
||
413 | -12x | +210 | +65x |
- NULL+ out <- unlist(sapply( |
414 | -10x | +211 | +65x |
- } else if (inherits(modules, "teal_modules")) {+ filters, function(filter) { |
415 | -10x | +212 | +24x |
- Filter(function(x) length(x) > 0L, lapply(modules$children, extract_module, class))+ dataname <- shiny::isolate(filter$dataname) |
416 | -+ | |||
213 | +24x |
- }+ if (!dataname %in% datanames) { |
||
417 | -+ | |||
214 | +3x |
- }+ sprintf( |
||
418 | -+ | |||
215 | +3x |
-
+ "- Filter '%s' refers to dataname not available in 'data':\n %s not in (%s)", |
||
419 | -+ | |||
216 | +3x |
- #' @keywords internal+ shiny::isolate(filter$id), |
||
420 | -+ | |||
217 | +3x |
- #' @return `teal_modules`+ dQuote(dataname, q = FALSE), |
||
421 | -+ | |||
218 | +3x |
- #' @rdname module_management+ toString(dQuote(datanames, q = FALSE)) |
||
422 | +219 |
- drop_module <- function(modules, class) {+ ) |
||
423 | -! | +|||
220 | +
- if (inherits(modules, class)) {+ } |
|||
424 | -! | +|||
221 | +
- NULL+ } |
|||
425 | -! | +|||
222 | +
- } else if (inherits(modules, "teal_module")) {+ )) |
|||
426 | -! | +|||
223 | +
- modules+ |
|||
427 | -! | +|||
224 | +
- } else if (inherits(modules, "teal_modules")) {+ |
|||
428 | -! | +|||
225 | +65x |
- do.call(+ if (length(out)) { |
||
429 | -! | +|||
226 | +3x |
- "modules",+ paste(out, collapse = "\n") |
||
430 | -! | +|||
227 | +
- c(Filter(function(x) length(x) > 0L, lapply(modules$children, drop_module, class)), label = modules$label)+ } else { |
|||
431 | -+ | |||
228 | +62x |
- )+ TRUE |
||
432 | +229 |
} |
||
433 | +230 |
} |
||
434 | +231 | |||
435 | -- |
- ## read modules ----- |
- ||
436 | +232 |
-
+ #' Function for validating the title parameter of `teal::init` |
||
437 | +233 |
- #' Does the object make use of the `arg`+ #' |
||
438 | +234 |
- #'+ #' Checks if the input of the title from `teal::init` will create a valid title and favicon tag. |
||
439 | +235 |
- #' @param modules (`teal_module` or `teal_modules`) object+ #' @param shiny_tag (`shiny.tag`) Object to validate for a valid title. |
||
440 | +236 |
- #' @param arg (`character(1)`) names of the arguments to be checked against formals of `teal` modules.+ #' @keywords internal |
||
441 | +237 |
- #' @return `logical` whether the object makes use of `arg`.+ validate_app_title_tag <- function(shiny_tag) { |
||
442 | -+ | |||
238 | +7x |
- #' @rdname is_arg_used+ checkmate::assert_class(shiny_tag, "shiny.tag") |
||
443 | -+ | |||
239 | +7x |
- #' @keywords internal+ checkmate::assert_true(shiny_tag$name == "head") |
||
444 | -+ | |||
240 | +6x |
- is_arg_used <- function(modules, arg) {+ child_names <- vapply(shiny_tag$children, `[[`, character(1L), "name") |
||
445 | -476x | +241 | +6x |
- checkmate::assert_string(arg)+ checkmate::assert_subset(c("title", "link"), child_names, .var.name = "child tags") |
446 | -473x | +242 | +4x |
- if (inherits(modules, "teal_modules")) {+ rel_attr <- shiny_tag$children[[which(child_names == "link")]]$attribs$rel |
447 | -17x | +243 | +4x |
- any(unlist(lapply(modules$children, is_arg_used, arg)))+ checkmate::assert_subset( |
448 | -456x | +244 | +4x |
- } else if (inherits(modules, "teal_module")) {+ rel_attr, |
449 | -29x | +245 | +4x |
- is_arg_used(modules$server, arg) || is_arg_used(modules$ui, arg)+ c("icon", "shortcut icon"), |
450 | -427x | +246 | +4x |
- } else if (is.function(modules)) {+ .var.name = "Link tag's rel attribute", |
451 | -425x | +247 | +4x |
- isTRUE(arg %in% names(formals(modules)))+ empty.ok = FALSE |
452 | +248 |
- } else {- |
- ||
453 | -2x | -
- stop("is_arg_used function not implemented for this object")+ ) |
||
454 | +249 |
- }+ } |
||
455 | +250 |
- }+ |
||
456 | +251 |
-
+ #' Build app title with favicon |
||
457 | +252 |
-
+ #' |
||
458 | +253 |
- #' Get module depth+ #' A helper function to create the browser title along with a logo. |
||
459 | +254 |
#' |
||
460 | +255 |
- #' Depth starts at 0, so a single `teal.module` has depth 0.+ #' @param title (`character`) The browser title for the `teal` app. |
||
461 | +256 |
- #' Nesting it increases overall depth by 1.+ #' @param favicon (`character`) The path for the icon for the title. |
||
462 | +257 |
- #'+ #' The image/icon path can be remote or the static path accessible by `shiny`, like the `www/` |
||
463 | +258 |
- #' @inheritParams init+ #' |
||
464 | +259 |
- #' @param depth optional integer determining current depth level+ #' @return A `shiny.tag` containing the element that adds the title and logo to the `shiny` app. |
||
465 | +260 |
- #'+ #' @export |
||
466 | +261 |
- #' @return Depth level for given module.+ build_app_title <- function( |
||
467 | +262 |
- #' @keywords internal+ title = "teal app", |
||
468 | +263 |
- modules_depth <- function(modules, depth = 0L) {+ favicon = "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/nest.png") { |
||
469 | +264 | 12x |
- checkmate::assert_multi_class(modules, c("teal_module", "teal_modules"))+ checkmate::assert_string(title, null.ok = TRUE) |
|
470 | +265 | 12x |
- checkmate::assert_int(depth, lower = 0)+ checkmate::assert_string(favicon, null.ok = TRUE) |
|
471 | -11x | +266 | +12x |
- if (inherits(modules, "teal_modules")) {+ tags$head( |
472 | -4x | +267 | +12x |
- max(vapply(modules$children, modules_depth, integer(1), depth = depth + 1L))+ tags$title(title), |
473 | -+ | |||
268 | +12x |
- } else {+ tags$link( |
||
474 | -7x | +269 | +12x |
- depth+ rel = "icon", |
475 | -+ | |||
270 | +12x |
- }+ href = favicon, |
||
476 | -+ | |||
271 | +12x |
- }+ sizes = "any" |
||
477 | +272 |
-
+ ) |
||
478 | +273 |
- #' Retrieve labels from `teal_modules`+ ) |
||
479 | +274 |
- #'+ } |
||
480 | +275 |
- #' @param modules (`teal_modules`)+ |
||
481 | +276 |
- #' @return A `list` containing the labels of the modules. If the modules are nested,+ #' Application ID |
||
482 | +277 |
- #' the function returns a nested `list` of labels.+ #' |
||
483 | +278 |
- #' @keywords internal+ #' Creates App ID used to match filter snapshots to application. |
||
484 | +279 |
- module_labels <- function(modules) {- |
- ||
485 | -165x | -
- if (inherits(modules, "teal_modules")) {- |
- ||
486 | -70x | -
- lapply(modules$children, module_labels)+ #' |
||
487 | +280 |
- } else {- |
- ||
488 | -95x | -
- modules$label+ #' Calculate app ID that will be used to stamp filter state snapshots. |
||
489 | +281 |
- }+ #' App ID is a hash of the app's data and modules. |
||
490 | +282 |
- }+ #' See "transferring snapshots" section in ?snapshot. |
||
491 | +283 |
-
+ #' |
||
492 | +284 |
- #' Retrieve `teal_bookmarkable` attribute from `teal_modules`+ #' @param data (`teal_data` or `teal_data_module`) as accepted by `init` |
||
493 | +285 |
- #'+ #' @param modules (`teal_modules`) object as accepted by `init` |
||
494 | +286 |
- #' @param modules (`teal_modules` or `teal_module`) object+ #' |
||
495 | +287 |
- #' @return named list of the same structure as `modules` with `TRUE` or `FALSE` values indicating+ #' @return A single character string. |
||
496 | +288 |
- #' whether the module is bookmarkable.+ #' |
||
497 | +289 |
#' @keywords internal |
||
498 | +290 |
- modules_bookmarkable <- function(modules) {+ create_app_id <- function(data, modules) { |
||
499 | -163x | +291 | +20x |
- checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"))+ checkmate::assert_multi_class(data, c("teal_data", "teal_data_module")) |
500 | -163x | +292 | +19x |
- if (inherits(modules, "teal_modules")) {+ checkmate::assert_class(modules, "teal_modules")+ |
+
293 | ++ | + | ||
501 | -69x | +294 | +18x |
- setNames(+ data <- if (inherits(data, "teal_data")) { |
502 | -69x | +295 | +16x |
- lapply(modules$children, modules_bookmarkable),+ as.list(teal.code::get_env(data)) |
503 | -69x | +296 | +18x |
- vapply(modules$children, `[[`, "label", FUN.VALUE = character(1))+ } else if (inherits(data, "teal_data_module")) { |
504 | -+ | |||
297 | +2x |
- )+ deparse1(body(data$server)) |
||
505 | +298 |
- } else {+ } |
||
506 | -94x | +299 | +18x |
- attr(modules, "teal_bookmarkable", exact = TRUE)+ modules <- lapply(modules, defunction) |
507 | +300 |
- }+ + |
+ ||
301 | +18x | +
+ rlang::hash(list(data = data, modules = modules)) |
||
508 | +302 |
} |
1 | +303 |
- #' Data summary+ |
||
2 | +304 |
- #' @description+ #' Go through list and extract bodies of encountered functions as string, recursively. |
||
3 | +305 |
- #' Module and its utils to display the number of rows and subjects in the filtered and unfiltered data.+ #' @keywords internal |
||
4 | +306 |
- #'+ #' @noRd |
||
5 | +307 |
- #' @details Handling different data classes:+ defunction <- function(x) {+ |
+ ||
308 | +219x | +
+ if (is.list(x)) {+ |
+ ||
309 | +64x | +
+ lapply(x, defunction)+ |
+ ||
310 | +155x | +
+ } else if (is.function(x)) {+ |
+ ||
311 | +48x | +
+ deparse1(body(x)) |
||
6 | +312 |
- #' `get_object_filter_overview()` is a pseudo S3 method which has variants for:+ } else {+ |
+ ||
313 | +107x | +
+ x |
||
7 | +314 |
- #' - `array` (`data.frame`, `DataFrame`, `array`, `Matrix` and `SummarizedExperiment`): Method variant+ } |
||
8 | +315 |
- #' can be applied to any two-dimensional objects on which [ncol()] can be used.+ } |
||
9 | +316 |
- #' - `MultiAssayExperiment`: for which summary contains counts for `colData` and all `experiments`.+ |
||
10 | +317 |
- #'+ #' Get unique labels |
||
11 | +318 |
- #' @param id (`character(1)`)+ #' |
||
12 | +319 |
- #' `shiny` module instance id.+ #' Get unique labels for the modules to avoid namespace conflicts. |
||
13 | +320 |
- #' @param teal_data (`reactive` returning `teal_data`)+ #' |
||
14 | +321 |
- #'+ #' @param labels (`character`) vector of labels |
||
15 | +322 |
#' |
||
16 | +323 |
- #' @name module_data_summary+ #' @return (`character`) vector of unique labels |
||
17 | +324 |
- #' @rdname module_data_summary+ #' |
||
18 | +325 |
#' @keywords internal |
||
19 | +326 |
- #' @return `NULL`.+ get_unique_labels <- function(labels) {+ |
+ ||
327 | +136x | +
+ make.unique(gsub("[^[:alnum:]]", "_", tolower(labels)), sep = "_") |
||
20 | +328 |
- NULL+ } |
||
21 | +329 | |||
22 | +330 |
- #' @rdname module_data_summary+ #' Remove ANSI escape sequences from a string |
||
23 | +331 |
- ui_data_summary <- function(id) {+ #' @noRd |
||
24 | -! | +|||
332 | +
- ns <- NS(id)+ strip_style <- function(string) { |
|||
25 | -! | +|||
333 | +2x |
- content_id <- ns("filters_overview_contents")+ checkmate::assert_string(string) |
||
26 | -! | +|||
334 | +
- tags$div(+ |
|||
27 | -! | +|||
335 | +2x |
- id = id,+ gsub( |
||
28 | -! | +|||
336 | +2x |
- class = "well",+ "(?:(?:\\x{001b}\\[)|\\x{009b})(?:(?:[0-9]{1,3})?(?:(?:;[0-9]{0,3})*)?[A-M|f-m])|\\x{001b}[A-M]", |
||
29 | -! | +|||
337 | +
- tags$div(+ "", |
|||
30 | -! | +|||
338 | +2x |
- class = "row",+ string, |
||
31 | -! | +|||
339 | +2x |
- tags$div(+ perl = TRUE, |
||
32 | -! | +|||
340 | +2x |
- class = "col-sm-9",+ useBytes = TRUE |
||
33 | -! | +|||
341 | +
- tags$label("Active Filter Summary", class = "text-primary mb-4")+ ) |
|||
34 | +342 |
- ),+ } |
||
35 | -! | +|||
343 | +
- tags$div(+ |
|||
36 | -! | +|||
344 | +
- class = "col-sm-3",+ #' Convert character list to human readable html with commas and "and" |
|||
37 | -! | +|||
345 | +
- tags$i(+ #' @noRd |
|||
38 | -! | +|||
346 | +
- class = "remove pull-right fa fa-angle-down",+ paste_datanames_character <- function(x, |
|||
39 | -! | +|||
347 | +
- style = "cursor: pointer;",+ tags = list(span = shiny::tags$span, code = shiny::tags$code), |
|||
40 | -! | +|||
348 | +
- title = "fold/expand data summary panel",+ tagList = shiny::tagList) { # nolint: object_name. |
|||
41 | -! | +|||
349 | +36x |
- onclick = sprintf("togglePanelItems(this, '%s', 'fa-angle-right', 'fa-angle-down');", content_id)+ checkmate::assert_character(x) |
||
42 | -+ | |||
350 | +36x |
- )+ do.call( |
||
43 | -+ | |||
351 | +36x |
- )+ tagList, |
||
44 | -+ | |||
352 | +36x |
- ),+ lapply(seq_along(x), function(.ix) { |
||
45 | -! | +|||
353 | +44x |
- tags$div(+ tagList( |
||
46 | -! | +|||
354 | +44x |
- id = content_id,+ tags$code(x[.ix]), |
||
47 | -! | +|||
355 | +44x |
- tags$div(+ if (.ix != length(x)) { |
||
48 | -! | +|||
356 | +8x |
- class = "teal_active_summary_filter_panel",+ tags$span(ifelse(.ix == length(x) - 1, " and ", ", ")) |
||
49 | -! | +|||
357 | +
- tableOutput(ns("table"))+ } |
|||
50 | +358 |
) |
||
51 | +359 |
- )+ }) |
||
52 | +360 |
) |
||
53 | +361 |
} |
||
54 | +362 | |||
55 | +363 |
- #' @rdname module_data_summary+ #' Build datanames error string for error message |
||
56 | +364 |
- srv_data_summary <- function(id, teal_data) {- |
- ||
57 | -94x | -
- assert_reactive(teal_data)- |
- ||
58 | -94x | -
- moduleServer(- |
- ||
59 | -94x | -
- id = id,- |
- ||
60 | -94x | -
- function(input, output, session) {+ #' |
||
61 | -94x | +|||
365 | +
- logger::log_debug("srv_data_summary initializing")+ #' tags and tagList are overwritten in arguments allowing to create strings for |
|||
62 | +366 |
-
+ #' logging purposes |
||
63 | -94x | +|||
367 | +
- summary_table <- reactive({+ #' @noRd |
|||
64 | -103x | +|||
368 | +
- req(inherits(teal_data(), "teal_data"))+ build_datanames_error_message <- function(label = NULL, |
|||
65 | -78x | +|||
369 | +
- if (!length(ls(teal.code::get_env(teal_data())))) {+ datanames, |
|||
66 | -1x | +|||
370 | +
- return(NULL)+ extra_datanames, |
|||
67 | +371 |
- }+ tags = list(span = shiny::tags$span, code = shiny::tags$code), |
||
68 | +372 |
-
+ tagList = shiny::tagList) { # nolint: object_name. |
||
69 | -77x | +373 | +19x |
- filter_overview <- get_filter_overview(teal_data)+ tags$span( |
70 | -77x | +374 | +19x |
- names(filter_overview)[[1]] <- "Data Name"+ tags$span(ifelse(length(extra_datanames) > 1, "Datasets", "Dataset")), |
71 | -+ | |||
375 | +19x |
-
+ paste_datanames_character(extra_datanames, tags, tagList), |
||
72 | -77x | +376 | +19x |
- filter_overview$Obs <- ifelse(+ tags$span( |
73 | -77x | +377 | +19x |
- !is.na(filter_overview$obs),+ paste0( |
74 | -77x | +378 | +19x |
- sprintf("%s/%s", filter_overview$obs_filtered, filter_overview$obs),+ ifelse(length(extra_datanames) > 1, "are missing", "is missing"), |
75 | -77x | +379 | +19x |
- ifelse(!is.na(filter_overview$obs_filtered), sprintf("%s", filter_overview$obs_filtered), "")+ ifelse(is.null(label), ".", sprintf(" for tab '%s'.", label)) |
76 | +380 |
- )+ ) |
||
77 | +381 |
-
+ ), |
||
78 | -77x | +382 | +19x |
- filter_overview$Subjects <- ifelse(+ if (length(datanames) >= 1) { |
79 | -77x | +383 | +17x |
- !is.na(filter_overview$subjects),+ tagList( |
80 | -77x | -
- sprintf("%s/%s", filter_overview$subjects_filtered, filter_overview$subjects),- |
- ||
81 | -- |
- ""- |
- ||
82 | -- |
- )- |
- ||
83 | -+ | 384 | +17x |
-
+ tags$span(ifelse(length(datanames) > 1, "Datasets", "Dataset")), |
84 | -77x | +385 | +17x |
- filter_overview <- filter_overview[, colnames(filter_overview) %in% c("Data Name", "Obs", "Subjects")]+ tags$span("available in data:"), |
85 | -77x | -
- Filter(function(col) !all(col == ""), filter_overview)- |
- ||
86 | -+ | 386 | +17x |
- })+ tagList( |
87 | -+ | |||
387 | +17x |
-
+ tags$span( |
||
88 | -94x | +388 | +17x |
- output$table <- renderUI({+ paste_datanames_character(datanames, tags, tagList), |
89 | -103x | +389 | +17x |
- summary_table_out <- try(summary_table(), silent = TRUE)+ tags$span(".", .noWS = "outside"), |
90 | -103x | +390 | +17x |
- if (inherits(summary_table_out, "try-error")) {+ .noWS = c("outside") |
91 | +391 |
- # Ignore silent shiny error- |
- ||
92 | -25x | -
- if (!inherits(attr(summary_table_out, "condition"), "shiny.silent.error")) {+ ) |
||
93 | -! | +|||
392 | +
- stop("Error occurred during data processing. See details in the main panel.")+ ) |
|||
94 | +393 |
- }+ ) |
||
95 | -78x | +|||
394 | +
- } else if (is.null(summary_table_out)) {+ } else { |
|||
96 | -1x | +395 | +2x |
- "no datasets to show"+ tags$span("No datasets are available in data.") |
97 | +396 |
- } else {- |
- ||
98 | -77x | -
- body_html <- apply(+ } |
||
99 | -77x | +|||
397 | +
- summary_table_out,+ ) |
|||
100 | -77x | +|||
398 | +
- 1,+ } |
|||
101 | -77x | +
1 | +
- function(x) {+ #' Execute and validate `teal_data_module` |
|||
102 | -141x | +|||
2 | +
- tags$tr(+ #' |
|||
103 | -141x | +|||
3 | +
- tagList(+ #' This is a low level module to handle `teal_data_module` execution and validation. |
|||
104 | -141x | +|||
4 | +
- tags$td(+ #' [teal_transform_module()] inherits from [teal_data_module()] so it is handled by this module too. |
|||
105 | -141x | +|||
5 | +
- if (all(x[-1] == "")) {+ #' [srv_teal()] accepts various `data` objects and eventually they are all transformed to `reactive` |
|||
106 | -1x | +|||
6 | +
- icon(+ #' [teal_data()] which is a standard data class in whole `teal` framework. |
|||
107 | -1x | +|||
7 | +
- name = "fas fa-exclamation-triangle",+ #' |
|||
108 | -1x | +|||
8 | +
- title = "Unsupported dataset",+ #' @section data validation: |
|||
109 | -1x | +|||
9 | +
- `data-container` = "body",+ #' |
|||
110 | -1x | +|||
10 | +
- `data-toggle` = "popover",+ #' Executed [teal_data_module()] is validated and output is validated for consistency. |
|||
111 | -1x | +|||
11 | +
- `data-content` = "object not supported by the data_summary module"+ #' Output `data` is invalid if: |
|||
112 | +12 |
- )+ #' 1. [teal_data_module()] is invalid if server doesn't return `reactive`. **Immediately crashes an app!** |
||
113 | +13 |
- },+ #' 2. `reactive` throws a `shiny.error` - happens when module creating [teal_data()] fails. |
||
114 | -141x | +|||
14 | +
- x[1]+ #' 3. `reactive` returns `qenv.error` - happens when [teal_data()] evaluates a failing code. |
|||
115 | +15 |
- ),+ #' 4. `reactive` object doesn't return [teal_data()]. |
||
116 | -141x | +|||
16 | +
- lapply(x[-1], tags$td)+ #' 5. [teal_data()] object lacks any `datanames` specified in the `modules` argument. |
|||
117 | +17 |
- )+ #' |
||
118 | +18 |
- )+ #' `teal` (observers in `srv_teal`) always waits to render an app until `reactive` `teal_data` is |
||
119 | +19 |
- }+ #' returned. If error 2-4 occurs, relevant error message is displayed to the app user. Once the issue is |
||
120 | +20 |
- )+ #' resolved, the app will continue to run. `teal` guarantees that errors in data don't crash the app |
||
121 | +21 |
-
+ #' (except error 1). |
||
122 | -77x | +|||
22 | +
- header_labels <- names(summary_table())+ #' |
|||
123 | -77x | +|||
23 | +
- header_html <- tags$tr(tagList(lapply(header_labels, tags$td)))+ #' @param id (`character(1)`) Module id |
|||
124 | +24 |
-
+ #' @param data (`reactive teal_data`) |
||
125 | -77x | +|||
25 | +
- table_html <- tags$table(+ #' @param data_module (`teal_data_module`) |
|||
126 | -77x | +|||
26 | +
- class = "table custom-table",+ #' @param modules (`teal_modules` or `teal_module`) For `datanames` validation purpose |
|||
127 | -77x | +|||
27 | +
- tags$thead(header_html),+ #' @param validate_shiny_silent_error (`logical`) If `TRUE`, then `shiny.silent.error` is validated and |
|||
128 | -77x | +|||
28 | +
- tags$tbody(body_html)+ #' @param is_transformer_failed (`reactiveValues`) contains `logical` flags named after each transformer. |
|||
129 | +29 |
- )+ #' Help to determine if any previous transformer failed, so that following transformers can be disabled |
||
130 | -77x | +|||
30 | +
- table_html+ #' and display a generic failure message. |
|||
131 | +31 |
- }+ #' |
||
132 | +32 |
- })+ #' @return `reactive` `teal_data` |
||
133 | +33 |
-
+ #' |
||
134 | -94x | +|||
34 | +
- summary_table # testing purpose+ #' @rdname module_teal_data |
|||
135 | +35 |
- }+ #' @name module_teal_data |
||
136 | +36 |
- )+ #' @keywords internal |
||
137 | +37 |
- }+ NULL |
||
138 | +38 | |||
139 | +39 |
- #' @rdname module_data_summary+ #' @rdname module_teal_data |
||
140 | +40 |
- get_filter_overview <- function(teal_data) {+ ui_teal_data <- function(id, data_module = function(id) NULL) { |
||
141 | -77x | +|||
41 | +! |
- datanames <- teal.data::datanames(teal_data())+ checkmate::assert_string(id) |
||
142 | -77x | +|||
42 | +! |
- joinkeys <- teal.data::join_keys(teal_data())+ checkmate::assert_function(data_module, args = "id") |
||
143 | -+ | |||
43 | +! |
-
+ ns <- NS(id) |
||
144 | -77x | +|||
44 | +
- filtered_data_objs <- sapply(+ |
|||
145 | -77x | +|||
45 | +! |
- datanames,+ shiny::tagList( |
||
146 | -77x | +|||
46 | +! |
- function(name) teal.code::get_var(teal_data(), name),+ tags$div(id = ns("wrapper"), data_module(id = ns("data"))), |
||
147 | -77x | +|||
47 | +! |
- simplify = FALSE+ ui_validate_reactive_teal_data(ns("validate")) |
||
148 | +48 |
) |
||
149 | -77x | +|||
49 | +
- unfiltered_data_objs <- teal.code::get_var(teal_data(), ".raw_data")+ } |
|||
150 | +50 | |||
151 | -77x | -
- rows <- lapply(- |
- ||
152 | -77x | -
- datanames,- |
- ||
153 | -77x | -
- function(dataname) {- |
- ||
154 | -141x | -
- parent <- teal.data::parent(joinkeys, dataname)- |
- ||
155 | +51 |
- # todo: what should we display for a parent dataset?+ #' @rdname module_teal_data |
||
156 | +52 |
- # - Obs and Subjects+ srv_teal_data <- function(id, |
||
157 | +53 |
- # - Obs only+ data_module = function(id) NULL, |
||
158 | +54 |
- # - Subjects only+ modules = NULL, |
||
159 | +55 |
- # todo (for later): summary table should be displayed in a way that child datasets+ validate_shiny_silent_error = TRUE, |
||
160 | +56 |
- # are indented under their parent dataset to form a tree structure+ is_transformer_failed = reactiveValues()) { |
||
161 | -141x | +57 | +18x |
- subject_keys <- if (length(parent) > 0) {+ checkmate::assert_string(id) |
162 | -7x | +58 | +18x |
- names(joinkeys[dataname, parent])+ checkmate::assert_function(data_module, args = "id") |
163 | -+ | |||
59 | +18x |
- } else {+ checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"), null.ok = TRUE) |
||
164 | -134x | +60 | +18x |
- joinkeys[dataname, dataname]+ checkmate::assert_class(is_transformer_failed, "reactivevalues") |
165 | +61 |
- }+ |
||
166 | -141x | +62 | +18x |
- get_object_filter_overview(+ moduleServer(id, function(input, output, session) { |
167 | -141x | +63 | +18x |
- filtered_data = filtered_data_objs[[dataname]],+ logger::log_debug("srv_teal_data initializing.") |
168 | -141x | +64 | +18x |
- unfiltered_data = unfiltered_data_objs[[dataname]],+ is_transformer_failed[[id]] <- FALSE |
169 | -141x | +65 | +18x |
- dataname = dataname,+ data_out <- data_module(id = "data") |
170 | -141x | +66 | +17x |
- subject_keys = subject_keys+ data_handled <- reactive(tryCatch(data_out(), error = function(e) e)) |
171 | -+ | |||
67 | +17x |
- )+ observeEvent(data_handled(), { |
||
172 | -+ | |||
68 | +19x |
- }+ if (!inherits(data_handled(), "teal_data")) { |
||
173 | -+ | |||
69 | +4x |
- )+ is_transformer_failed[[id]] <- TRUE |
||
174 | +70 |
-
+ } else { |
||
175 | -77x | +71 | +15x |
- unssuported_idx <- vapply(rows, function(x) all(is.na(x[-1])), logical(1)) # this is mainly for vectors+ is_transformer_failed[[id]] <- FALSE |
176 | -77x | +|||
72 | +
- do.call(rbind, c(rows[!unssuported_idx], rows[unssuported_idx]))+ } |
|||
177 | +73 |
- }+ }) |
||
178 | +74 | |||
179 | -+ | |||
75 | +17x |
- #' @rdname module_data_summary+ is_previous_failed <- reactive({ |
||
180 | -+ | |||
76 | +17x |
- #' @param filtered_data (`list`) of filtered objects+ idx_this <- which(names(is_transformer_failed) == id) |
||
181 | -+ | |||
77 | +17x |
- #' @param unfiltered_data (`list`) of unfiltered objects+ is_transformer_failed_list <- reactiveValuesToList(is_transformer_failed) |
||
182 | -+ | |||
78 | +17x |
- #' @param dataname (`character(1)`)+ idx_failures <- which(unlist(is_transformer_failed_list))+ |
+ ||
79 | +17x | +
+ any(idx_failures < idx_this) |
||
183 | +80 |
- get_object_filter_overview <- function(filtered_data, unfiltered_data, dataname, subject_keys) {+ }) |
||
184 | -141x | +|||
81 | +
- if (inherits(filtered_data, c("data.frame", "DataFrame", "array", "Matrix", "SummarizedExperiment"))) {+ |
|||
185 | -140x | +82 | +17x |
- get_object_filter_overview_array(filtered_data, unfiltered_data, dataname, subject_keys)+ observeEvent(is_previous_failed(), { |
186 | -1x | +83 | +17x |
- } else if (inherits(filtered_data, "MultiAssayExperiment")) {+ if (is_previous_failed()) { |
187 | +84 | ! |
- get_object_filter_overview_MultiAssayExperiment(filtered_data, unfiltered_data, dataname)+ shinyjs::disable("wrapper") |
|
188 | +85 |
- } else {+ } else { |
||
189 | -1x | +86 | +17x |
- data.frame(+ shinyjs::enable("wrapper") |
190 | -1x | +|||
87 | +
- dataname = dataname,+ } |
|||
191 | -1x | +|||
88 | +
- obs = NA,+ })+ |
+ |||
89 | ++ | + | ||
192 | -1x | +90 | +17x |
- obs_filtered = NA,+ srv_validate_reactive_teal_data( |
193 | -1x | +91 | +17x |
- subjects = NA,+ "validate", |
194 | -1x | +92 | +17x |
- subjects_filtered = NA+ data = data_handled, |
195 | -+ | |||
93 | +17x |
- )+ modules = modules, |
||
196 | -+ | |||
94 | +17x |
- }+ validate_shiny_silent_error = validate_shiny_silent_error, |
||
197 | -+ | |||
95 | +17x |
- }+ hide_validation_error = is_previous_failed |
||
198 | +96 |
-
+ ) |
||
199 | +97 |
- #' @rdname module_data_summary+ }) |
||
200 | +98 |
- get_object_filter_overview_array <- function(filtered_data, # nolint: object_length.+ } |
||
201 | +99 |
- unfiltered_data,+ |
||
202 | +100 |
- dataname,+ #' @rdname module_teal_data |
||
203 | +101 |
- subject_keys) {+ ui_validate_reactive_teal_data <- function(id) { |
||
204 | -140x | +102 | +70x |
- if (length(subject_keys) == 0) {+ ns <- NS(id) |
205 | -127x | +103 | +70x |
- data.frame(+ tagList( |
206 | -127x | +104 | +70x |
- dataname = dataname,+ div( |
207 | -127x | +105 | +70x |
- obs = ifelse(!is.null(nrow(unfiltered_data)), nrow(unfiltered_data), NA),+ id = ns("validate_messages"), |
208 | -127x | +106 | +70x |
- obs_filtered = nrow(filtered_data),+ class = "teal_validated", |
209 | -127x | +107 | +70x |
- subjects = NA,+ ui_validate_error(ns("silent_error")), |
210 | -127x | +108 | +70x |
- subjects_filtered = NA+ ui_check_class_teal_data(ns("class_teal_data")), |
211 | -+ | |||
109 | +70x |
- )+ ui_check_shiny_warnings(ns("shiny_warnings")) |
||
212 | +110 |
- } else {- |
- ||
213 | -13x | -
- data.frame(- |
- ||
214 | -13x | -
- dataname = dataname,- |
- ||
215 | -13x | -
- obs = ifelse(!is.null(nrow(unfiltered_data)), nrow(unfiltered_data), NA),+ ), |
||
216 | -13x | +111 | +70x |
- obs_filtered = nrow(filtered_data),+ div( |
217 | -13x | +112 | +70x |
- subjects = nrow(unique(unfiltered_data[subject_keys])),+ class = "teal_validated", |
218 | -13x | +113 | +70x |
- subjects_filtered = nrow(unique(filtered_data[subject_keys]))+ uiOutput(ns("previous_failed")) |
219 | +114 |
) |
||
220 | +115 |
- }+ ) |
||
221 | +116 |
} |
||
222 | +117 | |||
223 | +118 |
- #' @rdname module_data_summary+ #' @rdname module_teal_data |
||
224 | +119 |
- get_object_filter_overview_MultiAssayExperiment <- function(filtered_data, # nolint: object_length, object_name.+ srv_validate_reactive_teal_data <- function(id, # nolint: object_length |
||
225 | +120 |
- unfiltered_data,+ data, |
||
226 | +121 |
- dataname) {+ modules = NULL, |
||
227 | -! | +|||
122 | +
- experiment_names <- names(unfiltered_data)+ validate_shiny_silent_error = FALSE, |
|||
228 | -! | +|||
123 | +
- mae_info <- data.frame(+ hide_validation_error = reactive(FALSE)) { |
|||
229 | -! | +|||
124 | +181x |
- dataname = dataname,+ checkmate::assert_string(id) |
||
230 | -! | +|||
125 | +181x |
- obs = NA,+ checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"), null.ok = TRUE) |
||
231 | -! | +|||
126 | +181x |
- obs_filtered = NA,+ checkmate::assert_flag(validate_shiny_silent_error) |
||
232 | -! | +|||
127 | +
- subjects = nrow(unfiltered_data@colData),+ |
|||
233 | -! | +|||
128 | +181x |
- subjects_filtered = nrow(filtered_data@colData)+ moduleServer(id, function(input, output, session) { |
||
234 | +129 |
- )+ # there is an empty reactive cycle on `init` and `data_rv` has `shiny.silent.error` class |
||
235 | -+ | |||
130 | +181x |
-
+ srv_validate_error("silent_error", data, validate_shiny_silent_error) |
||
236 | -! | +|||
131 | +181x |
- experiment_obs_info <- do.call("rbind", lapply(+ srv_check_class_teal_data("class_teal_data", data) |
||
237 | -! | +|||
132 | +181x |
- experiment_names,+ srv_check_shiny_warnings("shiny_warnings", data, modules) |
||
238 | -! | +|||
133 | +181x |
- function(experiment_name) {+ output$previous_failed <- renderUI({ |
||
239 | -! | +|||
134 | +156x |
- transform(+ if (hide_validation_error()) { |
||
240 | +135 | ! |
- get_object_filter_overview(+ shinyjs::hide("validate_messages") |
|
241 | +136 | ! |
- filtered_data[[experiment_name]],+ tags$div("One of previous transformers failed. Please fix and continue.", class = "teal-output-warning") |
|
242 | -! | +|||
137 | +
- unfiltered_data[[experiment_name]],+ } else { |
|||
243 | -! | +|||
138 | +156x |
- dataname = experiment_name,+ shinyjs::show("validate_messages") |
||
244 | -! | +|||
139 | +156x |
- subject_keys = join_keys() # empty join keys+ NULL |
||
245 | +140 |
- ),+ } |
||
246 | -! | +|||
141 | +
- dataname = paste0(" - ", experiment_name)+ }) |
|||
247 | +142 |
- )+ + |
+ ||
143 | +181x | +
+ .trigger_on_success(data) |
||
248 | +144 |
- }+ }) |
||
249 | +145 |
- ))+ } |
||
250 | +146 | |||
251 | -! | +|||
147 | +
- get_experiment_keys <- function(mae, experiment) {+ #' @keywords internal |
|||
252 | -! | +|||
148 | +
- sample_subset <- mae@sampleMap[mae@sampleMap$colname %in% colnames(experiment), ]+ ui_validate_error <- function(id) { |
|||
253 | -! | +|||
149 | +70x |
- length(unique(sample_subset$primary))+ ns <- NS(id)+ |
+ ||
150 | +70x | +
+ uiOutput(ns("message")) |
||
254 | +151 |
- }+ } |
||
255 | +152 | |||
256 | -! | +|||
153 | +
- experiment_subjects_info <- do.call("rbind", lapply(+ #' @keywords internal |
|||
257 | -! | +|||
154 | +
- experiment_names,+ srv_validate_error <- function(id, data, validate_shiny_silent_error) { |
|||
258 | -! | +|||
155 | +181x |
- function(experiment_name) {+ checkmate::assert_string(id) |
||
259 | -! | +|||
156 | +181x |
- data.frame(+ checkmate::assert_flag(validate_shiny_silent_error) |
||
260 | -! | +|||
157 | +181x |
- subjects = get_experiment_keys(filtered_data, unfiltered_data[[experiment_name]]),+ moduleServer(id, function(input, output, session) { |
||
261 | -! | +|||
158 | +181x |
- subjects_filtered = get_experiment_keys(filtered_data, filtered_data[[experiment_name]])+ output$message <- renderUI({ |
||
262 | -+ | |||
159 | +181x |
- )+ is_shiny_silent_error <- inherits(data(), "shiny.silent.error") && identical(data()$message, "") |
||
263 | -+ | |||
160 | +152x |
- }+ if (inherits(data(), "qenv.error")) { |
||
264 | -+ | |||
161 | +2x |
- ))+ validate( |
||
265 | -+ | |||
162 | +2x |
-
+ need( |
||
266 | -! | +|||
163 | +2x |
- experiment_info <- cbind(experiment_obs_info[, c("dataname", "obs", "obs_filtered")], experiment_subjects_info)+ FALSE, |
||
267 | -! | +|||
164 | +2x |
- rbind(mae_info, experiment_info)+ paste( |
||
268 | -+ | |||
165 | +2x |
- }+ "Error when executing the `data` module:", |
1 | -+ | ||
166 | +2x |
- # This is the main function from teal to be used by the end-users. Although it delegates+ strip_style(paste(data()$message, collapse = "\n")), |
|
2 | -+ | ||
167 | +2x |
- # directly to `module_teal_with_splash.R`, we keep it in a separate file because its documentation is quite large+ "\nCheck your inputs or contact app developer if error persists.", |
|
3 | -+ | ||
168 | +2x |
- # and it is very end-user oriented. It may also perform more argument checking with more informative+ collapse = "\n" |
|
4 | +169 |
- # error messages.+ ) |
|
5 | +170 |
-
+ ) |
|
6 | +171 |
- #' Create the server and UI function for the `shiny` app+ ) |
|
7 | -+ | ||
172 | +150x |
- #'+ } else if (inherits(data(), "error")) { |
|
8 | -+ | ||
173 | +4x |
- #' @description `r lifecycle::badge("stable")`+ if (is_shiny_silent_error && !validate_shiny_silent_error) { |
|
9 | -+ | ||
174 | +! |
- #'+ return(NULL) |
|
10 | +175 |
- #' End-users: This is the most important function for you to start a+ } |
|
11 | -+ | ||
176 | +4x |
- #' `teal` app that is composed of `teal` modules.+ validate( |
|
12 | -+ | ||
177 | +4x |
- #'+ need( |
|
13 | -+ | ||
178 | +4x |
- #' @param data (`teal_data` or `teal_data_module`)+ FALSE, |
|
14 | -+ | ||
179 | +4x |
- #' For constructing the data object, refer to [teal_data()] and [teal_data_module()].+ sprintf( |
|
15 | -+ | ||
180 | +4x |
- #' If `datanames` are not set for the `teal_data` object, defaults from the `teal_data` environment will be used.+ "Shiny error when executing the `data` module.\n%s\n%s", |
|
16 | -+ | ||
181 | +4x |
- #' @param modules (`list` or `teal_modules` or `teal_module`)+ data()$message, |
|
17 | -+ | ||
182 | +4x |
- #' Nested list of `teal_modules` or `teal_module` objects or a single+ "Check your inputs or contact app developer if error persists." |
|
18 | +183 |
- #' `teal_modules` or `teal_module` object. These are the specific output modules which+ ) |
|
19 | +184 |
- #' will be displayed in the `teal` application. See [modules()] and [module()] for+ ) |
|
20 | +185 |
- #' more details.+ ) |
|
21 | +186 |
- #' @param filter (`teal_slices`) Optionally,+ } |
|
22 | +187 |
- #' specifies the initial filter using [teal_slices()].+ }) |
|
23 | +188 |
- #' @param title (`shiny.tag` or `character(1)`) Optionally,+ }) |
|
24 | +189 |
- #' the browser window title. Defaults to a title "teal app" with the icon of NEST.+ } |
|
25 | +190 |
- #' Can be created using the `build_app_title()` or+ |
|
26 | +191 |
- #' by passing a valid `shiny.tag` which is a head tag with title and link tag.+ |
|
27 | +192 |
- #' @param header (`shiny.tag` or `character(1)`) Optionally,+ #' @keywords internal |
|
28 | +193 |
- #' the header of the app.+ ui_check_class_teal_data <- function(id) { |
|
29 | -+ | ||
194 | +70x |
- #' @param footer (`shiny.tag` or `character(1)`) Optionally,+ ns <- NS(id) |
|
30 | -+ | ||
195 | +70x |
- #' the footer of the app.+ uiOutput(ns("message")) |
|
31 | +196 |
- #' @param id (`character`) Optionally,+ } |
|
32 | +197 |
- #' a string specifying the `shiny` module id in cases it is used as a `shiny` module+ |
|
33 | +198 |
- #' rather than a standalone `shiny` app. This is a legacy feature.+ #' @keywords internal |
|
34 | +199 |
- #' @param landing_popup (`teal_module_landing`) Optionally,+ srv_check_class_teal_data <- function(id, data) { |
|
35 | -+ | ||
200 | +181x |
- #' a `landing_popup_module` to show up as soon as the teal app is initialized.+ checkmate::assert_string(id) |
|
36 | -+ | ||
201 | +181x |
- #'+ moduleServer(id, function(input, output, session) { |
|
37 | -+ | ||
202 | +181x |
- #' @return Named list containing server and UI functions.+ output$message <- renderUI({ |
|
38 | -+ | ||
203 | +181x |
- #'+ validate( |
|
39 | -+ | ||
204 | +181x |
- #' @export+ need( |
|
40 | -+ | ||
205 | +181x |
- #'+ inherits(data(), c("teal_data", "error")), |
|
41 | -+ | ||
206 | +181x |
- #' @include modules.R+ "Did not receive `teal_data` object. Cannot proceed further." |
|
42 | +207 |
- #'+ ) |
|
43 | +208 |
- #' @examples+ ) |
|
44 | +209 |
- #' app <- init(+ }) |
|
45 | +210 |
- #' data = within(+ }) |
|
46 | +211 |
- #' teal_data(),+ } |
|
47 | +212 |
- #' {+ |
|
48 | +213 |
- #' new_iris <- transform(iris, id = seq_len(nrow(iris)))+ #' @keywords internal |
|
49 | +214 |
- #' new_mtcars <- transform(mtcars, id = seq_len(nrow(mtcars)))+ ui_check_shiny_warnings <- function(id) { |
|
50 | -+ | ||
215 | +70x |
- #' }+ ns <- NS(id) |
|
51 | -+ | ||
216 | +70x |
- #' ),+ uiOutput(NS(id, "message")) |
|
52 | +217 |
- #' modules = modules(+ } |
|
53 | +218 |
- #' module(+ |
|
54 | +219 |
- #' label = "data source",+ #' @keywords internal |
|
55 | +220 |
- #' server = function(input, output, session, data) {},+ srv_check_shiny_warnings <- function(id, data, modules) { |
|
56 | -+ | ||
221 | +181x |
- #' ui = function(id, ...) tags$div(p("information about data source")),+ checkmate::assert_string(id) |
|
57 | -+ | ||
222 | +181x |
- #' datanames = "all"+ moduleServer(id, function(input, output, session) { |
|
58 | -+ | ||
223 | +181x |
- #' ),+ output$message <- renderUI({ |
|
59 | -+ | ||
224 | +181x |
- #' example_module(label = "example teal module"),+ if (inherits(data(), "teal_data")) { |
|
60 | -+ | ||
225 | +144x |
- #' module(+ is_modules_ok <- check_modules_datanames(modules = modules, datanames = ls(teal.code::get_env(data()))) |
|
61 | -+ | ||
226 | +144x |
- #' "Iris Sepal.Length histogram",+ if (!isTRUE(is_modules_ok)) { |
|
62 | -+ | ||
227 | +9x |
- #' server = function(input, output, session, data) {+ tags$div( |
|
63 | -+ | ||
228 | +9x |
- #' output$hist <- renderPlot(+ class = "teal-output-warning", |
|
64 | -+ | ||
229 | +9x |
- #' hist(data()[["new_iris"]]$Sepal.Length)+ is_modules_ok$html( |
|
65 | +230 |
- #' )+ # Show modules prefix on message only in teal_data_module tab |
|
66 | -+ | ||
231 | +9x |
- #' },+ grepl(sprintf("data-teal_data_module-%s", id), session$ns(NULL), fixed = TRUE) |
|
67 | +232 |
- #' ui = function(id, ...) {+ ) |
|
68 | +233 |
- #' ns <- NS(id)+ ) |
|
69 | +234 |
- #' plotOutput(ns("hist"))+ } |
|
70 | +235 |
- #' },+ } |
|
71 | +236 |
- #' datanames = "new_iris"+ }) |
|
72 | +237 |
- #' )+ }) |
|
73 | +238 |
- #' ),+ } |
|
74 | +239 |
- #' filter = teal_slices(+ |
|
75 | +240 |
- #' teal_slice(dataname = "new_iris", varname = "Species"),+ .trigger_on_success <- function(data) { |
|
76 | -+ | ||
241 | +181x |
- #' teal_slice(dataname = "new_iris", varname = "Sepal.Length"),+ out <- reactiveVal(NULL) |
|
77 | -+ | ||
242 | +181x |
- #' teal_slice(dataname = "new_mtcars", varname = "cyl"),+ observeEvent(data(), { |
|
78 | -+ | ||
243 | +152x |
- #' exclude_varnames = list(new_iris = c("Sepal.Width", "Petal.Width")),+ if (inherits(data(), "teal_data")) {+ |
+ |
244 | +144x | +
+ if (!identical(data(), out())) {+ |
+ |
245 | +144x | +
+ out(data()) |
|
79 | +246 |
- #' module_specific = TRUE,+ } |
|
80 | +247 |
- #' mapping = list(+ } |
|
81 | +248 |
- #' `example teal module` = "new_iris Species",+ }) |
|
82 | +249 |
- #' `Iris Sepal.Length histogram` = "new_iris Species",+ + |
+ |
250 | +181x | +
+ out |
|
83 | +251 |
- #' global_filters = "new_mtcars cyl"+ } |
84 | +1 |
- #' )+ #' Create a `tdata` object |
|
85 | +2 |
- #' ),+ #' |
|
86 | +3 |
- #' title = "App title",+ #' @description `r lifecycle::badge("superseded")` |
|
87 | +4 |
- #' header = tags$h1("Sample App"),+ #' |
|
88 | +5 |
- #' footer = tags$p("Sample footer")+ #' Recent changes in `teal` cause modules to fail because modules expect a `tdata` object |
|
89 | +6 |
- #' )+ #' to be passed to the `data` argument but instead they receive a `teal_data` object, |
|
90 | +7 |
- #' if (interactive()) {+ #' which is additionally wrapped in a reactive expression in the server functions. |
|
91 | +8 |
- #' shinyApp(app$ui, app$server)+ #' In order to easily adapt such modules without a proper refactor, |
|
92 | +9 |
- #' }+ #' use this function to downgrade the `data` argument. |
|
93 | +10 |
#' |
|
94 | +11 |
- init <- function(data,+ #' @name tdata |
|
95 | +12 |
- modules,+ #' @param ... ignored |
|
96 | +13 |
- filter = teal_slices(),+ #' @return nothing |
|
97 | +14 |
- title = build_app_title(),+ NULL |
|
98 | +15 |
- header = tags$p(),+ |
|
99 | +16 |
- footer = tags$p(),+ #' @rdname tdata |
|
100 | +17 |
- id = character(0),+ #' @export |
|
101 | +18 |
- landing_popup = NULL) {+ new_tdata <- function(...) { |
|
102 | -11x | +||
19 | +! |
- logger::log_debug("init initializing teal app with: data ('{ class(data) }').")+ .deprecate_tdata_msg() |
|
103 | +20 |
-
+ } |
|
104 | +21 |
- # argument checking (independent)+ |
|
105 | +22 |
- ## `data`- |
- |
106 | -11x | -
- if (inherits(data, "TealData")) {- |
- |
107 | -! | -
- lifecycle::deprecate_stop(+ #' @rdname tdata |
|
108 | -! | +||
23 | +
- when = "0.15.0",+ #' @export |
||
109 | -! | +||
24 | +
- what = "init(data)",+ tdata2env <- function(...) { |
||
110 | +25 | ! |
- paste(+ .deprecate_tdata_msg() |
111 | -! | +||
26 | +
- "TealData is no longer supported. Use teal_data() instead.",+ } |
||
112 | -! | +||
27 | +
- "Please follow migration instructions https://github.com/insightsengineering/teal/discussions/988."+ |
||
113 | +28 |
- )+ #' @rdname tdata |
|
114 | +29 |
- )+ #' @export |
|
115 | +30 |
- }+ get_code_tdata <- function(...) { |
|
116 | -11x | +||
31 | +! |
- checkmate::assert_multi_class(data, c("teal_data", "teal_data_module"))+ .deprecate_tdata_msg() |
|
117 | -11x | +||
32 | +
- checkmate::assert_class(landing_popup, "teal_module_landing", null.ok = TRUE)+ } |
||
118 | +33 | ||
119 | +34 |
- ## `modules`+ #' @rdname tdata |
|
120 | -11x | +||
35 | +
- checkmate::assert(+ #' @export |
||
121 | -11x | +||
36 | +
- .var.name = "modules",+ join_keys.tdata <- function(...) { |
||
122 | -11x | +||
37 | +! |
- checkmate::check_multi_class(modules, c("teal_modules", "teal_module")),+ .deprecate_tdata_msg() |
|
123 | -11x | +||
38 | +
- checkmate::check_list(modules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules"))+ } |
||
124 | +39 |
- )+ |
|
125 | -11x | +||
40 | +
- if (inherits(modules, "teal_module")) {+ #' @rdname tdata |
||
126 | -1x | +||
41 | +
- modules <- list(modules)+ #' @export |
||
127 | +42 |
- }+ get_metadata <- function(...) { |
|
128 | -11x | +||
43 | +! |
- if (checkmate::test_list(modules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules"))) {+ .deprecate_tdata_msg() |
|
129 | -5x | +||
44 | +
- modules <- do.call(teal::modules, modules)+ } |
||
130 | +45 |
- }+ |
|
131 | +46 |
-
+ #' @rdname tdata |
|
132 | +47 |
- ## `filter`+ #' @export |
|
133 | -11x | +||
48 | +
- checkmate::assert_class(filter, "teal_slices")+ as_tdata <- function(...) {+ |
+ ||
49 | +! | +
+ .deprecate_tdata_msg() |
|
134 | +50 |
-
+ } |
|
135 | +51 |
- ## all other arguments+ |
|
136 | -10x | +||
52 | +
- checkmate::assert(+ |
||
137 | -10x | +||
53 | +
- .var.name = "title",+ .deprecate_tdata_msg <- function() { |
||
138 | -10x | +||
54 | +! |
- checkmate::check_string(title),+ lifecycle::deprecate_stop( |
|
139 | -10x | +||
55 | +! |
- checkmate::check_multi_class(title, c("shiny.tag", "shiny.tag.list", "html"))+ when = "0.16", |
|
140 | -+ | ||
56 | +! |
- )+ what = "tdata()", |
|
141 | -10x | +||
57 | +! |
- checkmate::assert(+ details = paste( |
|
142 | -10x | +||
58 | +! |
- .var.name = "header",+ "tdata has been removed in favour of `teal_data`.\n", |
|
143 | -10x | +||
59 | +! |
- checkmate::check_string(header),+ "Please follow migration instructions https://github.com/insightsengineering/teal/discussions/987." |
|
144 | -10x | +||
60 | +
- checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html"))+ ) |
||
145 | +61 |
) |
|
146 | -10x | +||
62 | +
- checkmate::assert(+ } |
||
147 | -10x | +
1 | +
- .var.name = "footer",+ #' `teal` main module |
||
148 | -10x | +||
2 | +
- checkmate::check_string(footer),+ #' |
||
149 | -10x | +||
3 | +
- checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html"))+ #' @description |
||
150 | +4 |
- )+ #' `r lifecycle::badge("stable")` |
|
151 | -10x | +||
5 | +
- checkmate::assert_character(id, max.len = 1, any.missing = FALSE)+ #' Module to create a `teal` app. This module can be called directly instead of [init()] and |
||
152 | +6 |
-
+ #' included in your custom application. Please note that [init()] adds `reporter_previewer_module` |
|
153 | +7 |
- # log+ #' automatically, which is not a case when calling `ui/srv_teal` directly. |
|
154 | -10x | +||
8 | +
- teal.logger::log_system_info()+ #' |
||
155 | +9 |
-
+ #' @details |
|
156 | +10 |
- # argument transformations+ #' |
|
157 | +11 |
- ## `modules` - landing module+ #' Module is responsible for creating the main `shiny` app layout and initializing all the necessary |
|
158 | -10x | +||
12 | +
- landing <- extract_module(modules, "teal_module_landing")+ #' components. This module establishes reactive connection between the input `data` and every other |
||
159 | -10x | +||
13 | +
- if (length(landing) == 1L) {+ #' component in the app. Reactive change of the `data` passed as an argument, reloads the app and |
||
160 | -! | +||
14 | +
- landing_popup <- landing[[1L]]+ #' possibly keeps all input settings the same so the user can continue where one left off. |
||
161 | -! | +||
15 | +
- modules <- drop_module(modules, "teal_module_landing")+ #' |
||
162 | -! | +||
16 | +
- lifecycle::deprecate_soft(+ #' ## data flow in `teal` application |
||
163 | -! | +||
17 | +
- when = "0.15.3",+ #' |
||
164 | -! | +||
18 | +
- what = "landing_popup_module()",+ #' This module supports multiple data inputs but eventually, they are all converted to `reactive` |
||
165 | -! | +||
19 | +
- details = paste(+ #' returning `teal_data` in this module. On this `reactive teal_data` object several actions are |
||
166 | -! | +||
20 | +
- "Pass `landing_popup_module` to the `landing_popup` argument of the `init` ",+ #' performed: |
||
167 | -! | +||
21 | +
- "instead of wrapping it into `modules()` and passing to the `modules` argument"+ #' - data loading in [`module_init_data`] |
||
168 | +22 |
- )+ #' - data filtering in [`module_filter_data`] |
|
169 | +23 |
- )+ #' - data transformation in [`module_transform_data`] |
|
170 | -10x | +||
24 | +
- } else if (length(landing) > 1L) {+ #' |
||
171 | -! | +||
25 | +
- stop("Only one `landing_popup_module` can be used.")+ #' ## Fallback on failure |
||
172 | +26 |
- }+ #' |
|
173 | +27 |
-
+ #' `teal` is designed in such way that app will never crash if the error is introduced in any |
|
174 | +28 |
- ## `filter` - set app_id attribute unless present (when restoring bookmark)+ #' custom `shiny` module provided by app developer (e.g. [teal_data_module()], [teal_transform_module()]). |
|
175 | -10x | +||
29 | +
- if (is.null(attr(filter, "app_id", exact = TRUE))) attr(filter, "app_id") <- create_app_id(data, modules)+ #' If any module returns a failing object, the app will halt the evaluation and display a warning message. |
||
176 | +30 |
-
+ #' App user should always have a chance to fix the improper input and continue without restarting the session. |
|
177 | +31 |
- ## `filter` - convert teal.slice::teal_slices to teal::teal_slices+ #' |
|
178 | -10x | +||
32 | +
- filter <- as.teal_slices(as.list(filter))+ #' @rdname module_teal |
||
179 | +33 |
-
+ #' @name module_teal |
|
180 | +34 |
- # argument checking (interdependent)+ #' |
|
181 | +35 |
- ## `filter` - `modules`+ #' @inheritParams module_init_data |
|
182 | -10x | +||
36 | +
- if (isTRUE(attr(filter, "module_specific"))) {+ #' @inheritParams init |
||
183 | -! | +||
37 | +
- module_names <- unlist(c(module_labels(modules), "global_filters"))+ #' |
||
184 | -! | +||
38 | +
- failed_mod_names <- setdiff(names(attr(filter, "mapping")), module_names)+ #' @return `NULL` invisibly |
||
185 | -! | +||
39 | +
- if (length(failed_mod_names)) {+ NULL |
||
186 | -! | +||
40 | +
- stop(+ |
||
187 | -! | +||
41 | +
- sprintf(+ #' @rdname module_teal |
||
188 | -! | +||
42 | +
- "Some module names in the mapping arguments don't match module labels.\n %s not in %s",+ #' @export |
||
189 | -! | +||
43 | +
- toString(failed_mod_names),+ ui_teal <- function(id, |
||
190 | -! | +||
44 | +
- toString(unique(module_names))+ modules, |
||
191 | +45 |
- )+ title = build_app_title(), |
|
192 | +46 |
- )+ header = tags$p(), |
|
193 | +47 |
- }+ footer = tags$p()) { |
|
194 | -+ | ||
48 | +! |
-
+ checkmate::assert_character(id, max.len = 1, any.missing = FALSE) |
|
195 | +49 | ! |
- if (anyDuplicated(module_names)) {+ checkmate::assert( |
196 | -+ | ||
50 | +! |
- # In teal we are able to set nested modules with duplicated label.+ .var.name = "title", |
|
197 | -+ | ||
51 | +! |
- # Because mapping argument bases on the relationship between module-label and filter-id,+ checkmate::check_string(title),+ |
+ |
52 | +! | +
+ checkmate::check_multi_class(title, c("shiny.tag", "shiny.tag.list", "html")) |
|
198 | +53 |
- # it is possible that module-label in mapping might refer to multiple teal_module (identified by the same label)+ ) |
|
199 | +54 | ! |
- stop(+ checkmate::assert( |
200 | +55 | ! |
- sprintf(+ .var.name = "header", |
201 | +56 | ! |
- "Module labels should be unique when teal_slices(mapping = TRUE). Duplicated labels:\n%s ",+ checkmate::check_string(header), |
202 | +57 | ! |
- toString(module_names[duplicated(module_names)])+ checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html")) |
203 | +58 |
- )+ ) |
|
204 | -+ | ||
59 | +! |
- )+ checkmate::assert( |
|
205 | -+ | ||
60 | +! |
- }+ .var.name = "footer",+ |
+ |
61 | +! | +
+ checkmate::check_string(footer),+ |
+ |
62 | +! | +
+ checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html")) |
|
206 | +63 |
- }+ ) |
|
207 | +64 | ||
208 | -+ | ||
65 | +! |
- ## `data` - `modules`+ if (is.character(title)) { |
|
209 | -10x | +||
66 | +! |
- if (inherits(data, "teal_data")) {+ title <- build_app_title(title) |
|
210 | -9x | +||
67 | +
- if (length(ls(teal.code::get_env(data))) == 0) {+ } else { |
||
211 | -1x | +||
68 | +! |
- stop("The environment of `data` is empty.")+ validate_app_title_tag(title) |
|
212 | +69 |
- }+ } |
|
213 | +70 | ||
214 | -8x | -
- is_modules_ok <- check_modules_datanames(modules, ls(teal.code::get_env(data)))- |
- |
215 | -8x | +||
71 | +! |
- if (!isTRUE(is_modules_ok) && length(unlist(extract_transformers(modules))) == 0) {+ if (checkmate::test_string(header)) { |
|
216 | -7x | +||
72 | +! |
- lapply(is_modules_ok$string, warning, call. = FALSE)+ header <- tags$p(header) |
|
217 | +73 |
- }+ } |
|
218 | +74 | ||
219 | -8x | +||
75 | +! |
- is_filter_ok <- check_filter_datanames(filter, ls(teal.code::get_env(data)))+ if (checkmate::test_string(footer)) { |
|
220 | -8x | +||
76 | +! |
- if (!isTRUE(is_filter_ok)) {+ footer <- tags$p(footer) |
|
221 | -1x | +||
77 | +
- warning(is_filter_ok)+ } |
||
222 | +78 |
- # we allow app to continue if applied filters are outside+ |
|
223 | -+ | ||
79 | +! |
- # of possible data range+ ns <- NS(id) |
|
224 | +80 |
- }+ |
|
225 | +81 |
- }+ # show busy icon when `shiny` session is busy computing stuff |
|
226 | +82 |
-
+ # based on https://stackoverflow.com/questions/17325521/r-shiny-display-loading-message-while-function-is-running/22475216#22475216 # nolint: line_length. |
|
227 | -9x | +||
83 | +! |
- reporter <- teal.reporter::Reporter$new()$set_id(attr(filter, "app_id"))+ shiny_busy_message_panel <- conditionalPanel( |
|
228 | -9x | +||
84 | +! |
- if (is_arg_used(modules, "reporter") && length(extract_module(modules, "teal_module_previewer")) == 0) {+ condition = "(($('html').hasClass('shiny-busy')) && (document.getElementById('shiny-notification-panel') == null))", # nolint: line_length. |
|
229 | +85 | ! |
- modules <- append_module(+ tags$div( |
230 | +86 | ! |
- modules,+ icon("arrows-rotate", class = "fa-spin", prefer_type = "solid"), |
231 | +87 | ! |
- reporter_previewer_module(server_args = list(previewer_buttons = c("download", "reset")))+ "Computing ...", |
232 | +88 |
- )+ # CSS defined in `custom.css` |
|
233 | -+ | ||
89 | +! |
- }+ class = "shinybusymessage" |
|
234 | +90 |
-
+ ) |
|
235 | -9x | +||
91 | +
- ns <- NS(id)+ ) |
||
236 | +92 |
- # Note: UI must be a function to support bookmarking.+ |
|
237 | -9x | +||
93 | +! |
- res <- list(+ fluidPage( |
|
238 | -9x | +||
94 | +! |
- ui = function(request) {+ id = id, |
|
239 | +95 | ! |
- ui_teal(+ title = title, |
240 | +96 | ! |
- id = ns("teal"),+ theme = get_teal_bs_theme(), |
241 | +97 | ! |
- data = if (inherits(data, "teal_data_module")) data,+ include_teal_css_js(), |
242 | +98 | ! |
- modules = modules,+ tags$header(header), |
243 | +99 | ! |
- title = title,+ tags$hr(class = "my-2"), |
244 | +100 | ! |
- header = header,+ shiny_busy_message_panel, |
245 | +101 | ! |
- footer = footer+ tags$div( |
246 | -- |
- )+ | |
102 | +! | +
+ id = ns("tabpanel_wrapper"),+ |
+ |
103 | +! | +
+ class = "teal-body",+ |
+ |
104 | +! | +
+ ui_teal_module(id = ns("teal_modules"), modules = modules) |
|
247 | +105 |
- },+ ), |
|
248 | -9x | +||
106 | +! |
- server = function(input, output, session) {+ tags$div( |
|
249 | +107 | ! |
- if (!is.null(landing_popup)) {+ id = ns("options_buttons"), |
250 | +108 | ! |
- do.call(landing_popup$server, c(list(id = "landing_module_shiny_id"), landing_popup$server_args))+ style = "position: absolute; right: 10px;", |
251 | -+ | ||
109 | +! |
- }+ ui_bookmark_panel(ns("bookmark_manager"), modules), |
|
252 | +110 | ! |
- srv_teal(id = ns("teal"), data = data, modules = modules, filter = deep_copy_filter(filter))+ tags$button( |
253 | -+ | ||
111 | +! |
- }+ class = "btn action-button filter_hamburger", # see sidebar.css for style filter_hamburger |
|
254 | -+ | ||
112 | +! |
- )+ href = "javascript:void(0)", |
|
255 | -+ | ||
113 | +! |
-
+ onclick = sprintf("toggleFilterPanel('%s');", ns("tabpanel_wrapper")), |
|
256 | -9x | +||
114 | +! |
- logger::log_debug("init teal app has been initialized.")+ title = "Toggle filter panel",+ |
+ |
115 | +! | +
+ icon("fas fa-bars") |
|
257 | +116 |
-
+ ), |
|
258 | -9x | +||
117 | +! |
- res+ ui_snapshot_manager_panel(ns("snapshot_manager_panel")), |
|
259 | -+ | ||
118 | +! |
- }+ ui_filter_manager_panel(ns("filter_manager_panel")) |
1 | +119 |
- #' App state management.+ ), |
||
2 | -+ | |||
120 | +! |
- #'+ tags$script( |
||
3 | -+ | |||
121 | +! |
- #' @description+ HTML( |
||
4 | -+ | |||
122 | +! |
- #' `r lifecycle::badge("experimental")`+ sprintf( |
||
5 | +123 |
- #'+ " |
||
6 | -+ | |||
124 | +! |
- #' Capture and restore the global (app) input state.+ $(document).ready(function() { |
||
7 | -+ | |||
125 | +! |
- #'+ $('#%s').appendTo('#%s'); |
||
8 | +126 |
- #' @details+ }); |
||
9 | +127 |
- #' This module introduces bookmarks into `teal` apps: the `shiny` bookmarking mechanism becomes enabled+ ", |
||
10 | -+ | |||
128 | +! |
- #' and server-side bookmarks can be created.+ ns("options_buttons"), |
||
11 | -+ | |||
129 | +! |
- #'+ ns("teal_modules-active_tab") |
||
12 | +130 |
- #' The bookmark manager presents a button with the bookmark icon and is placed in the tab-bar.+ ) |
||
13 | +131 |
- #' When clicked, the button creates a bookmark and opens a modal which displays the bookmark URL.+ ) |
||
14 | +132 |
- #'+ ), |
||
15 | -+ | |||
133 | +! |
- #' `teal` does not guarantee that all modules (`teal_module` objects) are bookmarkable.+ tags$hr(), |
||
16 | -+ | |||
134 | +! |
- #' Those that are, have a `teal_bookmarkable` attribute set to `TRUE`. If any modules are not bookmarkable,+ tags$footer( |
||
17 | -+ | |||
135 | +! |
- #' the bookmark manager modal displays a warning and the bookmark button displays a flag.+ tags$div( |
||
18 | -+ | |||
136 | +! |
- #' In order to communicate that a external module is bookmarkable, the module developer+ footer, |
||
19 | -+ | |||
137 | +! |
- #' should set the `teal_bookmarkable` attribute to `TRUE`.+ teal.widgets::verbatim_popup_ui(ns("sessionInfo"), "Session Info", type = "link"), |
||
20 | -+ | |||
138 | +! |
- #'+ br(), |
||
21 | -+ | |||
139 | +! |
- #' @section Server logic:+ ui_teal_lockfile(ns("lockfile")), |
||
22 | -+ | |||
140 | +! |
- #' A bookmark is a URL that contains the app address with a `/?_state_id_=<bookmark_dir>` suffix.+ textOutput(ns("identifier")) |
||
23 | +141 |
- #' `<bookmark_dir>` is a directory created on the server, where the state of the application is saved.+ ) |
||
24 | +142 |
- #' Accessing the bookmark URL opens a new session of the app that starts in the previously saved state.+ ) |
||
25 | +143 |
- #'+ ) |
||
26 | +144 |
- #' @section Note:+ } |
||
27 | +145 |
- #' To enable bookmarking use either:+ |
||
28 | +146 |
- #' - `shiny` app by using `shinyApp(..., enableBookmarking = "server")` (not supported in `shinytest2`)+ #' @rdname module_teal |
||
29 | +147 |
- #' - set `options(shiny.bookmarkStore = "server")` before running the app+ #' @export |
||
30 | +148 |
- #'+ srv_teal <- function(id, data, modules, filter = teal_slices()) { |
||
31 | -+ | |||
149 | +72x |
- #'+ checkmate::assert_character(id, max.len = 1, any.missing = FALSE) |
||
32 | -+ | |||
150 | +72x |
- #' @inheritParams init+ checkmate::assert_multi_class(data, c("teal_data", "teal_data_module", "reactive")) |
||
33 | -+ | |||
151 | +71x |
- #'+ checkmate::assert_class(modules, "teal_modules") |
||
34 | -+ | |||
152 | +71x |
- #' @return Invisible `NULL`.+ checkmate::assert_class(filter, "teal_slices") |
||
35 | +153 |
- #'+ |
||
36 | -+ | |||
154 | +71x |
- #' @aliases bookmark bookmark_manager bookmark_manager_module+ moduleServer(id, function(input, output, session) { |
||
37 | -+ | |||
155 | +71x |
- #'+ logger::log_debug("srv_teal initializing.") |
||
38 | +156 |
- #' @name module_bookmark_manager+ |
||
39 | -+ | |||
157 | +71x |
- #' @rdname module_bookmark_manager+ if (getOption("teal.show_js_log", default = FALSE)) { |
||
40 | -+ | |||
158 | +! |
- #'+ shinyjs::showLog() |
||
41 | +159 |
- #' @keywords internal+ } |
||
42 | +160 |
- #'+ |
||
43 | -+ | |||
161 | +71x |
- NULL+ srv_teal_lockfile("lockfile") |
||
44 | +162 | |||
163 | +71x | +
+ output$identifier <- renderText(+ |
+ ||
164 | +71x | +
+ paste0("Pid:", Sys.getpid(), " Token:", substr(session$token, 25, 32))+ |
+ ||
45 | +165 |
- #' @rdname module_bookmark_manager+ ) |
||
46 | +166 |
- ui_bookmark_panel <- function(id, modules) {+ |
||
47 | -! | +|||
167 | +71x |
- ns <- NS(id)+ teal.widgets::verbatim_popup_srv( |
||
48 | -+ | |||
168 | +71x |
-
+ "sessionInfo", |
||
49 | -! | +|||
169 | +71x |
- bookmark_option <- get_bookmarking_option()+ verbatim_content = utils::capture.output(utils::sessionInfo()), |
||
50 | -! | +|||
170 | +71x |
- is_unbookmarkable <- need_bookmarking(modules)+ title = "SessionInfo" |
||
51 | -! | +|||
171 | +
- shinyOptions(bookmarkStore = bookmark_option)+ ) |
|||
52 | +172 | |||
53 | +173 |
- # Render bookmark warnings count+ # `JavaScript` code |
||
54 | -! | +|||
174 | +71x |
- if (!all(is_unbookmarkable) && identical(bookmark_option, "server")) {+ run_js_files(files = "init.js") |
||
55 | -! | +|||
175 | +
- tags$button(+ |
|||
56 | -! | +|||
176 | +
- id = ns("do_bookmark"),+ # set timezone in shiny app |
|||
57 | -! | +|||
177 | +
- class = "btn action-button wunder_bar_button bookmark_manager_button",+ # timezone is set in the early beginning so it will be available also |
|||
58 | -! | +|||
178 | +
- title = "Add bookmark",+ # for `DDL` and all shiny modules |
|||
59 | -! | +|||
179 | +71x |
- tags$span(+ get_client_timezone(session$ns) |
||
60 | -! | +|||
180 | +71x |
- suppressMessages(icon("fas fa-bookmark")),+ observeEvent( |
||
61 | -! | -
- if (any(is_unbookmarkable)) {+ | ||
181 | +71x | +
+ eventExpr = input$timezone, |
||
62 | -! | +|||
182 | +71x |
- tags$span(+ once = TRUE,+ |
+ ||
183 | +71x | +
+ handlerExpr = { |
||
63 | +184 | ! |
- sum(is_unbookmarkable),+ session$userData$timezone <- input$timezone |
|
64 | +185 | ! |
- class = "badge-warning badge-count text-white bg-danger"+ logger::log_debug("srv_teal@1 Timezone set to client's timezone: { input$timezone }.") |
|
65 | +186 |
- )+ } |
||
66 | +187 |
- }+ ) |
||
67 | +188 |
- )+ |
||
68 | -+ | |||
189 | +71x |
- )+ data_pulled <- srv_init_data("data", data = data) |
||
69 | -+ | |||
190 | +70x |
- }+ data_validated <- srv_validate_reactive_teal_data( |
||
70 | -+ | |||
191 | +70x |
- }+ "validate", |
||
71 | -+ | |||
192 | +70x |
-
+ data = data_pulled, |
||
72 | -+ | |||
193 | +70x |
- #' @rdname module_bookmark_manager+ modules = modules, |
||
73 | -+ | |||
194 | +70x |
- srv_bookmark_panel <- function(id, modules) {+ validate_shiny_silent_error = FALSE |
||
74 | -69x | +|||
195 | +
- checkmate::assert_character(id)+ ) |
|||
75 | -69x | +196 | +70x |
- checkmate::assert_class(modules, "teal_modules")+ data_rv <- reactive({ |
76 | -69x | +197 | +116x |
- moduleServer(id, function(input, output, session) {+ req(inherits(data_validated(), "teal_data")) |
77 | -69x | +198 | +57x |
- logger::log_debug("bookmark_manager_srv initializing")+ is_filter_ok <- check_filter_datanames(filter, ls(teal.code::get_env(data_validated()))) |
78 | -69x | +199 | +57x |
- ns <- session$ns+ if (!isTRUE(is_filter_ok)) { |
79 | -69x | +200 | +2x |
- bookmark_option <- get_bookmarking_option()+ showNotification( |
80 | -69x | +201 | +2x |
- is_unbookmarkable <- need_bookmarking(modules)+ "Some filters were not applied because of incompatibility with data. Contact app developer.", |
81 | -+ | |||
202 | +2x |
-
+ type = "warning", |
||
82 | -+ | |||
203 | +2x |
- # Set up bookmarking callbacks ----+ duration = 10 |
||
83 | +204 |
- # Register bookmark exclusions: do_bookmark button to avoid re-bookmarking+ ) |
||
84 | -69x | +205 | +2x |
- setBookmarkExclude(c("do_bookmark"))+ warning(is_filter_ok) |
85 | +206 |
- # This bookmark can only be used on the app session.- |
- ||
86 | -69x | -
- app_session <- .subset2(session, "parent")+ } |
||
87 | -69x | +207 | +57x |
- app_session$onBookmarked(function(url) {+ .add_signature_to_data(data_validated()) |
88 | -! | +|||
208 | +
- logger::log_debug("bookmark_manager_srv@onBookmarked: bookmark button clicked, registering bookmark")+ }) |
|||
89 | -! | +|||
209 | +
- modal_content <- if (bookmark_option != "server") {+ |
|||
90 | -! | +|||
210 | +70x |
- msg <- sprintf(+ data_load_status <- reactive({ |
||
91 | -! | +|||
211 | +61x |
- "Bookmarking has been set to \"%s\".\n%s\n%s",+ if (inherits(data_pulled(), "teal_data")) { |
||
92 | -! | +|||
212 | +57x |
- bookmark_option,+ "ok" |
||
93 | -! | +|||
213 | +4x |
- "Only server-side bookmarking is supported.",+ } else if (inherits(data, "teal_data_module")) { |
||
94 | -! | +|||
214 | +4x |
- "Please contact your app developer."+ "teal_data_module failed" |
||
95 | +215 |
- )- |
- ||
96 | -! | -
- tags$div(+ } else { |
||
97 | +216 | ! |
- tags$p(msg, class = "text-warning")+ "external failed" |
|
98 | +217 |
- )+ } |
||
99 | +218 |
- } else {- |
- ||
100 | -! | -
- tags$div(- |
- ||
101 | -! | -
- tags$span(- |
- ||
102 | -! | -
- tags$pre(url)+ }) |
||
103 | +219 |
- ),- |
- ||
104 | -! | -
- if (any(is_unbookmarkable)) {- |
- ||
105 | -! | -
- bkmb_summary <- rapply2(- |
- ||
106 | -! | -
- modules_bookmarkable(modules),+ |
||
107 | -! | +|||
220 | +70x |
- function(x) {+ datasets_rv <- if (!isTRUE(attr(filter, "module_specific"))) { |
||
108 | -! | +|||
221 | +61x |
- if (isTRUE(x)) {+ eventReactive(data_rv(), { |
||
109 | -! | +|||
222 | +50x |
- "\u2705" # check mark+ req(inherits(data_rv(), "teal_data")) |
||
110 | -! | +|||
223 | +50x |
- } else if (isFALSE(x)) {+ logger::log_debug("srv_teal@1 initializing FilteredData") |
||
111 | -! | +|||
224 | +50x |
- "\u274C" # cross mark+ teal_data_to_filtered_data(data_rv()) |
||
112 | +225 |
- } else {+ }) |
||
113 | -! | +|||
226 | +
- "\u2753" # question mark+ } |
|||
114 | +227 |
- }+ |
||
115 | -+ | |||
228 | +70x |
- }+ if (inherits(data, "teal_data_module")) { |
||
116 | -+ | |||
229 | +8x |
- )+ setBookmarkExclude(c("teal_modules-active_tab")) |
||
117 | -! | +|||
230 | +8x |
- tags$div(+ shiny::insertTab( |
||
118 | -! | +|||
231 | +8x |
- tags$p(+ inputId = "teal_modules-active_tab", |
||
119 | -! | +|||
232 | +8x |
- icon("fas fa-exclamation-triangle"),+ position = "before", |
||
120 | -! | +|||
233 | +8x |
- "Some modules will not be restored when using this bookmark.",+ select = TRUE, |
||
121 | -! | +|||
234 | +8x |
- tags$br(),+ tabPanel( |
||
122 | -! | +|||
235 | +8x |
- "Check the list below to see which modules are not bookmarkable.",+ title = icon("fas fa-database"), |
||
123 | -! | +|||
236 | +8x |
- class = "text-warning"+ value = "teal_data_module", |
||
124 | -+ | |||
237 | +8x |
- ),+ tags$div( |
||
125 | -! | +|||
238 | +8x |
- tags$pre(yaml::as.yaml(bkmb_summary))+ ui_init_data(session$ns("data")), |
||
126 | -+ | |||
239 | +8x |
- )+ ui_validate_reactive_teal_data(session$ns("validate")) |
||
127 | +240 |
- }+ ) |
||
128 | +241 |
) |
||
129 | +242 |
- }+ ) |
||
130 | +243 | |||
131 | -! | -
- showModal(- |
- ||
132 | -! | +|||
244 | +8x |
- modalDialog(+ if (attr(data, "once")) { |
||
133 | -! | +|||
245 | +8x |
- id = ns("bookmark_modal"),+ observeEvent(data_rv(), once = TRUE, { |
||
134 | -! | +|||
246 | +3x |
- title = "Bookmarked teal app url",+ logger::log_debug("srv_teal@2 removing data tab.") |
||
135 | -! | +|||
247 | +
- modal_content,+ # when once = TRUE we pull data once and then remove data tab |
|||
136 | -! | +|||
248 | +3x |
- easyClose = TRUE+ removeTab("teal_modules-active_tab", target = "teal_data_module") |
||
137 | +249 |
- )+ }) |
||
138 | +250 |
- )+ } |
||
139 | +251 |
- })+ } else { |
||
140 | +252 |
-
+ # when no teal_data_module then we want to display messages above tabsetPanel (because there is no data-tab) |
||
141 | -+ | |||
253 | +62x |
- # manually trigger bookmarking because of the problems reported on windows with bookmarkButton in teal+ insertUI( |
||
142 | -69x | +254 | +62x |
- observeEvent(input$do_bookmark, {+ selector = sprintf("#%s", session$ns("tabpanel_wrapper")), |
143 | -! | +|||
255 | +62x |
- logger::log_debug("bookmark_manager_srv@1 do_bookmark module clicked.")+ where = "beforeBegin", |
||
144 | -! | +|||
256 | +62x |
- session$doBookmark()+ ui = tags$div(ui_validate_reactive_teal_data(session$ns("validate")), tags$br()) |
||
145 | +257 |
- })+ ) |
||
146 | +258 |
-
+ } |
||
147 | -69x | +|||
259 | +
- invisible(NULL)+ |
|||
148 | -+ | |||
260 | +70x |
- })+ module_labels <- unlist(module_labels(modules), use.names = FALSE) |
||
149 | -+ | |||
261 | +70x |
- }+ slices_global <- methods::new(".slicesGlobal", filter, module_labels) |
||
150 | -+ | |||
262 | +70x |
-
+ modules_output <- srv_teal_module( |
||
151 | -+ | |||
263 | +70x |
-
+ id = "teal_modules", |
||
152 | -+ | |||
264 | +70x |
- #' @rdname module_bookmark_manager+ data_rv = data_rv, |
||
153 | -+ | |||
265 | +70x |
- get_bookmarking_option <- function() {+ datasets = datasets_rv, |
||
154 | -69x | +266 | +70x |
- bookmark_option <- getShinyOption("bookmarkStore")+ modules = modules, |
155 | -69x | +267 | +70x |
- if (is.null(bookmark_option) && identical(getOption("shiny.bookmarkStore"), "server")) {+ slices_global = slices_global, |
156 | -! | +|||
268 | +70x |
- bookmark_option <- getOption("shiny.bookmarkStore")+ data_load_status = data_load_status |
||
157 | +269 |
- }+ ) |
||
158 | +270 | 69x |
- bookmark_option+ mapping_table <- srv_filter_manager_panel("filter_manager_panel", slices_global = slices_global) |
|
159 | -+ | |||
271 | +69x |
- }+ snapshots <- srv_snapshot_manager_panel("snapshot_manager_panel", slices_global = slices_global) |
||
160 | -+ | |||
272 | +69x |
-
+ srv_bookmark_panel("bookmark_manager", modules) |
||
161 | +273 |
- #' @rdname module_bookmark_manager+ }) |
||
162 | +274 |
- need_bookmarking <- function(modules) {+ |
||
163 | +275 | 69x |
- unlist(rapply2(+ invisible(NULL) |
|
164 | -69x | +|||
276 | +
- modules_bookmarkable(modules),+ } |
|||
165 | -69x | +
1 | +
- Negate(isTRUE)+ setOldClass("teal_module") |
|||
166 | +2 |
- ))+ setOldClass("teal_modules") |
||
167 | +3 |
- }+ |
||
168 | +4 |
-
+ #' Create `teal_module` and `teal_modules` objects |
||
169 | +5 |
-
+ #' |
||
170 | +6 |
- # utilities ----+ #' @description |
||
171 | +7 |
-
+ #' `r lifecycle::badge("stable")` |
||
172 | +8 |
- #' Restore value from bookmark.+ #' Create a nested tab structure to embed modules in a `teal` application. |
||
173 | +9 |
#' |
||
174 | +10 |
- #' Get value from bookmark or return default.+ #' @details |
||
175 | +11 |
- #'+ #' `module()` creates an instance of a `teal_module` that can be placed in a `teal` application. |
||
176 | +12 |
- #' Bookmarks can store not only inputs but also arbitrary values.+ #' `modules()` shapes the structure of a the application by organizing `teal_module` within the navigation panel. |
||
177 | +13 |
- #' These values are stored by `onBookmark` callbacks and restored by `onBookmarked` callbacks,+ #' It wraps `teal_module` and `teal_modules` objects in a `teal_modules` object, |
||
178 | +14 |
- #' and they are placed in the `values` environment in the `session$restoreContext` field.+ #' which results in a nested structure corresponding to the nested tabs in the final application. |
||
179 | +15 |
- #' Using `teal_data_module` makes it impossible to run the callbacks+ #' |
||
180 | +16 |
- #' because the app becomes ready before modules execute and callbacks are registered.+ #' Note that for `modules()` `label` comes after `...`, so it must be passed as a named argument, |
||
181 | +17 |
- #' In those cases the stored values can still be recovered from the `session` object directly.+ #' otherwise it will be captured by `...`. |
||
182 | +18 |
#' |
||
183 | +19 |
- #' Note that variable names in the `values` environment are prefixed with module name space names,+ #' The labels `"global_filters"` and `"Report previewer"` are reserved |
||
184 | +20 |
- #' therefore, when using this function in modules, `value` must be run through the name space function.+ #' because they are used by the `mapping` argument of [teal_slices()] |
||
185 | +21 |
- #'+ #' and the report previewer module [reporter_previewer_module()], respectively. |
||
186 | +22 |
- #' @param value (`character(1)`) name of value to restore+ #' |
||
187 | +23 |
- #' @param default fallback value+ #' @param label (`character(1)`) Label shown in the navigation item for the module or module group. |
||
188 | +24 |
- #'+ #' For `modules()` defaults to `"root"`. See `Details`. |
||
189 | +25 |
- #' @return+ #' @param server (`function`) `shiny` module with following arguments: |
||
190 | +26 |
- #' In an application restored from a server-side bookmark,+ #' - `id` - `teal` will set proper `shiny` namespace for this module (see [shiny::moduleServer()]). |
||
191 | +27 |
- #' the variable specified by `value` from the `values` environment.+ #' - `input`, `output`, `session` - (optional; not recommended) When provided, then [shiny::callModule()] |
||
192 | +28 |
- #' Otherwise `default`.+ #' will be used to call a module. From `shiny` 1.5.0, the recommended way is to use |
||
193 | +29 |
- #'+ #' [shiny::moduleServer()] instead which doesn't require these arguments. |
||
194 | +30 |
- #' @keywords internal+ #' - `data` (optional) When provided, the module will be called with `teal_data` object (i.e. a list of |
||
195 | +31 |
- #'+ #' reactive (filtered) data specified in the `filters` argument) as the value of this argument. |
||
196 | +32 |
- restoreValue <- function(value, default) { # nolint: object_name.+ #' - `datasets` (optional) When provided, the module will be called with `FilteredData` object as the |
||
197 | -138x | +|||
33 | +
- checkmate::assert_character("value")+ #' value of this argument. (See [`teal.slice::FilteredData`]). |
|||
198 | -138x | +|||
34 | +
- session_default <- shiny::getDefaultReactiveDomain()+ #' - `reporter` (optional) When provided, the module will be called with `Reporter` object as the value |
|||
199 | -138x | +|||
35 | +
- session_parent <- .subset2(session_default, "parent")+ #' of this argument. (See [`teal.reporter::Reporter`]). |
|||
200 | -138x | +|||
36 | +
- session <- if (is.null(session_parent)) session_default else session_parent+ #' - `filter_panel_api` (optional) When provided, the module will be called with `FilterPanelAPI` object |
|||
201 | +37 |
-
+ #' as the value of this argument. (See [`teal.slice::FilterPanelAPI`]). |
||
202 | -138x | +|||
38 | +
- if (isTRUE(session$restoreContext$active) && exists(value, session$restoreContext$values, inherits = FALSE)) {+ #' - `...` (optional) When provided, `server_args` elements will be passed to the module named argument |
|||
203 | -! | +|||
39 | +
- session$restoreContext$values[[value]]+ #' or to the `...`. |
|||
204 | +40 |
- } else {+ #' @param ui (`function`) `shiny` UI module function with following arguments: |
||
205 | -138x | +|||
41 | +
- default+ #' - `id` - `teal` will set proper `shiny` namespace for this module. |
|||
206 | +42 |
- }+ #' - `...` (optional) When provided, `ui_args` elements will be passed to the module named argument |
||
207 | +43 |
- }+ #' or to the `...`. |
||
208 | +44 |
-
+ #' @param filters (`character`) Deprecated. Use `datanames` instead. |
||
209 | +45 |
- #' Compare bookmarks.+ #' @param datanames (`character`) Names of the datasets that are relevant for the item. |
||
210 | +46 |
- #'+ #' The keyword `"all"` provides all datasets available in `data` passed to `teal` application. |
||
211 | +47 |
- #' Test if two bookmarks store identical state.+ #' `NULL` will hide the filter panel. |
||
212 | +48 |
- #'+ #' @param server_args (named `list`) with additional arguments passed on to the server function. |
||
213 | +49 |
- #' `input` environments are compared one variable at a time and if not identical,+ #' @param ui_args (named `list`) with additional arguments passed on to the UI function. |
||
214 | +50 |
- #' values in both bookmarks are reported. States of `datatable`s are stripped+ #' @param x (`teal_module` or `teal_modules`) Object to format/print. |
||
215 | +51 |
- #' of the `time` element before comparing because the time stamp is always different.+ #' @param indent (`integer(1)`) Indention level; each nested element is indented one level more. |
||
216 | +52 |
- #' The contents themselves are not printed as they are large and the contents are not informative.+ #' @param transformers (`list` of `teal_data_module`) that will be applied to transform the data. |
||
217 | +53 |
- #' Elements present in one bookmark and absent in the other are also reported.+ #' Each transform module UI will appear in the `teal` application, unless the `custom_ui` attribute is set on the list. |
||
218 | +54 |
- #' Differences are printed as messages.+ #' If so, the module developer is responsible to display the UI in the module itself. `datanames` of the `transformers` |
||
219 | +55 |
- #'+ #' will be added to the `datanames`. |
||
220 | +56 |
- #' `values` environments are compared with `all.equal`.+ #' |
||
221 | +57 |
- #'+ #' When the transformation does not have sufficient input data, the resulting data will fallback |
||
222 | +58 |
- #' @section How to use:+ #' to the last successful transform or, in case there are none, to the filtered data. |
||
223 | +59 |
- #' Open an application, change relevant inputs (typically, all of them), and create a bookmark.+ #' @param ... |
||
224 | +60 |
- #' Then open that bookmark and immediately create a bookmark of that.+ #' - For `modules()`: (`teal_module` or `teal_modules`) Objects to wrap into a tab. |
||
225 | +61 |
- #' If restoring bookmarks occurred properly, the two bookmarks should store the same state.+ #' - For `format()` and `print()`: Arguments passed to other methods. |
||
226 | +62 |
#' |
||
227 | +63 |
- #'+ #' @section `datanames`: |
||
228 | +64 |
- #' @param book1,book2 bookmark directories stored in `shiny_bookmarks/`;+ #' The module's `datanames` argument determines a subset of datasets from the `data` object, as specified in the |
||
229 | +65 |
- #' default to the two most recently modified directories+ #' server function argument, to be presented in the module. Datasets displayed in the filter panel will be limited |
||
230 | +66 |
- #'+ #' to this subset. |
||
231 | +67 |
- #' @return+ #' When `datanames` is set to `"all"`, all available datasets in the `data` object are considered relevant for the |
||
232 | +68 |
- #' Invisible `NULL` if bookmarks are identical or if there are no bookmarks to test.+ #' module. However, setting `datanames` argument to `"all"` might include datasets that are irrelevant for the module, |
||
233 | +69 |
- #' `FALSE` if inconsistencies are detected.+ #' for example: |
||
234 | +70 |
- #'+ #' - Proxy variables used for modifying columns. |
||
235 | +71 |
- #' @keywords internal+ #' - Modified copies of datasets used to create a final dataset. |
||
236 | +72 |
- #'+ #' - Connection objects. |
||
237 | +73 |
- bookmarks_identical <- function(book1, book2) {+ #' To prevent these irrelevant datasets from appearing in the module, use the [set_datanames()] function on the |
||
238 | -! | +|||
74 | +
- if (!dir.exists("shiny_bookmarks")) {+ #' [module] or [modules()] to change the `datanames` from `"all"` to specific dataset names. Attempting to change |
|||
239 | -! | +|||
75 | +
- message("no bookmark directory")+ #' `datanames` values that was not set to `"all"` using [set_datanames()] will be ignored with a warning. |
|||
240 | -! | +|||
76 | +
- return(invisible(NULL))+ #' |
|||
241 | +77 |
- }+ #' Additionally, datasets with names starting with `.` are ignored when `datanames` is set to `"all"`. |
||
242 | +78 |
-
+ #' |
||
243 | -! | +|||
79 | +
- ans <- TRUE+ #' @return |
|||
244 | +80 |
-
+ #' `module()` returns an object of class `teal_module`. |
||
245 | -! | -
- if (missing(book1) && missing(book2)) {+ | ||
81 | ++ |
+ #' |
||
246 | -! | +|||
82 | +
- dirs <- list.dirs("shiny_bookmarks", recursive = FALSE)+ #' `modules()` returns a `teal_modules` object which contains following fields: |
|||
247 | -! | +|||
83 | +
- bookmarks_sorted <- basename(rev(dirs[order(file.mtime(dirs))]))+ #' - `label`: taken from the `label` argument. |
|||
248 | -! | +|||
84 | +
- if (length(bookmarks_sorted) < 2L) {+ #' - `children`: a list containing objects passed in `...`. List elements are named after |
|||
249 | -! | +|||
85 | +
- message("no bookmarks to compare")+ #' their `label` attribute converted to a valid `shiny` id. |
|||
250 | -! | +|||
86 | +
- return(invisible(NULL))+ #' |
|||
251 | +87 |
- }+ #' @name teal_modules |
||
252 | -! | +|||
88 | +
- book1 <- bookmarks_sorted[2L]+ #' @aliases teal_module |
|||
253 | -! | +|||
89 | +
- book2 <- bookmarks_sorted[1L]+ #' |
|||
254 | +90 |
- } else {+ #' @examples |
||
255 | -! | +|||
91 | +
- if (!dir.exists(file.path("shiny_bookmarks", book1))) stop(book1, " not found")+ #' library(shiny) |
|||
256 | -! | +|||
92 | +
- if (!dir.exists(file.path("shiny_bookmarks", book2))) stop(book2, " not found")+ #' |
|||
257 | +93 |
- }+ #' module_1 <- module( |
||
258 | +94 |
-
+ #' label = "a module", |
||
259 | -! | +|||
95 | +
- book1_input <- readRDS(file.path("shiny_bookmarks", book1, "input.rds"))+ #' server = function(id, data) { |
|||
260 | -! | +|||
96 | +
- book2_input <- readRDS(file.path("shiny_bookmarks", book2, "input.rds"))+ #' moduleServer( |
|||
261 | +97 |
-
+ #' id, |
||
262 | -! | +|||
98 | +
- elements_common <- intersect(names(book1_input), names(book2_input))+ #' module = function(input, output, session) { |
|||
263 | -! | +|||
99 | +
- dt_states <- grepl("_state$", elements_common)+ #' output$data <- renderDataTable(data()[["iris"]]) |
|||
264 | -! | +|||
100 | +
- if (any(dt_states)) {+ #' } |
|||
265 | -! | +|||
101 | +
- for (el in elements_common[dt_states]) {+ #' ) |
|||
266 | -! | +|||
102 | +
- book1_input[[el]][["time"]] <- NULL+ #' }, |
|||
267 | -! | +|||
103 | +
- book2_input[[el]][["time"]] <- NULL+ #' ui = function(id) { |
|||
268 | +104 |
- }+ #' ns <- NS(id) |
||
269 | +105 |
- }+ #' tagList(dataTableOutput(ns("data"))) |
||
270 | +106 |
-
+ #' }, |
||
271 | -! | +|||
107 | +
- identicals <- mapply(identical, book1_input[elements_common], book2_input[elements_common])+ #' datanames = "all" |
|||
272 | -! | +|||
108 | +
- non_identicals <- names(identicals[!identicals])+ #' ) |
|||
273 | -! | +|||
109 | +
- compares <- sprintf("$ %s:\t%s --- %s", non_identicals, book1_input[non_identicals], book2_input[non_identicals])+ #' |
|||
274 | -! | +|||
110 | +
- if (length(compares) != 0L) {+ #' module_2 <- module( |
|||
275 | -! | +|||
111 | +
- message("common elements not identical: \n", paste(compares, collapse = "\n"))+ #' label = "another module", |
|||
276 | -! | +|||
112 | +
- ans <- FALSE+ #' server = function(id) { |
|||
277 | +113 |
- }+ #' moduleServer( |
||
278 | +114 |
-
+ #' id, |
||
279 | -! | +|||
115 | +
- elements_boook1 <- setdiff(names(book1_input), names(book2_input))+ #' module = function(input, output, session) { |
|||
280 | -! | +|||
116 | +
- if (length(elements_boook1) != 0L) {+ #' output$text <- renderText("Another Module") |
|||
281 | -! | +|||
117 | +
- dt_states <- grepl("_state$", elements_boook1)+ #' } |
|||
282 | -! | +|||
118 | +
- if (any(dt_states)) {+ #' ) |
|||
283 | -! | +|||
119 | +
- for (el in elements_boook1[dt_states]) {+ #' }, |
|||
284 | -! | +|||
120 | +
- if (is.list(book1_input[[el]])) book1_input[[el]] <- "--- data table state ---"+ #' ui = function(id) { |
|||
285 | +121 |
- }+ #' ns <- NS(id) |
||
286 | +122 |
- }+ #' tagList(textOutput(ns("text"))) |
||
287 | -! | +|||
123 | +
- excess1 <- sprintf("$ %s:\t%s", elements_boook1, book1_input[elements_boook1])+ #' }, |
|||
288 | -! | +|||
124 | +
- message("elements only in book1: \n", paste(excess1, collapse = "\n"))+ #' datanames = NULL |
|||
289 | -! | +|||
125 | +
- ans <- FALSE+ #' ) |
|||
290 | +126 |
- }+ #' |
||
291 | +127 |
-
+ #' modules <- modules( |
||
292 | -! | +|||
128 | +
- elements_boook2 <- setdiff(names(book2_input), names(book1_input))+ #' label = "modules", |
|||
293 | -! | +|||
129 | +
- if (length(elements_boook2) != 0L) {+ #' modules( |
|||
294 | -! | +|||
130 | +
- dt_states <- grepl("_state$", elements_boook1)+ #' label = "nested modules", |
|||
295 | -! | +|||
131 | +
- if (any(dt_states)) {+ #' module_1 |
|||
296 | -! | +|||
132 | +
- for (el in elements_boook1[dt_states]) {+ #' ), |
|||
297 | -! | +|||
133 | +
- if (is.list(book2_input[[el]])) book2_input[[el]] <- "--- data table state ---"+ #' module_2 |
|||
298 | +134 |
- }+ #' ) |
||
299 | +135 |
- }+ #' |
||
300 | -! | +|||
136 | +
- excess2 <- sprintf("$ %s:\t%s", elements_boook2, book2_input[elements_boook2])+ #' app <- init( |
|||
301 | -! | +|||
137 | +
- message("elements only in book2: \n", paste(excess2, collapse = "\n"))+ #' data = teal_data(iris = iris), |
|||
302 | -! | +|||
138 | +
- ans <- FALSE+ #' modules = modules |
|||
303 | +139 |
- }+ #' ) |
||
304 | +140 |
-
+ #' |
||
305 | -! | +|||
141 | +
- book1_values <- readRDS(file.path("shiny_bookmarks", book1, "values.rds"))+ #' if (interactive()) { |
|||
306 | -! | +|||
142 | +
- book2_values <- readRDS(file.path("shiny_bookmarks", book2, "values.rds"))+ #' shinyApp(app$ui, app$server) |
|||
307 | +143 |
-
+ #' } |
||
308 | -! | +|||
144 | +
- if (!isTRUE(all.equal(book1_values, book2_values))) {+ #' @rdname teal_modules |
|||
309 | -! | +|||
145 | +
- message("different values detected")+ #' @export |
|||
310 | -! | +|||
146 | +
- message("choices for numeric filters MAY be different, see RangeFilterState$set_choices")+ #' |
|||
311 | -! | +|||
147 | +
- ans <- FALSE+ module <- function(label = "module", |
|||
312 | +148 |
- }+ server = function(id, data, ...) moduleServer(id, function(input, output, session) NULL), |
||
313 | +149 |
-
+ ui = function(id, ...) tags$p(paste0("This module has no UI (id: ", id, " )")), |
||
314 | -! | +|||
150 | +
- if (ans) message("perfect!")+ filters,+ |
+ |||
151 | ++ |
+ datanames = "all",+ |
+ ||
152 | ++ |
+ server_args = NULL,+ |
+ ||
153 | ++ |
+ ui_args = NULL,+ |
+ ||
154 | ++ |
+ transformers = list()) {+ |
+ ||
155 | ++ |
+ # argument checking (independent)+ |
+ ||
156 | ++ |
+ ## `label`+ |
+ ||
157 | +201x | +
+ checkmate::assert_string(label)+ |
+ ||
158 | +198x | +
+ if (label == "global_filters") {+ |
+ ||
159 | +1x | +
+ stop(+ |
+ ||
160 | +1x | +
+ sprintf("module(label = \"%s\", ...\n ", label),+ |
+ ||
161 | +1x | +
+ "Label 'global_filters' is reserved in teal. Please change to something else.",+ |
+ ||
162 | +1x | +
+ call. = FALSE+ |
+ ||
163 | ++ |
+ )+ |
+ ||
164 | ++ |
+ }+ |
+ ||
165 | +197x | +
+ if (label == "Report previewer") {+ |
+ ||
166 | +! | +
+ stop(+ |
+ ||
167 | +! | +
+ sprintf("module(label = \"%s\", ...\n ", label),+ |
+ ||
168 | +! | +
+ "Label 'Report previewer' is reserved in teal. Please change to something else.",+ |
+ ||
169 | +! | +
+ call. = FALSE+ |
+ ||
170 | ++ |
+ )+ |
+ ||
171 | ++ |
+ }+ |
+ ||
172 | ++ | + + | +||
173 | ++ |
+ ## server+ |
+ ||
174 | +197x | +
+ checkmate::assert_function(server)+ |
+ ||
175 | +197x | +
+ server_formals <- names(formals(server))+ |
+ ||
176 | +197x | +
+ if (!(+ |
+ ||
177 | +197x | +
+ "id" %in% server_formals ||+ |
+ ||
178 | +197x | +
+ all(c("input", "output", "session") %in% server_formals)+ |
+ ||
179 | ++ |
+ )) {+ |
+ ||
180 | +2x | +
+ stop(+ |
+ ||
181 | +2x | +
+ "\nmodule() `server` argument requires a function with following arguments:",+ |
+ ||
182 | +2x | +
+ "\n - id - `teal` will set proper `shiny` namespace for this module.",+ |
+ ||
183 | +2x | +
+ "\n - input, output, session (not recommended) - then `shiny::callModule` will be used to call a module.",+ |
+ ||
184 | +2x | +
+ "\n\nFollowing arguments can be used optionaly:",+ |
+ ||
185 | +2x | +
+ "\n - `data` - module will receive list of reactive (filtered) data specified in the `filters` argument",+ |
+ ||
186 | +2x | +
+ "\n - `datasets` - module will receive `FilteredData`. See `help(teal.slice::FilteredData)`",+ |
+ ||
187 | +2x | +
+ "\n - `reporter` - module will receive `Reporter`. See `help(teal.reporter::Reporter)`",+ |
+ ||
188 | +2x | +
+ "\n - `filter_panel_api` - module will receive `FilterPanelAPI`. (See [teal.slice::FilterPanelAPI]).",+ |
+ ||
189 | +2x | +
+ "\n - `...` server_args elements will be passed to the module named argument or to the `...`"+ |
+ ||
190 | ++ |
+ )+ |
+ ||
191 | ++ |
+ }+ |
+ ||
192 | +195x | +
+ if ("datasets" %in% server_formals) {+ |
+ ||
193 | +2x | +
+ warning(+ |
+ ||
194 | +2x | +
+ sprintf("Called from module(label = \"%s\", ...)\n ", label),+ |
+ ||
195 | +2x | +
+ "`datasets` argument in the server is deprecated and will be removed in the next release. ",+ |
+ ||
196 | +2x | +
+ "Please use `data` instead.",+ |
+ ||
197 | +2x | +
+ call. = FALSE+ |
+ ||
198 | ++ |
+ )+ |
+ ||
199 | ++ |
+ }+ |
+ ||
200 | ++ | + + | +||
201 | ++ | + + | +||
202 | ++ |
+ ## UI+ |
+ ||
203 | +195x | +
+ checkmate::assert_function(ui)+ |
+ ||
204 | +195x | +
+ ui_formals <- names(formals(ui))+ |
+ ||
205 | +195x | +
+ if (!"id" %in% ui_formals) {+ |
+ ||
206 | +1x | +
+ stop(+ |
+ ||
207 | +1x | +
+ "\nmodule() `ui` argument requires a function with following arguments:",+ |
+ ||
208 | +1x | +
+ "\n - id - `teal` will set proper `shiny` namespace for this module.",+ |
+ ||
209 | +1x | +
+ "\n\nFollowing arguments can be used optionally:",+ |
+ ||
210 | +1x | +
+ "\n - `...` ui_args elements will be passed to the module argument of the same name or to the `...`"+ |
+ ||
211 | ++ |
+ )+ |
+ ||
212 | ++ |
+ }+ |
+ ||
213 | +194x | +
+ if (any(c("data", "datasets") %in% ui_formals)) {+ |
+ ||
214 | +2x | +
+ stop(+ |
+ ||
215 | +2x | +
+ sprintf("Called from module(label = \"%s\", ...)\n ", label),+ |
+ ||
216 | +2x | +
+ "UI with `data` or `datasets` argument is no longer accepted.\n ",+ |
+ ||
217 | +2x | +
+ "If some UI inputs depend on data, please move the logic to your server instead.\n ",+ |
+ ||
218 | +2x | +
+ "Possible solutions are renderUI() or updateXyzInput() functions."+ |
+ ||
219 | ++ |
+ )+ |
+ ||
220 | ++ |
+ }+ |
+ ||
221 | ++ | + + | +||
222 | ++ | + + | +||
223 | ++ |
+ ## `filters`+ |
+ ||
224 | +192x | +
+ if (!missing(filters)) {+ |
+ ||
225 | +! | +
+ datanames <- filters+ |
+ ||
226 | +! | +
+ msg <-+ |
+ ||
227 | +! | +
+ "The `filters` argument is deprecated and will be removed in the next release. Please use `datanames` instead."+ |
+ ||
228 | +! | +
+ warning(msg)+ |
+ ||
229 | ++ |
+ }+ |
+ ||
230 | ++ | + + | +||
231 | ++ |
+ ## `datanames` (also including deprecated `filters`)+ |
+ ||
232 | ++ |
+ # please note a race condition between datanames set when filters is not missing and data arg in server function+ |
+ ||
233 | +192x | +
+ if (!is.element("data", server_formals) && !is.null(datanames)) {+ |
+ ||
234 | +12x | +
+ message(sprintf("module \"%s\" server function takes no data so \"datanames\" will be ignored", label))+ |
+ ||
235 | +12x | +
+ datanames <- NULL+ |
+ ||
236 | ++ |
+ }+ |
+ ||
237 | +192x | +
+ checkmate::assert_character(datanames, min.len = 1, null.ok = TRUE, any.missing = FALSE)+ |
+ ||
238 | ++ | + + | +||
239 | ++ |
+ ## `server_args`+ |
+ ||
240 | +191x | +
+ checkmate::assert_list(server_args, null.ok = TRUE, names = "named")+ |
+ ||
241 | +189x | +
+ srv_extra_args <- setdiff(names(server_args), server_formals)+ |
+ ||
242 | +189x | +
+ if (length(srv_extra_args) > 0 && !"..." %in% server_formals) {+ |
+ ||
243 | +1x | +
+ stop(+ |
+ ||
244 | +1x | +
+ "\nFollowing `server_args` elements have no equivalent in the formals of the server:\n",+ |
+ ||
245 | +1x | +
+ paste(paste(" -", srv_extra_args), collapse = "\n"),+ |
+ ||
246 | +1x | +
+ "\n\nUpdate the server arguments by including above or add `...`"+ |
+ ||
247 | ++ |
+ )+ |
+ ||
248 | ++ |
+ }+ |
+ ||
249 | ++ | + + | +||
250 | ++ |
+ ## `ui_args`+ |
+ ||
251 | +188x | +
+ checkmate::assert_list(ui_args, null.ok = TRUE, names = "named")+ |
+ ||
252 | +186x | +
+ ui_extra_args <- setdiff(names(ui_args), ui_formals)+ |
+ ||
253 | +186x | +
+ if (length(ui_extra_args) > 0 && !"..." %in% ui_formals) {+ |
+ ||
254 | +1x | +
+ stop(+ |
+ ||
255 | +1x | +
+ "\nFollowing `ui_args` elements have no equivalent in the formals of UI:\n",+ |
+ ||
256 | +1x | +
+ paste(paste(" -", ui_extra_args), collapse = "\n"), |
||
315 | -! | +|||
257 | +1x |
- invisible(NULL)+ "\n\nUpdate the UI arguments by including above or add `...`" |
||
316 | +258 |
- }+ ) |
||
317 | +259 |
-
+ } |
||
318 | +260 | |||
319 | +261 |
- # Replacement for [base::rapply] which doesn't handle NULL values - skips the evaluation+ ## `transformers` |
||
320 | -+ | |||
262 | +185x |
- # of the function and returns NULL for given element.+ if (inherits(transformers, "teal_transform_module")) {+ |
+ ||
263 | +1x | +
+ transformers <- list(transformers) |
||
321 | +264 |
- rapply2 <- function(x, f) {+ } |
||
322 | -163x | +265 | +185x |
- if (inherits(x, "list")) {+ checkmate::assert_list(transformers, types = "teal_transform_module") |
323 | -69x | +266 | +185x |
- lapply(x, rapply2, f = f)+ transformer_datanames <- unlist(lapply(transformers, attr, "datanames"))+ |
+
267 | +185x | +
+ combined_datanames <- if (identical(datanames, "all") || any(sapply(transformer_datanames, identical, "all"))) {+ |
+ ||
268 | +139x | +
+ "all" |
||
324 | +269 |
} else { |
||
325 | -94x | +270 | +46x |
- f(x)+ union(datanames, transformer_datanames) |
326 | +271 |
} |
||
327 | +272 |
- }+ |
1 | -+ | |||
273 | +185x |
- #' An example `teal` module+ structure( |
||
2 | -+ | |||
274 | +185x |
- #'+ list( |
||
3 | -+ | |||
275 | +185x |
- #' `r lifecycle::badge("experimental")`+ label = label, |
||
4 | -+ | |||
276 | +185x |
- #'+ server = server, |
||
5 | -+ | |||
277 | +185x |
- #' @inheritParams teal_modules+ ui = ui, |
||
6 | -+ | |||
278 | +185x |
- #' @return A `teal` module which can be included in the `modules` argument to [init()].+ datanames = combined_datanames, |
||
7 | -+ | |||
279 | +185x |
- #' @examples+ server_args = server_args, |
||
8 | -+ | |||
280 | +185x |
- #' app <- init(+ ui_args = ui_args, |
||
9 | -+ | |||
281 | +185x |
- #' data = teal_data(IRIS = iris, MTCARS = mtcars),+ transformers = transformers |
||
10 | +282 |
- #' modules = example_module()+ ), |
||
11 | -+ | |||
283 | +185x |
- #' )+ class = "teal_module" |
||
12 | +284 |
- #' if (interactive()) {+ ) |
||
13 | +285 |
- #' shinyApp(app$ui, app$server)+ } |
||
14 | +286 |
- #' }+ |
||
15 | +287 |
- #' @export+ #' @rdname teal_modules |
||
16 | +288 |
- example_module <- function(label = "example teal module", datanames = "all", transformers = list()) {+ #' @export |
||
17 | -37x | +|||
289 | +
- checkmate::assert_string(label)+ #' |
|||
18 | -37x | +|||
290 | +
- ans <- module(+ modules <- function(..., label = "root") { |
|||
19 | -37x | +291 | +124x |
- label,+ checkmate::assert_string(label) |
20 | -37x | +292 | +122x |
- server = function(id, data) {+ submodules <- list(...) |
21 | -2x | +293 | +122x |
- checkmate::assert_class(isolate(data()), "teal_data")+ if (any(vapply(submodules, is.character, FUN.VALUE = logical(1)))) { |
22 | +294 | 2x |
- moduleServer(id, function(input, output, session) {+ stop( |
|
23 | +295 | 2x |
- datanames_rv <- reactive(ls(teal.code::get_env((req(data())))))+ "The only character argument to modules() must be 'label' and it must be named, ", |
|
24 | +296 | 2x |
- observeEvent(datanames_rv(), {+ "change modules('lab', ...) to modules(label = 'lab', ...)" |
|
25 | -2x | +|||
297 | +
- selected <- input$dataname+ ) |
|||
26 | -2x | +|||
298 | +
- if (identical(selected, "")) {+ } |
|||
27 | -! | +|||
299 | +
- selected <- restoreInput(session$ns("dataname"), NULL)+ |
|||
28 | -2x | +300 | +120x |
- } else if (isFALSE(selected %in% datanames_rv())) {+ checkmate::assert_list(submodules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules")) |
29 | -! | +|||
301 | +
- selected <- datanames_rv()[1]+ # name them so we can more easily access the children |
|||
30 | +302 |
- }+ # beware however that the label of the submodules should not be changed as it must be kept synced |
||
31 | -2x | +303 | +117x |
- updateSelectInput(+ labels <- vapply(submodules, function(submodule) submodule$label, character(1)) |
32 | -2x | +304 | +117x |
- session = session,+ names(submodules) <- get_unique_labels(labels) |
33 | -2x | +305 | +117x |
- inputId = "dataname",+ structure( |
34 | -2x | +306 | +117x |
- choices = datanames_rv(),+ list( |
35 | -2x | +307 | +117x |
- selected = selected+ label = label, |
36 | -+ | |||
308 | +117x |
- )+ children = submodules |
||
37 | +309 |
- })+ ),+ |
+ ||
310 | +117x | +
+ class = "teal_modules" |
||
38 | +311 |
-
+ ) |
||
39 | -2x | +|||
312 | +
- output$text <- renderPrint({+ } |
|||
40 | -2x | +|||
313 | +
- req(input$dataname)+ |
|||
41 | -! | +|||
314 | +
- data()[[input$dataname]]+ # printing methods ---- |
|||
42 | +315 |
- })+ |
||
43 | +316 |
-
+ #' @rdname teal_modules |
||
44 | -2x | +|||
317 | +
- teal.widgets::verbatim_popup_srv(+ #' @export |
|||
45 | -2x | +|||
318 | +
- id = "rcode",+ format.teal_module <- function(x, indent = 0, ...) { |
|||
46 | -2x | +319 | +3x |
- verbatim_content = reactive(teal.code::get_code(data())),+ paste0(paste(rep(" ", indent), collapse = ""), "+ ", x$label, "\n", collapse = "") |
47 | -2x | +|||
320 | +
- title = "Example Code"+ } |
|||
48 | +321 |
- )+ |
||
49 | +322 |
- })+ |
||
50 | +323 |
- },+ #' @rdname teal_modules |
||
51 | -37x | +|||
324 | +
- ui = function(id) {+ #' @export |
|||
52 | -! | +|||
325 | +
- ns <- NS(id)+ print.teal_module <- function(x, ...) { |
|||
53 | +326 | ! |
- teal.widgets::standard_layout(+ cat(format(x, ...)) |
|
54 | +327 | ! |
- output = verbatimTextOutput(ns("text")),+ invisible(x) |
|
55 | -! | +|||
328 | +
- encoding = tags$div(+ } |
|||
56 | -! | +|||
329 | +
- selectInput(ns("dataname"), "Choose a dataset", choices = NULL),+ |
|||
57 | -! | +|||
330 | +
- teal.widgets::verbatim_popup_ui(ns("rcode"), "Show R code")+ |
|||
58 | +331 |
- )+ #' @rdname teal_modules |
||
59 | +332 |
- )+ #' @export |
||
60 | +333 |
- },+ format.teal_modules <- function(x, indent = 0, ...) { |
||
61 | -37x | +334 | +1x |
- datanames = datanames,+ paste( |
62 | -37x | +335 | +1x |
- transformers = transformers+ c(+ |
+
336 | +1x | +
+ paste0(rep(" ", indent), "+ ", x$label, "\n"),+ |
+ ||
337 | +1x | +
+ unlist(lapply(x$children, format, indent = indent + 1, ...)) |
||
63 | +338 |
- )+ ), |
||
64 | -37x | +339 | +1x |
- attr(ans, "teal_bookmarkable") <- TRUE+ collapse = "" |
65 | -37x | +|||
340 | +
- ans+ ) |
|||
66 | +341 |
} |
1 | +342 |
- #' `teal_data` utils+ |
||
2 | +343 |
- #'+ #' @param modules (`teal_module` or `teal_modules`) |
||
3 | +344 |
- #' In `teal` we need to recreate the `teal_data` object due to two operations:+ #' @rdname teal_modules |
||
4 | +345 |
- #' - we need to append filter-data code and objects which have been evaluated in `FilteredData` and+ #' @examples |
||
5 | +346 |
- #' we want to avoid double-evaluation.+ #' # change the module's datanames |
||
6 | +347 |
- #' - we need to subset `teal_data` to `datanames` used by the module, to shorten obtainable R-code+ #' set_datanames(module(datanames = "all"), "a") |
||
7 | +348 |
#' |
||
8 | +349 |
- #' Due to above recreation of `teal_data` object can't be done simply by using public+ #' # change modules' datanames |
||
9 | +350 |
- #' `teal.code` and `teal.data` methods.+ #' set_datanames( |
||
10 | +351 |
- #'+ #' modules( |
||
11 | +352 |
- #' @param data (`teal_data`)+ #' module(datanames = "all"), |
||
12 | +353 |
- #' @param code (`character`) code to append to `data@code`+ #' module(datanames = "a") |
||
13 | +354 |
- #' @param objects (`list`) objects to append to `data@env`+ #' ), |
||
14 | +355 |
- #' @param datanames (`character`) names of the datasets+ #' "b" |
||
15 | +356 |
- #' @return modified `teal_data`+ #' ) |
||
16 | +357 |
- #' @keywords internal+ #' @export |
||
17 | +358 |
- #' @name teal_data_utilities+ set_datanames <- function(modules, datanames) { |
||
18 | -+ | |||
359 | +! |
- NULL+ checkmate::assert_multi_class(modules, c("teal_modules", "teal_module")) |
||
19 | -+ | |||
360 | +! |
-
+ if (inherits(modules, "teal_modules")) {+ |
+ ||
361 | +! | +
+ modules$children <- lapply(modules$children, set_datanames, datanames) |
||
20 | +362 |
- #' @rdname teal_data_utilities+ } else {+ |
+ ||
363 | +! | +
+ if (identical(modules$datanames, "all")) {+ |
+ ||
364 | +! | +
+ modules$datanames <- datanames |
||
21 | +365 |
- .append_evaluated_code <- function(data, code) {+ } else { |
||
22 | -76x | +|||
366 | +! |
- checkmate::assert_class(data, "teal_data")+ warning( |
||
23 | -76x | +|||
367 | +! |
- data@code <- c(data@code, code)+ "Not possible to modify datanames of the module ", modules$label, |
||
24 | -76x | +|||
368 | +! |
- data@id <- c(data@id, max(data@id) + 1L + seq_along(code))+ ". set_datanames() can only change datanames if it was set to \"all\".", |
||
25 | -76x | +|||
369 | +! |
- data@messages <- c(data@messages, rep("", length(code)))+ call. = FALSE |
||
26 | -76x | +|||
370 | +
- data@warnings <- c(data@warnings, rep("", length(code)))+ ) |
|||
27 | -76x | +|||
371 | +
- methods::validObject(data)+ } |
|||
28 | -76x | +|||
372 | +
- data+ }+ |
+ |||
373 | +! | +
+ modules |
||
29 | +374 |
} |
||
30 | +375 | |||
31 | +376 |
- #' @rdname teal_data_utilities+ #' @rdname teal_modules |
||
32 | +377 |
- .append_modified_data <- function(data, objects) {+ #' @export |
||
33 | -76x | +|||
378 | +
- checkmate::assert_class(data, "teal_data")+ print.teal_modules <- print.teal_module |
|||
34 | -76x | +|||
379 | +
- checkmate::assert_class(objects, "list")+ |
|||
35 | -76x | +|||
380 | +
- new_env <- list2env(objects, parent = .GlobalEnv)+ |
|||
36 | -76x | +|||
381 | +
- rlang::env_coalesce(new_env, teal.code::get_env(data))+ # utilities ---- |
|||
37 | -76x | +|||
382 | +
- data@env <- new_env+ ## subset or modify modules ---- |
|||
38 | -76x | +|||
383 | +
- data+ |
|||
39 | +384 |
- }+ #' Append a `teal_module` to `children` of a `teal_modules` object |
||
40 | +385 |
-
+ #' @keywords internal |
||
41 | +386 |
- #' @rdname teal_data_utilities+ #' @param modules (`teal_modules`) |
||
42 | +387 |
- .subset_teal_data <- function(data, datanames) {+ #' @param module (`teal_module`) object to be appended onto the children of `modules`+ |
+ ||
388 | ++ |
+ #' @return A `teal_modules` object with `module` appended.+ |
+ ||
389 | ++ |
+ append_module <- function(modules, module) { |
||
43 | -78x | +390 | +8x |
- checkmate::assert_class(data, "teal_data")+ checkmate::assert_class(modules, "teal_modules") |
44 | -78x | +391 | +6x |
- checkmate::assert_class(datanames, "character")+ checkmate::assert_class(module, "teal_module") |
45 | -78x | +392 | +4x |
- datanames_corrected <- intersect(datanames, ls(teal.code::get_env(data)))+ modules$children <- c(modules$children, list(module)) |
46 | -78x | +393 | +4x |
- datanames_corrected_with_raw <- c(datanames_corrected, ".raw_data")+ labels <- vapply(modules$children, function(submodule) submodule$label, character(1)) |
47 | -78x | +394 | +4x |
- if (!length(datanames_corrected)) {+ names(modules$children) <- get_unique_labels(labels) |
48 | -1x | +395 | +4x |
- return(teal_data())+ modules |
49 | +396 |
- }+ } |
||
50 | +397 | |||
51 | -77x | +|||
398 | +
- new_data <- do.call(+ #' Extract/Remove module(s) of specific class |
|||
52 | -77x | +|||
399 | +
- teal.data::teal_data,+ #' |
|||
53 | -77x | +|||
400 | +
- args = c(+ #' Given a `teal_module` or a `teal_modules`, return the elements of the structure according to `class`. |
|||
54 | -77x | +|||
401 | +
- mget(x = datanames_corrected_with_raw, envir = teal.code::get_env(data)),+ #' |
|||
55 | -77x | +|||
402 | +
- list(+ #' @param modules (`teal_modules`) |
|||
56 | -77x | +|||
403 | +
- code = teal.data::get_code(data, datanames = datanames_corrected_with_raw),+ #' @param class The class name of `teal_module` to be extracted or dropped. |
|||
57 | -77x | +|||
404 | +
- join_keys = teal.data::join_keys(data)[datanames_corrected]+ #' @keywords internal |
|||
58 | +405 |
- )+ #' @return |
||
59 | +406 |
- )+ #' - For `extract_module`, a `teal_module` of class `class` or `teal_modules` containing modules of class `class`. |
||
60 | +407 |
- )+ #' - For `drop_module`, the opposite, which is all `teal_modules` of class other than `class`.+ |
+ ||
408 | ++ |
+ #' @rdname module_management+ |
+ ||
409 | ++ |
+ extract_module <- function(modules, class) { |
||
61 | -77x | +410 | +22x |
- new_data@verified <- data@verified+ if (inherits(modules, class)) {+ |
+
411 | +! | +
+ modules |
||
62 | -77x | +412 | +22x |
- teal.data::datanames(new_data) <- datanames_corrected+ } else if (inherits(modules, "teal_module")) { |
63 | -77x | +413 | +12x |
- new_data+ NULL |
64 | -+ | |||
414 | +10x |
- }+ } else if (inherits(modules, "teal_modules")) { |
1 | -+ | |||
415 | +10x |
- #' Filter panel module in teal+ Filter(function(x) length(x) > 0L, lapply(modules$children, extract_module, class)) |
||
2 | +416 |
- #'+ } |
||
3 | +417 |
- #' Creates filter panel module from `teal_data` object and returns `teal_data`. It is build in a way+ } |
||
4 | +418 |
- #' that filter panel changes and anything what happens before (e.g. [`module_init_data`]) is triggering+ |
||
5 | +419 |
- #' further reactive events only if something has changed and if the module is visible. Thanks to+ #' @keywords internal |
||
6 | +420 |
- #' this special implementation all modules' data are recalculated only for those modules which are+ #' @return `teal_modules` |
||
7 | +421 |
- #' currently displayed.+ #' @rdname module_management |
||
8 | +422 |
- #'+ drop_module <- function(modules, class) { |
||
9 | -+ | |||
423 | +! |
- #' @return A `eventReactive` containing `teal_data` containing filtered objects and filter code.+ if (inherits(modules, class)) { |
||
10 | -+ | |||
424 | +! |
- #' `eventReactive` triggers only if all conditions are met:+ NULL |
||
11 | -+ | |||
425 | +! | +
+ } else if (inherits(modules, "teal_module")) {+ |
+ ||
426 | +! | +
+ modules+ |
+ ||
427 | +! | +
+ } else if (inherits(modules, "teal_modules")) {+ |
+ ||
428 | +! |
- #' - tab is selected (`is_active`)+ do.call( |
||
12 | -+ | |||
429 | +! |
- #' - when filters are changed (`get_filter_expr` is different than previous)+ "modules", |
||
13 | -+ | |||
430 | +! |
- #'+ c(Filter(function(x) length(x) > 0L, lapply(modules$children, drop_module, class)), label = modules$label) |
||
14 | +431 |
- #' @inheritParams module_teal_module+ ) |
||
15 | +432 |
- #' @param active_datanames (`reactive` returning `character`) this module's data names+ } |
||
16 | +433 |
- #' @name module_filter_data+ } |
||
17 | +434 |
- #' @keywords internal+ |
||
18 | +435 |
- NULL+ ## read modules ---- |
||
19 | +436 | |||
20 | +437 |
- #' @rdname module_filter_data+ #' Does the object make use of the `arg` |
||
21 | +438 |
- ui_filter_data <- function(id) {+ #' |
||
22 | -! | +|||
439 | +
- ns <- shiny::NS(id)+ #' @param modules (`teal_module` or `teal_modules`) object |
|||
23 | -! | +|||
440 | +
- uiOutput(ns("panel"))+ #' @param arg (`character(1)`) names of the arguments to be checked against formals of `teal` modules. |
|||
24 | +441 |
- }+ #' @return `logical` whether the object makes use of `arg`. |
||
25 | +442 |
-
+ #' @rdname is_arg_used |
||
26 | +443 |
- #' @rdname module_filter_data+ #' @keywords internal |
||
27 | +444 |
- srv_filter_data <- function(id, datasets, active_datanames, data_rv, is_active) {+ is_arg_used <- function(modules, arg) { |
||
28 | -95x | +445 | +427x |
- assert_reactive(datasets)+ checkmate::assert_string(arg) |
29 | -95x | +446 | +424x |
- moduleServer(id, function(input, output, session) {+ if (inherits(modules, "teal_modules")) { |
30 | -95x | -
- active_corrected <- reactive(intersect(active_datanames(), datasets()$datanames()))- |
- ||
31 | -+ | 447 | +17x |
-
+ any(unlist(lapply(modules$children, is_arg_used, arg))) |
32 | -95x | +448 | +407x |
- output$panel <- renderUI({+ } else if (inherits(modules, "teal_module")) { |
33 | -83x | +449 | +29x |
- req(inherits(datasets(), "FilteredData"))+ is_arg_used(modules$server, arg) || is_arg_used(modules$ui, arg) |
34 | -75x | +450 | +378x |
- isolate({+ } else if (is.function(modules)) { |
35 | -+ | |||
451 | +376x |
- # render will be triggered only when FilteredData object changes (not when filters change)+ isTRUE(arg %in% names(formals(modules))) |
||
36 | +452 |
- # technically it means that teal_data_module needs to be refreshed- |
- ||
37 | -75x | -
- logger::log_debug("srv_filter_panel rendering filter panel.")- |
- ||
38 | -75x | -
- if (length(active_corrected())) {- |
- ||
39 | -74x | -
- datasets()$srv_active("filters", active_datanames = active_corrected)+ } else { |
||
40 | -74x | +453 | +2x |
- datasets()$ui_active(session$ns("filters"), active_datanames = active_corrected)+ stop("is_arg_used function not implemented for this object") |
41 | +454 |
- }+ } |
||
42 | +455 |
- })+ } |
||
43 | +456 |
- })+ |
||
44 | +457 | |||
45 | -95x | +|||
458 | +
- trigger_data <- .observe_active_filter_changed(datasets, is_active, active_corrected, data_rv)+ #' Get module depth |
|||
46 | +459 |
-
+ #' |
||
47 | -95x | +|||
460 | +
- eventReactive(trigger_data(), {+ #' Depth starts at 0, so a single `teal.module` has depth 0. |
|||
48 | -76x | +|||
461 | +
- .make_filtered_teal_data(modules, data = data_rv(), datasets = datasets(), datanames = active_corrected())+ #' Nesting it increases overall depth by 1. |
|||
49 | +462 |
- })+ #' |
||
50 | +463 |
- })+ #' @inheritParams init |
||
51 | +464 |
- }+ #' @param depth optional integer determining current depth level |
||
52 | +465 |
-
+ #' |
||
53 | +466 |
- #' @rdname module_filter_data+ #' @return Depth level for given module. |
||
54 | +467 |
- .make_filtered_teal_data <- function(modules, data, datasets = NULL, datanames) {+ #' @keywords internal |
||
55 | -76x | +|||
468 | +
- data <- eval_code(+ modules_depth <- function(modules, depth = 0L) { |
|||
56 | -76x | +469 | +12x |
- data,+ checkmate::assert_multi_class(modules, c("teal_module", "teal_modules")) |
57 | -76x | +470 | +12x |
- paste0(+ checkmate::assert_int(depth, lower = 0) |
58 | -76x | +471 | +11x |
- ".raw_data <- list2env(list(",+ if (inherits(modules, "teal_modules")) { |
59 | -76x | +472 | +4x |
- toString(sprintf("%1$s = %1$s", datanames)),+ max(vapply(modules$children, modules_depth, integer(1), depth = depth + 1L)) |
60 | -76x | +|||
473 | +
- "))\n",+ } else { |
|||
61 | -76x | +474 | +7x |
- "lockEnvironment(.raw_data) #@linksto .raw_data" # this is environment and it is shared by qenvs. CAN'T MODIFY!+ depth |
62 | +475 |
- )+ } |
||
63 | +476 |
- )- |
- ||
64 | -76x | -
- filtered_code <- teal.slice::get_filter_expr(datasets = datasets, datanames = datanames)- |
- ||
65 | -76x | -
- filtered_teal_data <- .append_evaluated_code(data, filtered_code)- |
- ||
66 | -76x | -
- filtered_datasets <- sapply(datanames, function(x) datasets$get_data(x, filtered = TRUE), simplify = FALSE)+ } |
||
67 | -76x | +|||
477 | +
- filtered_teal_data <- .append_modified_data(filtered_teal_data, filtered_datasets)+ |
|||
68 | -76x | +|||
478 | +
- filtered_teal_data+ #' Retrieve labels from `teal_modules` |
|||
69 | +479 |
- }+ #' |
||
70 | +480 |
-
+ #' @param modules (`teal_modules`) |
||
71 | +481 |
- #' @rdname module_filter_data+ #' @return A `list` containing the labels of the modules. If the modules are nested, |
||
72 | +482 |
- .observe_active_filter_changed <- function(datasets, is_active, active_datanames, data_rv) {+ #' the function returns a nested `list` of labels. |
||
73 | -95x | +|||
483 | +
- previous_signature <- reactiveVal(NULL)+ #' @keywords internal |
|||
74 | -95x | +|||
484 | +
- filter_changed <- reactive({+ module_labels <- function(modules) { |
|||
75 | -163x | +485 | +165x |
- req(inherits(datasets(), "FilteredData"))+ if (inherits(modules, "teal_modules")) { |
76 | -159x | +486 | +70x |
- new_signature <- c(+ lapply(modules$children, module_labels) |
77 | -159x | +|||
487 | +
- teal.data::get_code(data_rv()),+ } else { |
|||
78 | -159x | +488 | +95x |
- teal.slice::get_filter_expr(datasets = datasets(), datanames = active_datanames())+ modules$label |
79 | +489 |
- )- |
- ||
80 | -159x | -
- if (!identical(previous_signature(), new_signature)) {+ } |
||
81 | -76x | +|||
490 | +
- previous_signature(new_signature)+ } |
|||
82 | -76x | +|||
491 | +
- TRUE+ |
|||
83 | +492 |
- } else {+ #' Retrieve `teal_bookmarkable` attribute from `teal_modules` |
||
84 | -83x | +|||
493 | +
- FALSE+ #' |
|||
85 | +494 |
- }+ #' @param modules (`teal_modules` or `teal_module`) object |
||
86 | +495 |
- })+ #' @return named list of the same structure as `modules` with `TRUE` or `FALSE` values indicating |
||
87 | +496 |
-
+ #' whether the module is bookmarkable. |
||
88 | -95x | +|||
497 | +
- trigger_data <- reactiveVal(NULL)+ #' @keywords internal |
|||
89 | -95x | +|||
498 | +
- observe({+ modules_bookmarkable <- function(modules) { |
|||
90 | -197x | +499 | +163x |
- if (isTRUE(is_active() && filter_changed())) {+ checkmate::assert_multi_class(modules, c("teal_modules", "teal_module")) |
91 | -76x | +500 | +163x |
- isolate({+ if (inherits(modules, "teal_modules")) { |
92 | -76x | +501 | +69x |
- if (is.null(trigger_data())) {+ setNames( |
93 | -68x | -
- trigger_data(0)- |
- ||
94 | -+ | 502 | +69x |
- } else {+ lapply(modules$children, modules_bookmarkable), |
95 | -8x | -
- trigger_data(trigger_data() + 1)- |
- ||
96 | -+ | 503 | +69x |
- }+ vapply(modules$children, `[[`, "label", FUN.VALUE = character(1)) |
97 | +504 |
- })+ ) |
||
98 | +505 |
- }+ } else { |
||
99 | -+ | |||
506 | +94x |
- })+ attr(modules, "teal_bookmarkable", exact = TRUE) |
||
100 | +507 | - - | -||
101 | -95x | -
- trigger_data+ } |
||
102 | +508 |
}@@ -33834,1523 +33941,1810 @@ teal coverage - 57.64% |
1 |
- #' `teal` main module+ # This is the main function from teal to be used by the end-users. Although it delegates |
|||
2 |
- #'+ # directly to `module_teal_with_splash.R`, we keep it in a separate file because its documentation is quite large |
|||
3 |
- #' @description+ # and it is very end-user oriented. It may also perform more argument checking with more informative |
|||
4 |
- #' `r lifecycle::badge("stable")`+ # error messages. |
|||
5 |
- #' Module to create a `teal` app. This module can be called directly instead of [init()] and+ |
|||
6 |
- #' included in your custom application. Please note that [init()] adds `reporter_previewer_module`+ #' Create the server and UI function for the `shiny` app |
|||
7 |
- #' automatically, which is not a case when calling `ui/srv_teal` directly.+ #' |
|||
8 |
- #'+ #' @description `r lifecycle::badge("stable")` |
|||
9 |
- #' @details+ #' |
|||
10 |
- #'+ #' End-users: This is the most important function for you to start a |
|||
11 |
- #' Module is responsible for creating the main `shiny` app layout and initializing all the necessary+ #' `teal` app that is composed of `teal` modules. |
|||
12 |
- #' components. This module establishes reactive connection between the input `data` and every other+ #' |
|||
13 |
- #' component in the app. Reactive change of the `data` passed as an argument, reloads the app and+ #' @param data (`teal_data` or `teal_data_module`) |
|||
14 |
- #' possibly keeps all input settings the same so the user can continue where one left off.+ #' For constructing the data object, refer to [teal_data()] and [teal_data_module()]. |
|||
15 |
- #'+ #' If `datanames` are not set for the `teal_data` object, defaults from the `teal_data` environment will be used. |
|||
16 |
- #' ## data flow in `teal` application+ #' @param modules (`list` or `teal_modules` or `teal_module`) |
|||
17 |
- #'+ #' Nested list of `teal_modules` or `teal_module` objects or a single |
|||
18 |
- #' This module supports multiple data inputs but eventually, they are all converted to `reactive`+ #' `teal_modules` or `teal_module` object. These are the specific output modules which |
|||
19 |
- #' returning `teal_data` in this module. On this `reactive teal_data` object several actions are+ #' will be displayed in the `teal` application. See [modules()] and [module()] for |
|||
20 |
- #' performed:+ #' more details. |
|||
21 |
- #' - data loading in [`module_init_data`]+ #' @param filter (`teal_slices`) Optionally, |
|||
22 |
- #' - data filtering in [`module_filter_data`]+ #' specifies the initial filter using [teal_slices()]. |
|||
23 |
- #' - data transformation in [`module_transform_data`]+ #' @param title (`shiny.tag` or `character(1)`) Optionally, |
|||
24 |
- #'+ #' the browser window title. Defaults to a title "teal app" with the icon of NEST. |
|||
25 |
- #' @rdname module_teal+ #' Can be created using the `build_app_title()` or+ |
+ |||
26 | ++ |
+ #' by passing a valid `shiny.tag` which is a head tag with title and link tag.+ |
+ ||
27 | ++ |
+ #' @param header (`shiny.tag` or `character(1)`) Optionally,+ |
+ ||
28 | ++ |
+ #' the header of the app.+ |
+ ||
29 | ++ |
+ #' @param footer (`shiny.tag` or `character(1)`) Optionally,+ |
+ ||
30 | ++ |
+ #' the footer of the app.+ |
+ ||
31 | ++ |
+ #' @param id (`character`) Optionally,+ |
+ ||
32 | ++ |
+ #' a string specifying the `shiny` module id in cases it is used as a `shiny` module+ |
+ ||
33 | ++ |
+ #' rather than a standalone `shiny` app. This is a legacy feature.+ |
+ ||
34 | ++ |
+ #' @param landing_popup (`teal_module_landing`) Optionally,+ |
+ ||
35 | ++ |
+ #' a `landing_popup_module` to show up as soon as the teal app is initialized.+ |
+ ||
36 | ++ |
+ #'+ |
+ ||
37 | ++ |
+ #' @return Named list containing server and UI functions.+ |
+ ||
38 | ++ |
+ #'+ |
+ ||
39 | ++ |
+ #' @export+ |
+ ||
40 | ++ |
+ #'+ |
+ ||
41 | ++ |
+ #' @include modules.R+ |
+ ||
42 | ++ |
+ #'+ |
+ ||
43 | ++ |
+ #' @examples+ |
+ ||
44 | ++ |
+ #' app <- init(+ |
+ ||
45 | ++ |
+ #' data = within(+ |
+ ||
46 | ++ |
+ #' teal_data(),+ |
+ ||
47 | ++ |
+ #' {+ |
+ ||
48 | ++ |
+ #' new_iris <- transform(iris, id = seq_len(nrow(iris)))+ |
+ ||
49 | ++ |
+ #' new_mtcars <- transform(mtcars, id = seq_len(nrow(mtcars)))+ |
+ ||
50 | ++ |
+ #' }+ |
+ ||
51 | ++ |
+ #' ),+ |
+ ||
52 | ++ |
+ #' modules = modules(+ |
+ ||
53 | ++ |
+ #' module(+ |
+ ||
54 | ++ |
+ #' label = "data source",+ |
+ ||
55 | ++ |
+ #' server = function(input, output, session, data) {},+ |
+ ||
56 | ++ |
+ #' ui = function(id, ...) tags$div(p("information about data source")),+ |
+ ||
57 | ++ |
+ #' datanames = "all"+ |
+ ||
58 | ++ |
+ #' ),+ |
+ ||
59 | ++ |
+ #' example_module(label = "example teal module"),+ |
+ ||
60 | ++ |
+ #' module(+ |
+ ||
61 | ++ |
+ #' "Iris Sepal.Length histogram",+ |
+ ||
62 | ++ |
+ #' server = function(input, output, session, data) {+ |
+ ||
63 | ++ |
+ #' output$hist <- renderPlot(+ |
+ ||
64 | ++ |
+ #' hist(data()[["new_iris"]]$Sepal.Length)+ |
+ ||
65 | ++ |
+ #' )+ |
+ ||
66 | ++ |
+ #' },+ |
+ ||
67 | ++ |
+ #' ui = function(id, ...) {+ |
+ ||
68 | ++ |
+ #' ns <- NS(id)+ |
+ ||
69 | ++ |
+ #' plotOutput(ns("hist"))+ |
+ ||
70 | ++ |
+ #' },+ |
+ ||
71 | ++ |
+ #' datanames = "new_iris"+ |
+ ||
72 | ++ |
+ #' )+ |
+ ||
73 | ++ |
+ #' ),+ |
+ ||
74 | ++ |
+ #' filter = teal_slices(+ |
+ ||
75 | ++ |
+ #' teal_slice(dataname = "new_iris", varname = "Species"),+ |
+ ||
76 | ++ |
+ #' teal_slice(dataname = "new_iris", varname = "Sepal.Length"),+ |
+ ||
77 | ++ |
+ #' teal_slice(dataname = "new_mtcars", varname = "cyl"),+ |
+ ||
78 | ++ |
+ #' exclude_varnames = list(new_iris = c("Sepal.Width", "Petal.Width")),+ |
+ ||
79 | ++ |
+ #' module_specific = TRUE,+ |
+ ||
80 | ++ |
+ #' mapping = list(+ |
+ ||
81 | ++ |
+ #' `example teal module` = "new_iris Species",+ |
+ ||
82 | ++ |
+ #' `Iris Sepal.Length histogram` = "new_iris Species", |
||
26 | +83 |
- #' @name module_teal+ #' global_filters = "new_mtcars cyl" |
||
27 | +84 |
- #'+ #' ) |
||
28 | +85 |
- #' @inheritParams module_init_data+ #' ), |
||
29 | +86 |
- #' @inheritParams init+ #' title = "App title", |
||
30 | +87 |
- #'+ #' header = tags$h1("Sample App"), |
||
31 | +88 |
- #' @return `NULL` invisibly+ #' footer = tags$p("Sample footer") |
||
32 | +89 |
- NULL+ #' ) |
||
33 | +90 |
-
+ #' if (interactive()) { |
||
34 | +91 |
- #' @rdname module_teal+ #' shinyApp(app$ui, app$server) |
||
35 | +92 |
- #' @export+ #' } |
||
36 | +93 |
- ui_teal <- function(id,+ #' |
||
37 | +94 |
- modules,+ init <- function(data, |
||
38 | +95 |
- data = NULL,+ modules, |
||
39 | +96 |
- title = build_app_title(),+ filter = teal_slices(), |
||
40 | +97 |
- header = tags$p(),+ title = build_app_title(), |
||
41 | +98 |
- footer = tags$p()) {+ header = tags$p(), |
||
42 | -! | +|||
99 | +
- checkmate::assert_character(id, max.len = 1, any.missing = FALSE)+ footer = tags$p(), |
|||
43 | -! | +|||
100 | +
- checkmate::assert_multi_class(data, "teal_data_module", null.ok = TRUE)+ id = character(0), |
|||
44 | -! | +|||
101 | +
- checkmate::assert(+ landing_popup = NULL) { |
|||
45 | -! | +|||
102 | +11x |
- .var.name = "title",+ logger::log_debug("init initializing teal app with: data ('{ class(data) }').") |
||
46 | -! | +|||
103 | +
- checkmate::check_string(title),+ |
|||
47 | -! | +|||
104 | +
- checkmate::check_multi_class(title, c("shiny.tag", "shiny.tag.list", "html"))+ # argument checking (independent) |
|||
48 | +105 |
- )+ ## `data` |
||
49 | -! | +|||
106 | +11x |
- checkmate::assert(+ if (inherits(data, "TealData")) { |
||
50 | +107 | ! |
- .var.name = "header",+ lifecycle::deprecate_stop( |
|
51 | +108 | ! |
- checkmate::check_string(header),+ when = "0.15.0", |
|
52 | +109 | ! |
- checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html"))- |
- |
53 | -- |
- )+ what = "init(data)", |
||
54 | +110 | ! |
- checkmate::assert(+ paste( |
|
55 | +111 | ! |
- .var.name = "footer",+ "TealData is no longer supported. Use teal_data() instead.", |
|
56 | +112 | ! |
- checkmate::check_string(footer),+ "Please follow migration instructions https://github.com/insightsengineering/teal/discussions/988." |
|
57 | -! | +|||
113 | +
- checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html"))+ ) |
|||
58 | +114 |
- )+ ) |
||
59 | +115 |
-
+ } |
||
60 | -! | +|||
116 | +11x |
- if (is.character(title)) {+ checkmate::assert_multi_class(data, c("teal_data", "teal_data_module")) |
||
61 | -! | +|||
117 | +11x |
- title <- build_app_title(title)+ checkmate::assert_class(landing_popup, "teal_module_landing", null.ok = TRUE) |
||
62 | +118 |
- } else {- |
- ||
63 | -! | -
- validate_app_title_tag(title)+ |
||
64 | +119 |
- }+ ## `modules` |
||
65 | -+ | |||
120 | +11x |
-
+ checkmate::assert( |
||
66 | -! | +|||
121 | +11x |
- if (checkmate::test_string(header)) {+ .var.name = "modules", |
||
67 | -! | +|||
122 | +11x |
- header <- tags$p(header)+ checkmate::check_multi_class(modules, c("teal_modules", "teal_module")), |
||
68 | -+ | |||
123 | +11x |
- }+ checkmate::check_list(modules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules")) |
||
69 | +124 |
-
+ ) |
||
70 | -! | +|||
125 | +11x |
- if (checkmate::test_string(footer)) {+ if (inherits(modules, "teal_module")) { |
||
71 | -! | +|||
126 | +1x |
- footer <- tags$p(footer)+ modules <- list(modules) |
||
72 | +127 |
} |
||
73 | -+ | |||
128 | +11x |
-
+ if (checkmate::test_list(modules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules"))) { |
||
74 | -! | +|||
129 | +5x |
- ns <- NS(id)+ modules <- do.call(teal::modules, modules) |
||
75 | +130 |
-
+ } |
||
76 | +131 |
- # show busy icon when `shiny` session is busy computing stuff+ |
||
77 | +132 |
- # based on https://stackoverflow.com/questions/17325521/r-shiny-display-loading-message-while-function-is-running/22475216#22475216 # nolint: line_length.- |
- ||
78 | -! | -
- shiny_busy_message_panel <- conditionalPanel(+ ## `filter` |
||
79 | -! | +|||
133 | +11x |
- condition = "(($('html').hasClass('shiny-busy')) && (document.getElementById('shiny-notification-panel') == null))", # nolint: line_length.+ checkmate::assert_class(filter, "teal_slices") |
||
80 | -! | +|||
134 | +
- tags$div(+ |
|||
81 | -! | +|||
135 | +
- icon("arrows-rotate", class = "fa-spin", prefer_type = "solid"),+ ## all other arguments |
|||
82 | -! | +|||
136 | +10x |
- "Computing ...",+ checkmate::assert( |
||
83 | -+ | |||
137 | +10x |
- # CSS defined in `custom.css`+ .var.name = "title", |
||
84 | -! | +|||
138 | +10x |
- class = "shinybusymessage"+ checkmate::check_string(title), |
||
85 | -+ | |||
139 | +10x |
- )+ checkmate::check_multi_class(title, c("shiny.tag", "shiny.tag.list", "html")) |
||
86 | +140 |
) |
||
87 | -- | - - | -||
88 | -! | -
- bookmark_panel_ui <- ui_bookmark_panel(ns("bookmark_manager"), modules)- |
- ||
89 | -! | -
- data_elem <- ui_init_data(ns("data"), data = data)- |
- ||
90 | -! | +|||
141 | +10x |
- if (!is.null(data)) {+ checkmate::assert( |
||
91 | -! | +|||
142 | +10x |
- modules$children <- c(list(teal_data_module = data_elem), modules$children)+ .var.name = "header", |
||
92 | -+ | |||
143 | +10x |
- }+ checkmate::check_string(header), |
||
93 | -! | +|||
144 | +10x |
- tabs_elem <- ui_teal_module(id = ns("teal_modules"), modules = modules)+ checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html")) |
||
94 | +145 | - - | -||
95 | -! | -
- fluidPage(+ ) |
||
96 | -! | +|||
146 | +10x |
- id = id,+ checkmate::assert( |
||
97 | -! | +|||
147 | +10x |
- title = title,+ .var.name = "footer", |
||
98 | -! | +|||
148 | +10x |
- theme = get_teal_bs_theme(),+ checkmate::check_string(footer), |
||
99 | -! | +|||
149 | +10x |
- include_teal_css_js(),+ checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html")) |
||
100 | -! | +|||
150 | +
- tags$header(header),+ ) |
|||
101 | -! | +|||
151 | +10x |
- tags$hr(class = "my-2"),+ checkmate::assert_character(id, max.len = 1, any.missing = FALSE) |
||
102 | -! | +|||
152 | +
- shiny_busy_message_panel,+ |
|||
103 | -! | +|||
153 | +
- tags$div(+ # log |
|||
104 | -! | +|||
154 | +10x |
- id = ns("tabpanel_wrapper"),+ teal.logger::log_system_info() |
||
105 | -! | +|||
155 | +
- class = "teal-body",+ |
|||
106 | -! | +|||
156 | +
- tabs_elem+ # argument transformations |
|||
107 | +157 |
- ),+ ## `modules` - landing module |
||
108 | -! | +|||
158 | +10x |
- tags$div(+ landing <- extract_module(modules, "teal_module_landing") |
||
109 | -! | +|||
159 | +10x |
- id = ns("options_buttons"),+ if (length(landing) == 1L) { |
||
110 | +160 | ! |
- style = "position: absolute; right: 10px;",+ landing_popup <- landing[[1L]] |
|
111 | +161 | ! |
- bookmark_panel_ui,+ modules <- drop_module(modules, "teal_module_landing") |
|
112 | +162 | ! |
- tags$button(+ lifecycle::deprecate_soft( |
|
113 | +163 | ! |
- class = "btn action-button filter_hamburger", # see sidebar.css for style filter_hamburger+ when = "0.15.3", |
|
114 | +164 | ! |
- href = "javascript:void(0)",+ what = "landing_popup_module()", |
|
115 | +165 | ! |
- onclick = sprintf("toggleFilterPanel('%s');", ns("tabpanel_wrapper")),+ details = paste( |
|
116 | +166 | ! |
- title = "Toggle filter panel",+ "Pass `landing_popup_module` to the `landing_popup` argument of the `init` ", |
|
117 | +167 | ! |
- icon("fas fa-bars")+ "instead of wrapping it into `modules()` and passing to the `modules` argument" |
|
118 | +168 |
- ),- |
- ||
119 | -! | -
- ui_snapshot_manager_panel(ns("snapshot_manager_panel")),- |
- ||
120 | -! | -
- ui_filter_manager_panel(ns("filter_manager_panel"))+ ) |
||
121 | +169 |
- ),+ ) |
||
122 | -! | +|||
170 | +10x |
- tags$script(+ } else if (length(landing) > 1L) { |
||
123 | +171 | ! |
- HTML(+ stop("Only one `landing_popup_module` can be used.") |
|
124 | -! | +|||
172 | +
- sprintf(+ } |
|||
125 | +173 |
- "+ |
||
126 | -! | +|||
174 | +
- $(document).ready(function() {+ ## `filter` - set app_id attribute unless present (when restoring bookmark) |
|||
127 | -! | +|||
175 | +10x |
- $('#%s').appendTo('#%s');+ if (is.null(attr(filter, "app_id", exact = TRUE))) attr(filter, "app_id") <- create_app_id(data, modules) |
||
128 | +176 |
- });+ |
||
129 | +177 |
- ",+ ## `filter` - convert teal.slice::teal_slices to teal::teal_slices |
||
130 | -! | +|||
178 | +10x |
- ns("options_buttons"),+ filter <- as.teal_slices(as.list(filter)) |
||
131 | -! | +|||
179 | +
- ns("teal_modules-active_tab")+ |
|||
132 | +180 |
- )+ # argument checking (interdependent) |
||
133 | +181 |
- )+ ## `filter` - `modules` |
||
134 | -+ | |||
182 | +10x |
- ),+ if (isTRUE(attr(filter, "module_specific"))) { |
||
135 | +183 | ! |
- tags$hr(),+ module_names <- unlist(c(module_labels(modules), "global_filters")) |
|
136 | +184 | ! |
- tags$footer(+ failed_mod_names <- setdiff(names(attr(filter, "mapping")), module_names) |
|
137 | +185 | ! |
- tags$div(+ if (length(failed_mod_names)) { |
|
138 | +186 | ! |
- footer,+ stop( |
|
139 | +187 | ! |
- teal.widgets::verbatim_popup_ui(ns("sessionInfo"), "Session Info", type = "link"),+ sprintf( |
|
140 | +188 | ! |
- br(),+ "Some module names in the mapping arguments don't match module labels.\n %s not in %s", |
|
141 | +189 | ! |
- ui_teal_lockfile(ns("lockfile")),+ toString(failed_mod_names), |
|
142 | +190 | ! |
- textOutput(ns("identifier"))+ toString(unique(module_names)) |
|
143 | +191 |
- )+ ) |
||
144 | +192 |
- )+ ) |
||
145 | +193 |
- )+ } |
||
146 | +194 |
- }+ |
||
147 | -+ | |||
195 | +! |
-
+ if (anyDuplicated(module_names)) { |
||
148 | +196 |
- #' @rdname module_teal+ # In teal we are able to set nested modules with duplicated label. |
||
149 | +197 |
- #' @export+ # Because mapping argument bases on the relationship between module-label and filter-id, |
||
150 | +198 |
- srv_teal <- function(id, data, modules, filter = teal_slices()) {+ # it is possible that module-label in mapping might refer to multiple teal_module (identified by the same label) |
||
151 | -72x | +|||
199 | +! |
- checkmate::assert_character(id, max.len = 1, any.missing = FALSE)+ stop( |
||
152 | -72x | +|||
200 | +! |
- checkmate::assert_multi_class(data, c("teal_data", "teal_data_module", "reactive", "reactiveVal"))+ sprintf( |
||
153 | -71x | +|||
201 | +! |
- checkmate::assert_class(modules, "teal_modules")+ "Module labels should be unique when teal_slices(mapping = TRUE). Duplicated labels:\n%s ", |
||
154 | -71x | +|||
202 | +! |
- checkmate::assert_class(filter, "teal_slices")+ toString(module_names[duplicated(module_names)]) |
||
155 | +203 | - - | -||
156 | -71x | -
- moduleServer(id, function(input, output, session) {- |
- ||
157 | -71x | -
- logger::log_debug("srv_teal initializing.")+ ) |
||
158 | +204 | - - | -||
159 | -71x | -
- srv_teal_lockfile("lockfile")+ ) |
||
160 | +205 | - - | -||
161 | -71x | -
- output$identifier <- renderText(- |
- ||
162 | -71x | -
- paste0("Pid:", Sys.getpid(), " Token:", substr(session$token, 25, 32))+ } |
||
163 | +206 |
- )+ } |
||
164 | +207 | |||
165 | -71x | +|||
208 | +
- teal.widgets::verbatim_popup_srv(+ ## `data` - `modules` |
|||
166 | -71x | +209 | +10x |
- "sessionInfo",+ if (inherits(data, "teal_data")) { |
167 | -71x | +210 | +9x |
- verbatim_content = utils::capture.output(utils::sessionInfo()),+ if (length(ls(teal.code::get_env(data))) == 0) { |
168 | -71x | +211 | +1x |
- title = "SessionInfo"+ stop("The environment of `data` is empty.") |
169 | +212 |
- )+ } |
||
170 | +213 | |||
171 | -- |
- # `JavaScript` code- |
- ||
172 | -71x | +214 | +8x |
- run_js_files(files = "init.js")+ is_modules_ok <- check_modules_datanames(modules, ls(teal.code::get_env(data))) |
173 | -+ | |||
215 | +8x |
-
+ if (!isTRUE(is_modules_ok) && length(unlist(extract_transformers(modules))) == 0) { |
||
174 | -+ | |||
216 | +1x |
- # set timezone in shiny app+ lapply(is_modules_ok$string, warning, call. = FALSE) |
||
175 | +217 |
- # timezone is set in the early beginning so it will be available also+ } |
||
176 | +218 |
- # for `DDL` and all shiny modules- |
- ||
177 | -71x | -
- get_client_timezone(session$ns)- |
- ||
178 | -71x | -
- observeEvent(+ |
||
179 | -71x | +219 | +8x |
- eventExpr = input$timezone,+ is_filter_ok <- check_filter_datanames(filter, ls(teal.code::get_env(data))) |
180 | -71x | +220 | +8x |
- once = TRUE,+ if (!isTRUE(is_filter_ok)) { |
181 | -71x | +221 | +1x |
- handlerExpr = {+ warning(is_filter_ok) |
182 | -! | +|||
222 | +
- session$userData$timezone <- input$timezone+ # we allow app to continue if applied filters are outside |
|||
183 | -! | +|||
223 | +
- logger::log_debug("srv_teal@1 Timezone set to client's timezone: { input$timezone }.")+ # of possible data range |
|||
184 | +224 |
- }+ } |
||
185 | +225 |
- )+ } |
||
186 | +226 | |||
187 | -71x | +227 | +9x |
- data_rv <- srv_init_data("data", data = data, modules = modules, filter = filter)+ reporter <- teal.reporter::Reporter$new()$set_id(attr(filter, "app_id")) |
188 | -70x | +228 | +9x |
- datasets_rv <- if (!isTRUE(attr(filter, "module_specific"))) {+ if (is_arg_used(modules, "reporter") && length(extract_module(modules, "teal_module_previewer")) == 0) { |
189 | -61x | +|||
229 | +! |
- eventReactive(data_rv(), {+ modules <- append_module( |
||
190 | -50x | +|||
230 | +! |
- if (!inherits(data_rv(), "teal_data")) {+ modules, |
||
191 | +231 | ! |
- stop("data_rv must be teal_data object.")+ reporter_previewer_module(server_args = list(previewer_buttons = c("download", "reset"))) |
|
192 | +232 |
- }- |
- ||
193 | -50x | -
- logger::log_debug("srv_teal@1 initializing FilteredData")+ ) |
||
194 | -50x | +|||
233 | +
- teal_data_to_filtered_data(data_rv())+ } |
|||
195 | +234 |
- })+ |
||
196 | -+ | |||
235 | +9x |
- }+ ns <- NS(id) |
||
197 | +236 |
-
+ # Note: UI must be a function to support bookmarking. |
||
198 | -70x | +237 | +9x |
- module_labels <- unlist(module_labels(modules), use.names = FALSE)+ res <- list( |
199 | -70x | +238 | +9x |
- slices_global <- methods::new(".slicesGlobal", filter, module_labels)+ ui = function(request) { |
200 | -70x | +|||
239 | +! |
- modules_output <- srv_teal_module(+ ui_teal( |
||
201 | -70x | +|||
240 | +! |
- id = "teal_modules",+ id = ns("teal"),+ |
+ ||
241 | +! | +
+ modules = modules, |
||
202 | -70x | +|||
242 | +! |
- data_rv = data_rv,+ title = title, |
||
203 | -70x | +|||
243 | +! |
- datasets = datasets_rv,+ header = header, |
||
204 | -70x | +|||
244 | +! |
- modules = modules,+ footer = footer |
||
205 | -70x | +|||
245 | +
- slices_global = slices_global+ ) |
|||
206 | +246 |
- )+ }, |
||
207 | -69x | +247 | +9x |
- mapping_table <- srv_filter_manager_panel("filter_manager_panel", slices_global = slices_global)+ server = function(input, output, session) { |
208 | -69x | +|||
248 | +! |
- snapshots <- srv_snapshot_manager_panel("snapshot_manager_panel", slices_global = slices_global)+ if (!is.null(landing_popup)) { |
||
209 | -69x | +|||
249 | +! |
- srv_bookmark_panel("bookmark_manager", modules)+ do.call(landing_popup$server, c(list(id = "landing_module_shiny_id"), landing_popup$server_args)) |
||
210 | +250 |
-
+ } |
||
211 | -69x | +|||
251 | +! |
- if (inherits(data, "teal_data_module")) {+ srv_teal(id = ns("teal"), data = data, modules = modules, filter = deep_copy_filter(filter)) |
||
212 | -8x | +|||
252 | +
- setBookmarkExclude(c("teal_modules-active_tab"))+ } |
|||
213 | +253 |
- }+ ) |
||
214 | +254 |
- })+ + |
+ ||
255 | +9x | +
+ logger::log_debug("init teal app has been initialized.") |
||
215 | +256 | |||
216 | -69x | +257 | +9x |
- invisible(NULL)+ res |
217 | +258 |
}@@ -35359,14 +35753,14 @@ teal coverage - 57.64% |
1 |
- #' Create a `teal` module for previewing a report+ #' An example `teal` module |
||
3 |
- #' @description `r lifecycle::badge("experimental")`+ #' `r lifecycle::badge("experimental")` |
||
5 |
- #' This function wraps [teal.reporter::reporter_previewer_ui()] and+ #' @inheritParams teal_modules |
||
6 |
- #' [teal.reporter::reporter_previewer_srv()] into a `teal_module` to be+ #' @return A `teal` module which can be included in the `modules` argument to [init()]. |
||
7 |
- #' used in `teal` applications.+ #' @examples |
||
8 |
- #'+ #' app <- init( |
||
9 |
- #' If you are creating a `teal` application using [init()] then this+ #' data = teal_data(IRIS = iris, MTCARS = mtcars), |
||
10 |
- #' module will be added to your application automatically if any of your `teal_modules`+ #' modules = example_module() |
||
11 |
- #' support report generation.+ #' ) |
||
12 |
- #'+ #' if (interactive()) { |
||
13 |
- #' @inheritParams teal_modules+ #' shinyApp(app$ui, app$server) |
||
14 |
- #' @param server_args (named `list`)+ #' } |
||
15 |
- #' Arguments passed to [teal.reporter::reporter_previewer_srv()].+ #' @export |
||
16 |
- #'+ example_module <- function(label = "example teal module", datanames = "all", transformers = list()) { |
||
17 | -+ | 37x |
- #' @return+ checkmate::assert_string(label) |
18 | -+ | 37x |
- #' `teal_module` (extended with `teal_module_previewer` class) containing the `teal.reporter` previewer functionality.+ ans <- module( |
19 | -+ | 37x |
- #'+ label, |
20 | -+ | 37x |
- #' @export+ server = function(id, data) { |
21 | -+ | 2x |
- #'+ checkmate::assert_class(isolate(data()), "teal_data") |
22 | -+ | 2x |
- reporter_previewer_module <- function(label = "Report previewer", server_args = list()) {+ moduleServer(id, function(input, output, session) { |
23 | -7x | +2x |
- checkmate::assert_string(label)+ datanames_rv <- reactive(ls(teal.code::get_env((req(data()))))) |
24 | -5x | +2x |
- checkmate::assert_list(server_args, names = "named")+ observeEvent(datanames_rv(), { |
25 | -5x | +2x |
- checkmate::assert_true(all(names(server_args) %in% names(formals(teal.reporter::reporter_previewer_srv))))+ selected <- input$dataname |
26 | -+ | 2x |
-
+ if (identical(selected, "")) { |
27 | -3x | +! |
- message("Initializing reporter_previewer_module")+ selected <- restoreInput(session$ns("dataname"), NULL) |
28 | -+ | 2x |
-
+ } else if (isFALSE(selected %in% datanames_rv())) { |
29 | -3x | +! |
- srv <- function(id, reporter, ...) {+ selected <- datanames_rv()[1] |
30 | -! | +
- teal.reporter::reporter_previewer_srv(id, reporter, ...)+ } |
|
31 | -+ | 2x |
- }+ updateSelectInput( |
32 | -+ | 2x |
-
+ session = session, |
33 | -3x | +2x |
- ui <- function(id, ...) {+ inputId = "dataname", |
34 | -! | +2x |
- teal.reporter::reporter_previewer_ui(id, ...)+ choices = datanames_rv(), |
35 | -+ | 2x |
- }+ selected = selected |
36 |
-
+ ) |
||
37 | -3x | +
- module <- module(+ }) |
|
38 | -3x | +
- label = "temporary label",+ |
|
39 | -3x | +2x |
- server = srv, ui = ui,+ output$text <- renderPrint({ |
40 | -3x | +2x |
- server_args = server_args, ui_args = list(), datanames = NULL+ req(input$dataname) |
41 | -+ | ! |
- )+ data()[[input$dataname]] |
42 |
- # Module is created with a placeholder label and the label is changed later.+ }) |
||
43 |
- # This is to prevent another module being labeled "Report previewer".+ |
||
44 | -3x | +2x |
- class(module) <- c(class(module), "teal_module_previewer")+ teal.widgets::verbatim_popup_srv( |
45 | -3x | +2x |
- module$label <- label+ id = "rcode", |
46 | -3x | +2x |
- attr(module, "teal_bookmarkable") <- TRUE+ verbatim_content = reactive(teal.code::get_code(data())), |
47 | -3x | +2x |
- module+ title = "Example Code" |
48 |
- }- |
-
1 | -- |
- setOldClass("teal_data_module")+ ) |
|
2 | +49 |
-
+ }) |
|
3 | +50 |
- #' Evaluate code on `teal_data_module`+ }, |
|
4 | -+ | ||
51 | +37x |
- #'+ ui = function(id) { |
|
5 | -+ | ||
52 | +! |
- #' @details+ ns <- NS(id) |
|
6 | -+ | ||
53 | +! |
- #' `eval_code` evaluates given code in the environment of the `teal_data` object created by the `teal_data_module`.+ teal.widgets::standard_layout( |
|
7 | -+ | ||
54 | +! |
- #' The code is added to the `@code` slot of the `teal_data`.+ output = verbatimTextOutput(ns("text")), |
|
8 | -+ | ||
55 | +! |
- #'+ encoding = tags$div( |
|
9 | -+ | ||
56 | +! |
- #' @param object (`teal_data_module`)+ selectInput(ns("dataname"), "Choose a dataset", choices = NULL), |
|
10 | -+ | ||
57 | +! |
- #' @inheritParams teal.code::eval_code+ teal.widgets::verbatim_popup_ui(ns("rcode"), "Show R code") |
|
11 | +58 |
- #'+ ) |
|
12 | +59 |
- #' @return+ ) |
|
13 | +60 |
- #' `eval_code` returns a `teal_data_module` object with a delayed evaluation of `code` when the module is run.+ }, |
|
14 | -+ | ||
61 | +37x |
- #'+ datanames = datanames, |
|
15 | -+ | ||
62 | +37x |
- #' @examples+ transformers = transformers |
|
16 | +63 |
- #' eval_code(tdm, "dataset1 <- subset(dataset1, Species == 'virginica')")+ ) |
|
17 | -+ | ||
64 | +37x |
- #'+ attr(ans, "teal_bookmarkable") <- TRUE |
|
18 | -+ | ||
65 | +37x |
- #' @include teal_data_module.R+ ans |
|
19 | +66 |
- #' @name eval_code+ } |
20 | +1 |
- #' @rdname teal_data_module+ #' UI and server modules of `teal` |
|
21 | +2 |
- #' @aliases eval_code,teal_data_module,character-method+ #' |
|
22 | +3 |
- #' @aliases eval_code,teal_data_module,language-method+ #' @description `r lifecycle::badge("deprecated")` |
|
23 | +4 |
- #' @aliases eval_code,teal_data_module,expression-method+ #' Please use [`module_teal`] instead. |
|
24 | +5 |
#' |
|
25 | +6 |
- #' @importFrom methods setMethod+ #' @inheritParams ui_teal |
|
26 | +7 |
- #' @importMethodsFrom teal.code eval_code+ #' @inheritParams srv_teal |
|
27 | +8 |
#' |
|
28 | +9 |
- setMethod("eval_code", signature = c("teal_data_module", "character"), function(object, code) {- |
- |
29 | -9x | -
- teal_data_module(+ #' @return |
|
30 | -9x | +||
10 | +
- ui = function(id) {+ #' Returns a `reactive` expression containing a `teal_data` object when data is loaded or `NULL` when it is not. |
||
31 | -1x | +||
11 | +
- ns <- NS(id)+ #' @name module_teal_with_splash |
||
32 | -1x | +||
12 | +
- object$ui(ns("mutate_inner"))+ #' |
||
33 | +13 |
- },+ NULL |
|
34 | -9x | +||
14 | +
- server = function(id) {+ |
||
35 | -7x | +||
15 | +
- moduleServer(id, function(input, output, session) {+ #' @export |
||
36 | -7x | +||
16 | +
- teal_data_rv <- object$server("mutate_inner")+ #' @rdname module_teal_with_splash |
||
37 | -6x | +||
17 | +
- td <- eventReactive(teal_data_rv(),+ ui_teal_with_splash <- function(id, |
||
38 | +18 |
- {+ data, |
|
39 | -6x | +||
19 | +
- if (inherits(teal_data_rv(), c("teal_data", "qenv.error"))) {+ title = build_app_title(), |
||
40 | -4x | +||
20 | +
- eval_code(teal_data_rv(), code)+ header = tags$p(), |
||
41 | +21 |
- } else {+ footer = tags$p()) { |
|
42 | -2x | +||
22 | +! |
- teal_data_rv()+ lifecycle::deprecate_soft( |
|
43 | -+ | ||
23 | +! |
- }+ when = "0.16", |
|
44 | -+ | ||
24 | +! |
- },+ what = "ui_teal_with_splash()", |
|
45 | -6x | +||
25 | +! |
- ignoreNULL = FALSE+ details = "Deprecated, please use `ui_teal` instead" |
|
46 | +26 |
- )+ ) |
|
47 | -6x | +||
27 | +! |
- td+ ui_teal(id = id, title = title, header = header, footer = footer) |
|
48 | +28 |
- })+ } |
|
49 | +29 |
- }+ |
|
50 | +30 |
- )+ #' @export |
|
51 | +31 |
- })+ #' @rdname module_teal_with_splash |
|
52 | +32 |
-
+ srv_teal_with_splash <- function(id, data, modules, filter = teal_slices()) { |
|
53 | -+ | ||
33 | +! |
- setMethod("eval_code", signature = c("teal_data_module", "language"), function(object, code) {+ lifecycle::deprecate_soft( |
|
54 | -1x | +||
34 | +! |
- eval_code(object, code = paste(lang2calls(code), collapse = "\n"))+ when = "0.16", |
|
55 | -+ | ||
35 | +! |
- })+ what = "srv_teal_with_splash()", |
|
56 | -+ | ||
36 | +! |
-
+ details = "Deprecated, please use `srv_teal` instead" |
|
57 | +37 |
- setMethod("eval_code", signature = c("teal_data_module", "expression"), function(object, code) {+ ) |
|
58 | -2x | +||
38 | +! |
- eval_code(object, code = paste(lang2calls(code), collapse = "\n"))+ srv_teal(id = id, data = data, modules = modules, filter = filter) |
|
59 | +39 |
- })+ } |
1 |
- #' Include `CSS` files from `/inst/css/` package directory to application header+ #' Validate that dataset has a minimum number of observations |
||
3 |
- #' `system.file` should not be used to access files in other packages, it does+ #' `r lifecycle::badge("stable")` |
||
4 |
- #' not work with `devtools`. Therefore, we redefine this method in each package+ #' |
||
5 |
- #' as needed. Thus, we do not export this method.+ #' This function is a wrapper for `shiny::validate`. |
||
7 |
- #' @param pattern (`character`) pattern of files to be included+ #' @param x (`data.frame`) |
||
8 |
- #'+ #' @param min_nrow (`numeric(1)`) Minimum allowed number of rows in `x`. |
||
9 |
- #' @return HTML code that includes `CSS` files.+ #' @param complete (`logical(1)`) Flag specifying whether to check only complete cases. Defaults to `FALSE`. |
||
10 |
- #' @keywords internal+ #' @param allow_inf (`logical(1)`) Flag specifying whether to allow infinite values. Defaults to `TRUE`. |
||
11 |
- include_css_files <- function(pattern = "*") {+ #' @param msg (`character(1)`) Additional message to display alongside the default message. |
||
12 | -! | +
- css_files <- list.files(+ #' |
|
13 | -! | +
- system.file("css", package = "teal", mustWork = TRUE),+ #' @export |
|
14 | -! | +
- pattern = pattern, full.names = TRUE+ #' |
|
15 |
- )+ #' @examples |
||
16 |
-
+ #' library(teal) |
||
17 | -! | +
- singleton(+ #' ui <- fluidPage( |
|
18 | -! | +
- tags$head(lapply(css_files, includeCSS))+ #' sliderInput("len", "Max Length of Sepal", |
|
19 |
- )+ #' min = 4.3, max = 7.9, value = 5 |
||
20 |
- }+ #' ), |
||
21 |
-
+ #' plotOutput("plot") |
||
22 |
- #' Include `JS` files from `/inst/js/` package directory to application header+ #' ) |
||
24 |
- #' `system.file` should not be used to access files in other packages, it does+ #' server <- function(input, output) { |
||
25 |
- #' not work with `devtools`. Therefore, we redefine this method in each package+ #' output$plot <- renderPlot({ |
||
26 |
- #' as needed. Thus, we do not export this method+ #' iris_df <- iris[iris$Sepal.Length <= input$len, ] |
||
27 |
- #'+ #' validate_has_data( |
||
28 |
- #' @param pattern (`character`) pattern of files to be included, passed to `system.file`+ #' iris_df, |
||
29 |
- #' @param except (`character`) vector of basename filenames to be excluded+ #' min_nrow = 10, |
||
30 |
- #'+ #' complete = FALSE, |
||
31 |
- #' @return HTML code that includes `JS` files.+ #' msg = "Please adjust Max Length of Sepal" |
||
32 |
- #' @keywords internal+ #' ) |
||
33 |
- include_js_files <- function(pattern = NULL, except = NULL) {+ #' |
||
34 | -! | +
- checkmate::assert_character(except, min.len = 1, any.missing = FALSE, null.ok = TRUE)+ #' hist(iris_df$Sepal.Length, breaks = 5) |
|
35 | -! | +
- js_files <- list.files(system.file("js", package = "teal", mustWork = TRUE), pattern = pattern, full.names = TRUE)+ #' }) |
|
36 | -! | +
- js_files <- js_files[!(basename(js_files) %in% except)] # no-op if except is NULL+ #' } |
|
37 |
-
+ #' if (interactive()) { |
||
38 | -! | +
- singleton(lapply(js_files, includeScript))+ #' shinyApp(ui, server) |
|
39 |
- }+ #' } |
||
40 |
-
+ #' |
||
41 |
- #' Run `JS` file from `/inst/js/` package directory+ validate_has_data <- function(x, |
||
42 |
- #'+ min_nrow = NULL, |
||
43 |
- #' This is triggered from the server to execute on the client+ complete = FALSE, |
||
44 |
- #' rather than triggered directly on the client.+ allow_inf = TRUE, |
||
45 |
- #' Unlike `include_js_files` which includes `JavaScript` functions,+ msg = NULL) { |
||
46 | +17x | +
+ checkmate::assert_string(msg, null.ok = TRUE)+ |
+ |
47 | +15x | +
+ checkmate::assert_data_frame(x)+ |
+ |
48 | +15x | +
+ if (!is.null(min_nrow)) {+ |
+ |
49 | +15x | +
+ if (complete) {+ |
+ |
50 | +5x | +
+ complete_index <- stats::complete.cases(x)+ |
+ |
51 | +5x | +
+ validate(need(+ |
+ |
52 | +5x | +
+ sum(complete_index) > 0 && nrow(x[complete_index, , drop = FALSE]) >= min_nrow,+ |
+ |
53 | +5x | +
+ paste(c(paste("Number of complete cases is less than:", min_nrow), msg), collapse = "\n")+ |
+ |
54 |
- #' the `run_js` actually executes `JavaScript` functions.+ )) |
||
47 | +55 |
- #'+ } else {+ |
+ |
56 | +10x | +
+ validate(need(+ |
+ |
57 | +10x | +
+ nrow(x) >= min_nrow,+ |
+ |
58 | +10x | +
+ paste(+ |
+ |
59 | +10x | +
+ c(paste("Minimum number of records not met: >=", min_nrow, "records required."), msg),+ |
+ |
60 | +10x | +
+ collapse = "\n" |
|
48 | +61 |
- #' `system.file` should not be used to access files in other packages, it does+ ) |
|
49 | +62 |
- #' not work with `devtools`. Therefore, we redefine this method in each package+ )) |
|
50 | +63 |
- #' as needed. Thus, we do not export this method.+ } |
|
51 | +64 | ++ | + + | +
65 | +10x | +
+ if (!allow_inf) {+ |
+ |
66 | +6x | +
+ validate(need(+ |
+ |
67 | +6x | +
+ all(vapply(x, function(col) !is.numeric(col) || !any(is.infinite(col)), logical(1))),+ |
+ |
68 | +6x | +
+ "Dataframe contains Inf values which is not allowed."+ |
+ |
69 | ++ |
+ ))+ |
+ |
70 | ++ |
+ }+ |
+ |
71 | ++ |
+ }+ |
+ |
72 | ++ |
+ }+ |
+ |
73 | ++ | + + | +|
74 | ++ |
+ #' Validate that dataset has unique rows for key variables+ |
+ |
75 |
#' |
||
52 | +76 |
- #' @param files (`character`) vector of filenames.+ #' `r lifecycle::badge("stable")` |
|
53 | +77 |
#' |
|
54 | +78 |
- #' @return `NULL`, invisibly.+ #' This function is a wrapper for `shiny::validate`. |
|
55 | +79 |
- #' @keywords internal+ #' |
|
56 | +80 |
- run_js_files <- function(files) {+ #' @param x (`data.frame`) |
|
57 | -71x | +||
81 | ++ |
+ #' @param key (`character`) Vector of ID variables from `x` that identify unique records.+ |
+ |
82 | ++ |
+ #'+ |
+ |
83 | ++ |
+ #' @export+ |
+ |
84 | ++ |
+ #'+ |
+ |
85 | ++ |
+ #' @examples+ |
+ |
86 | +
- checkmate::assert_character(files, min.len = 1, any.missing = FALSE)+ #' iris$id <- rep(1:50, times = 3) |
||
58 | -71x | +||
87 | +
- lapply(files, function(file) {+ #' ui <- fluidPage( |
||
59 | -71x | +||
88 | +
- shinyjs::runjs(paste0(readLines(system.file("js", file, package = "teal", mustWork = TRUE)), collapse = "\n"))+ #' selectInput( |
||
60 | +89 |
- })+ #' inputId = "species", |
|
61 | -71x | +||
90 | +
- invisible(NULL)+ #' label = "Select species", |
||
62 | +91 |
- }+ #' choices = c("setosa", "versicolor", "virginica"), |
|
63 | +92 |
-
+ #' selected = "setosa", |
|
64 | +93 |
- #' Code to include `teal` `CSS` and `JavaScript` files+ #' multiple = TRUE |
|
65 | +94 |
- #'+ #' ), |
|
66 | +95 |
- #' This is useful when you want to use the same `JavaScript` and `CSS` files that are+ #' plotOutput("plot") |
|
67 | +96 |
- #' used with the `teal` application.+ #' ) |
|
68 | +97 |
- #' This is also useful for running standalone modules in `teal` with the correct+ #' server <- function(input, output) { |
|
69 | +98 |
- #' styles.+ #' output$plot <- renderPlot({ |
|
70 | +99 |
- #' Also initializes `shinyjs` so you can use it.+ #' iris_f <- iris[iris$Species %in% input$species, ] |
|
71 | +100 |
- #'+ #' validate_one_row_per_id(iris_f, key = c("id")) |
|
72 | +101 |
- #' Simply add `include_teal_css_js()` as one of the UI elements.+ #' |
|
73 | +102 |
- #' @return A `shiny.tag.list`.+ #' hist(iris_f$Sepal.Length, breaks = 5) |
|
74 | +103 |
- #' @keywords internal+ #' }) |
|
75 | +104 |
- include_teal_css_js <- function() {+ #' } |
|
76 | -! | +||
105 | +
- tagList(+ #' if (interactive()) { |
||
77 | -! | +||
106 | +
- shinyjs::useShinyjs(),+ #' shinyApp(ui, server) |
||
78 | -! | +||
107 | +
- include_css_files(),+ #' } |
||
79 | +108 |
- # init.js is executed from the server+ #' |
|
80 | -! | +||
109 | +
- include_js_files(except = "init.js"),+ validate_one_row_per_id <- function(x, key = c("USUBJID", "STUDYID")) { |
||
81 | +110 | ! |
- shinyjs::hidden(icon("fas fa-gear")), # add hidden icon to load font-awesome css for icons+ validate(need(!any(duplicated(x[key])), paste("Found more than one row per id."))) |
82 | +111 |
- )+ } |
|
83 | +112 |
- }+ |
1 | +113 |
- #' Store and restore `teal_slices` object+ #' Validates that vector includes all expected values |
|
2 | +114 |
#' |
|
3 | +115 |
- #' Functions that write a `teal_slices` object to a file in the `JSON` format,+ #' `r lifecycle::badge("stable")` |
|
4 | +116 |
- #' and also restore the object from disk.+ #' |
|
5 | +117 |
- #'+ #' This function is a wrapper for `shiny::validate`. |
|
6 | +118 |
- #' Date and date time objects are stored in the following formats:+ #' |
|
7 | +119 |
- #'+ #' @param x Vector of values to test. |
|
8 | +120 |
- #' - `Date` class is converted to the `"ISO8601"` standard (`YYYY-MM-DD`).+ #' @param choices Vector to test against. |
|
9 | +121 |
- #' - `POSIX*t` classes are converted to character by using+ #' @param msg (`character(1)`) Error message to display if some elements of `x` are not elements of `choices`. |
|
10 | +122 |
- #' `format.POSIX*t(usetz = TRUE, tz = "UTC")` (`YYYY-MM-DD HH:MM:SS UTC`, where+ #' |
|
11 | +123 |
- #' `UTC` is the `Coordinated Universal Time` timezone short-code).+ #' @export |
|
12 | +124 |
#' |
|
13 | +125 |
- #' This format is assumed during `slices_restore`. All `POSIX*t` objects in+ #' @examples |
|
14 | +126 |
- #' `selected` or `choices` fields of `teal_slice` objects are always printed in+ #' ui <- fluidPage( |
|
15 | +127 |
- #' `UTC` timezone as well.+ #' selectInput( |
|
16 | +128 |
- #'+ #' "species", |
|
17 | +129 |
- #' @param tss (`teal_slices`) object to be stored.+ #' "Select species", |
|
18 | +130 |
- #' @param file (`character(1)`) file path where `teal_slices` object will be+ #' choices = c("setosa", "versicolor", "virginica", "unknown species"), |
|
19 | +131 |
- #' saved and restored. The file extension should be `".json"`.+ #' selected = "setosa", |
|
20 | +132 |
- #'+ #' multiple = FALSE |
|
21 | +133 |
- #' @return `slices_store` returns `NULL`, invisibly.+ #' ), |
|
22 | +134 |
- #'+ #' verbatimTextOutput("summary") |
|
23 | +135 |
- #' @seealso [teal_slices()]+ #' ) |
|
24 | +136 |
#' |
|
25 | +137 |
- #' @keywords internal+ #' server <- function(input, output) { |
|
26 | +138 |
- #'+ #' output$summary <- renderPrint({ |
|
27 | +139 |
- slices_store <- function(tss, file) {- |
- |
28 | -9x | -
- checkmate::assert_class(tss, "teal_slices")- |
- |
29 | -9x | -
- checkmate::assert_path_for_output(file, overwrite = TRUE, extension = "json")+ #' validate_in(input$species, iris$Species, "Species does not exist.") |
|
30 | +140 |
-
+ #' nrow(iris[iris$Species == input$species, ]) |
|
31 | -9x | +||
141 | +
- cat(format(tss, trim_lines = FALSE), "\n", file = file)+ #' }) |
||
32 | +142 |
- }+ #' } |
|
33 | +143 |
-
+ #' if (interactive()) { |
|
34 | +144 |
- #' @rdname slices_store+ #' shinyApp(ui, server) |
|
35 | +145 |
- #' @return `slices_restore` returns a `teal_slices` object restored from the file.+ #' } |
|
36 | +146 |
- #' @keywords internal+ #' |
|
37 | +147 |
- slices_restore <- function(file) {+ validate_in <- function(x, choices, msg) { |
|
38 | -9x | +||
148 | +! |
- checkmate::assert_file_exists(file, access = "r", extension = "json")+ validate(need(length(x) > 0 && length(choices) > 0 && all(x %in% choices), msg)) |
|
39 | +149 | - - | -|
40 | -9x | -
- tss_json <- jsonlite::fromJSON(file, simplifyDataFrame = FALSE)+ } |
|
41 | -9x | +||
150 | +
- tss_json$slices <-+ |
||
42 | -9x | +||
151 | +
- lapply(tss_json$slices, function(slice) {+ #' Validates that vector has length greater than 0 |
||
43 | -9x | +||
152 | +
- for (field in c("selected", "choices")) {+ #' |
||
44 | -18x | +||
153 | +
- if (!is.null(slice[[field]])) {+ #' `r lifecycle::badge("stable")` |
||
45 | -12x | +||
154 | +
- if (length(slice[[field]]) > 0) {+ #' |
||
46 | -9x | +||
155 | +
- date_partial_regex <- "^[0-9]{4}-[0-9]{2}-[0-9]{2}"+ #' This function is a wrapper for `shiny::validate`. |
||
47 | -9x | +||
156 | +
- time_stamp_regex <- paste0(date_partial_regex, "\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\sUTC$")+ #' |
||
48 | +157 |
-
+ #' @param x vector |
|
49 | -9x | +||
158 | +
- slice[[field]] <-+ #' @param msg message to display |
||
50 | -9x | +||
159 | +
- if (all(grepl(paste0(date_partial_regex, "$"), slice[[field]]))) {+ #' |
||
51 | -3x | +||
160 | +
- as.Date(slice[[field]])+ #' @export |
||
52 | -9x | +||
161 | +
- } else if (all(grepl(time_stamp_regex, slice[[field]]))) {+ #' |
||
53 | -3x | +||
162 | +
- as.POSIXct(slice[[field]], tz = "UTC")+ #' @examples |
||
54 | +163 |
- } else {+ #' data <- data.frame( |
|
55 | -3x | +||
164 | +
- slice[[field]]+ #' id = c(1:10, 11:20, 1:10), |
||
56 | +165 |
- }+ #' strata = rep(c("A", "B"), each = 15) |
|
57 | +166 |
- } else {+ #' ) |
|
58 | -3x | +||
167 | +
- slice[[field]] <- character(0)+ #' ui <- fluidPage( |
||
59 | +168 |
- }+ #' selectInput("ref1", "Select strata1 to compare", |
|
60 | +169 |
- }+ #' choices = c("A", "B", "C"), selected = "A" |
|
61 | +170 |
- }+ #' ), |
|
62 | -9x | +||
171 | +
- slice+ #' selectInput("ref2", "Select strata2 to compare", |
||
63 | +172 |
- })+ #' choices = c("A", "B", "C"), selected = "B" |
|
64 | +173 |
-
+ #' ), |
|
65 | -9x | +||
174 | +
- tss_elements <- lapply(tss_json$slices, as.teal_slice)+ #' verbatimTextOutput("arm_summary") |
||
66 | +175 |
-
+ #' ) |
|
67 | -9x | +||
176 | +
- do.call(teal_slices, c(tss_elements, tss_json$attributes))+ #' |
||
68 | +177 |
- }+ #' server <- function(input, output) { |
1 | +178 |
- #' Execute and validate `teal_data_module`+ #' output$arm_summary <- renderText({ |
|
2 | +179 |
- #'+ #' sample_1 <- data$id[data$strata == input$ref1] |
|
3 | +180 |
- #' This is a low level module to handle `teal_data_module` execution and validation.+ #' sample_2 <- data$id[data$strata == input$ref2] |
|
4 | +181 |
- #' [teal_transform_module()] inherits from [teal_data_module()] so it is handled by this module too.+ #' |
|
5 | +182 |
- #' [srv_teal()] accepts various `data` objects and eventually they are all transformed to `reactive`+ #' validate_has_elements(sample_1, "No subjects in strata1.") |
|
6 | +183 |
- #' [teal_data()] which is a standard data class in whole `teal` framework.+ #' validate_has_elements(sample_2, "No subjects in strata2.") |
|
7 | +184 |
#' |
|
8 | +185 |
- #' @section data validation:+ #' paste0( |
|
9 | +186 |
- #'+ #' "Number of samples in: strata1=", length(sample_1), |
|
10 | +187 |
- #' Executed [teal_data_module()] is validated and output is validated for consistency.+ #' " comparions strata2=", length(sample_2) |
|
11 | +188 |
- #' Output `data` is invalid if:+ #' ) |
|
12 | +189 |
- #' 1. [teal_data_module()] is invalid if server doesn't return `reactive`. **Immediately crashes an app!**+ #' }) |
|
13 | +190 |
- #' 2. `reactive` throws a `shiny.error` - happens when module creating [teal_data()] fails.+ #' } |
|
14 | +191 |
- #' 3. `reactive` returns `qenv.error` - happens when [teal_data()] evaluates a failing code.+ #' if (interactive()) { |
|
15 | +192 |
- #' 4. `reactive` object doesn't return [teal_data()].+ #' shinyApp(ui, server) |
|
16 | +193 |
- #' 5. [teal_data()] object lacks any `datanames` specified in the `modules` argument.+ #' } |
|
17 | +194 |
- #'+ validate_has_elements <- function(x, msg) { |
|
18 | -+ | ||
195 | +! |
- #' `teal` (observers in `srv_teal`) always waits to render an app until `reactive` `teal_data` is+ validate(need(length(x) > 0, msg)) |
|
19 | +196 |
- #' returned. If error 2-4 occurs, relevant error message is displayed to app user and after issue is+ } |
|
20 | +197 |
- #' resolved app will continue to run. `teal` guarantees that errors in a data don't crash an app+ |
|
21 | +198 |
- #' (except error 1). This is possible thanks to `.fallback_on_failure` which returns input-data+ #' Validates no intersection between two vectors |
|
22 | +199 |
- #' when output-data fails+ #' |
|
23 | +200 |
- #'+ #' `r lifecycle::badge("stable")` |
|
24 | +201 |
#' |
|
25 | +202 |
- #' @param id (`character(1)`) Module id+ #' This function is a wrapper for `shiny::validate`. |
|
26 | +203 |
- #' @param data (`reactive teal_data`)+ #' |
|
27 | +204 |
- #' @param data_module (`teal_data_module`)+ #' @param x vector |
|
28 | +205 |
- #' @param modules (`teal_modules` or `teal_module`) For `datanames` validation purpose+ #' @param y vector |
|
29 | +206 |
- #' @param validate_shiny_silent_error (`logical`) If `TRUE`, then `shiny.silent.error` is validated and+ #' @param msg (`character(1)`) message to display if `x` and `y` intersect |
|
30 | +207 |
- #' error message is displayed.+ #' |
|
31 | +208 |
- #' Default is `FALSE` to handle empty reactive cycle on `init`.+ #' @export |
|
32 | +209 |
#' |
|
33 | +210 |
- #' @return `reactive` `teal_data`+ #' @examples |
|
34 | +211 |
- #'+ #' data <- data.frame( |
|
35 | +212 |
- #' @rdname module_teal_data+ #' id = c(1:10, 11:20, 1:10), |
|
36 | +213 |
- #' @name module_teal_data+ #' strata = rep(c("A", "B", "C"), each = 10) |
|
37 | +214 |
- #' @keywords internal+ #' ) |
|
38 | +215 |
- NULL+ #' |
|
39 | +216 |
-
+ #' ui <- fluidPage( |
|
40 | +217 |
- #' @rdname module_teal_data+ #' selectInput("ref1", "Select strata1 to compare", |
|
41 | +218 |
- ui_teal_data <- function(id, data_module) {+ #' choices = c("A", "B", "C"), |
|
42 | -! | +||
219 | +
- checkmate::assert_string(id)+ #' selected = "A" |
||
43 | -! | +||
220 | +
- checkmate::assert_class(data_module, "teal_data_module")+ #' ), |
||
44 | -! | +||
221 | +
- ns <- NS(id)+ #' selectInput("ref2", "Select strata2 to compare", |
||
45 | -! | +||
222 | +
- shiny::tagList(+ #' choices = c("A", "B", "C"), |
||
46 | -! | +||
223 | +
- data_module$ui(id = ns("data")),+ #' selected = "B" |
||
47 | -! | +||
224 | +
- ui_validate_reactive_teal_data(ns("validate"))+ #' ), |
||
48 | +225 |
- )+ #' verbatimTextOutput("summary") |
|
49 | +226 |
- }+ #' ) |
|
50 | +227 |
-
+ #' |
|
51 | +228 |
- #' @rdname module_teal_data+ #' server <- function(input, output) { |
|
52 | +229 |
- srv_teal_data <- function(id,+ #' output$summary <- renderText({ |
|
53 | +230 |
- data,+ #' sample_1 <- data$id[data$strata == input$ref1] |
|
54 | +231 |
- data_module,+ #' sample_2 <- data$id[data$strata == input$ref2] |
|
55 | +232 |
- modules = NULL,+ #' |
|
56 | +233 |
- validate_shiny_silent_error = TRUE) {+ #' validate_no_intersection( |
|
57 | -29x | +||
234 | +
- checkmate::assert_string(id)+ #' sample_1, sample_2, |
||
58 | -29x | +||
235 | +
- checkmate::assert_class(data_module, "teal_data_module")+ #' "subjects within strata1 and strata2 cannot overlap" |
||
59 | -29x | +||
236 | +
- checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"), null.ok = TRUE)+ #' ) |
||
60 | +237 |
-
+ #' paste0( |
|
61 | -29x | +||
238 | +
- moduleServer(id, function(input, output, session) {+ #' "Number of subject in: reference treatment=", length(sample_1), |
||
62 | -29x | +||
239 | +
- logger::log_debug("srv_teal_data initializing.")+ #' " comparions treatment=", length(sample_2) |
||
63 | +240 |
-
+ #' ) |
|
64 | -29x | +||
241 | +
- data_out <- if (is_arg_used(data_module$server, "data")) {+ #' }) |
||
65 | -20x | +||
242 | +
- data_module$server(id = "data", data = data)+ #' } |
||
66 | +243 |
- } else {+ #' if (interactive()) { |
|
67 | -9x | +||
244 | +
- data_module$server(id = "data")+ #' shinyApp(ui, server) |
||
68 | +245 |
- }+ #' } |
|
69 | +246 |
-
+ #' |
|
70 | -27x | +||
247 | +
- data_validated <- srv_validate_reactive_teal_data(+ validate_no_intersection <- function(x, y, msg) { |
||
71 | -27x | +||
248 | +! |
- id = "validate",+ validate(need(length(intersect(x, y)) == 0, msg)) |
|
72 | -27x | +||
249 | +
- data = data_out,+ } |
||
73 | -27x | +||
250 | +
- modules = modules,+ |
||
74 | -27x | +||
251 | +
- validate_shiny_silent_error = validate_shiny_silent_error+ |
||
75 | +252 |
- )+ #' Validates that dataset contains specific variable |
|
76 | +253 |
-
+ #' |
|
77 | -27x | +||
254 | +
- .fallback_on_failure(+ #' `r lifecycle::badge("stable")` |
||
78 | -27x | +||
255 | +
- this = data_validated,+ #' |
||
79 | -27x | +||
256 | +
- that = data,+ #' This function is a wrapper for `shiny::validate`. |
||
80 | -27x | +||
257 | +
- label = sprintf("Data element '%s' for module '%s'", id, modules$label)+ #' |
||
81 | +258 |
- )+ #' @param data (`data.frame`) |
|
82 | +259 |
- })+ #' @param varname (`character(1)`) name of variable to check for in `data` |
|
83 | +260 |
- }+ #' @param msg (`character(1)`) message to display if `data` does not include `varname` |
|
84 | +261 |
-
+ #' |
|
85 | +262 |
- #' @rdname module_teal_data+ #' @export |
|
86 | +263 |
- ui_validate_reactive_teal_data <- function(id) {+ #' |
|
87 | -! | +||
264 | +
- tagList(+ #' @examples |
||
88 | -! | +||
265 | +
- uiOutput(NS(id, "shiny_errors")),+ #' data <- data.frame( |
||
89 | -! | +||
266 | +
- uiOutput(NS(id, "shiny_warnings"))+ #' one = rep("a", length.out = 20), |
||
90 | +267 |
- )+ #' two = rep(c("a", "b"), length.out = 20) |
|
91 | +268 |
- }+ #' ) |
|
92 | +269 |
-
+ #' ui <- fluidPage( |
|
93 | +270 |
- #' @rdname module_teal_data+ #' selectInput( |
|
94 | +271 |
- srv_validate_reactive_teal_data <- function(id, # nolint: object_length+ #' "var", |
|
95 | +272 |
- data,+ #' "Select variable", |
|
96 | +273 |
- modules = NULL,+ #' choices = c("one", "two", "three", "four"), |
|
97 | +274 |
- validate_shiny_silent_error = FALSE) {+ #' selected = "one" |
|
98 | -121x | +||
275 | +
- checkmate::assert_string(id)+ #' ), |
||
99 | -121x | +||
276 | +
- checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"), null.ok = TRUE)+ #' verbatimTextOutput("summary") |
||
100 | -121x | +||
277 | +
- checkmate::assert_flag(validate_shiny_silent_error)+ #' ) |
||
101 | +278 |
-
+ #' |
|
102 | -121x | +||
279 | +
- moduleServer(id, function(input, output, session) {+ #' server <- function(input, output) { |
||
103 | -121x | +||
280 | +
- data_out_r <- reactive(tryCatch(data(), error = function(e) e))+ #' output$summary <- renderText({ |
||
104 | +281 |
-
+ #' validate_has_variable(data, input$var) |
|
105 | -121x | +||
282 | +
- data_validated <- reactive({+ #' paste0("Selected treatment variables: ", paste(input$var, collapse = ", ")) |
||
106 | +283 |
- # custom module can return error+ #' }) |
|
107 | -136x | +||
284 | ++ |
+ #' }+ |
+ |
285 | +
- data_out <- data_out_r()+ #' if (interactive()) { |
||
108 | +286 |
-
+ #' shinyApp(ui, server) |
|
109 | +287 |
- # there is an empty reactive cycle on init!+ #' } |
|
110 | -136x | +||
288 | +
- if (inherits(data_out, "shiny.silent.error") && identical(data_out$message, "")) {+ validate_has_variable <- function(data, varname, msg) { |
||
111 | -25x | +||
289 | +! |
- if (!validate_shiny_silent_error) {+ if (length(varname) != 0) { |
|
112 | -25x | +||
290 | +! |
- return(NULL)+ has_vars <- varname %in% names(data) |
|
113 | +291 |
- } else {+ |
|
114 | +292 | ! |
- validate(+ if (!all(has_vars)) { |
115 | +293 | ! |
- need(+ if (missing(msg)) { |
116 | +294 | ! |
- FALSE,+ msg <- sprintf( |
117 | +295 | ! |
- paste(+ "%s does not have the required variables: %s.", |
118 | +296 | ! |
- "Shiny error when executing the `data` module",+ deparse(substitute(data)), |
119 | +297 | ! |
- "Check your inputs or contact app developer if error persists.",+ toString(varname[!has_vars]) |
120 | -! | +||
298 | +
- collapse = "\n"+ ) |
||
121 | +299 |
- )+ } |
|
122 | -+ | ||
300 | +! |
- )+ validate(need(FALSE, msg)) |
|
123 | +301 |
- )+ } |
|
124 | +302 |
- }+ } |
|
125 | +303 |
- }+ } |
|
126 | +304 | ||
127 | +305 |
- # to handle errors and qenv.error(s)- |
- |
128 | -111x | -
- if (inherits(data_out, c("qenv.error", "error"))) {- |
- |
129 | -7x | -
- validate(- |
- |
130 | -7x | -
- need(- |
- |
131 | -7x | -
- FALSE,- |
- |
132 | -7x | -
- paste0(- |
- |
133 | -7x | -
- "Error when executing the `data` module:",- |
- |
134 | -7x | -
- strip_style(paste(data_out$message, collapse = "\n")),- |
- |
135 | -7x | -
- "Check your inputs or contact app developer if error persists.",- |
- |
136 | -7x | -
- collapse = "\n"+ #' Validate that variables has expected number of levels |
|
137 | +306 |
- )+ #' |
|
138 | +307 |
- )+ #' `r lifecycle::badge("stable")` |
|
139 | +308 |
- )+ #' |
|
140 | +309 |
- }+ #' If the number of levels of `x` is less than `min_levels` |
|
141 | +310 | - - | -|
142 | -104x | -
- validate(- |
- |
143 | -104x | -
- need(+ #' or greater than `max_levels` the validation will fail. |
|
144 | -104x | +||
311 | +
- checkmate::test_class(data_out, "teal_data"),+ #' This function is a wrapper for `shiny::validate`. |
||
145 | -104x | +||
312 | +
- paste0(+ #' |
||
146 | -104x | +||
313 | +
- "Assertion on return value from the 'data' module failed:",+ #' @param x variable name. If `x` is not a factor, the unique values |
||
147 | -104x | +||
314 | +
- checkmate::test_class(data_out, "teal_data"),+ #' are treated as levels. |
||
148 | -104x | +||
315 | +
- "Check your inputs or contact app developer if error persists.",+ #' @param min_levels cutoff for minimum number of levels of `x` |
||
149 | -104x | +||
316 | +
- collapse = "\n"+ #' @param max_levels cutoff for maximum number of levels of `x` |
||
150 | +317 |
- )+ #' @param var_name name of variable being validated for use in |
|
151 | +318 |
- )+ #' validation message |
|
152 | +319 |
- )+ #' |
|
153 | +320 |
-
+ #' @export |
|
154 | -103x | +||
321 | +
- data_out+ #' @examples |
||
155 | +322 |
- })+ #' data <- data.frame( |
|
156 | +323 |
-
+ #' one = rep("a", length.out = 20), |
|
157 | -121x | +||
324 | +
- output$shiny_errors <- renderUI({+ #' two = rep(c("a", "b"), length.out = 20), |
||
158 | -136x | +||
325 | +
- data_validated()+ #' three = rep(c("a", "b", "c"), length.out = 20), |
||
159 | -128x | +||
326 | +
- NULL+ #' four = rep(c("a", "b", "c", "d"), length.out = 20), |
||
160 | +327 |
- })+ #' stringsAsFactors = TRUE |
|
161 | +328 |
-
+ #' ) |
|
162 | -121x | +||
329 | +
- output$shiny_warnings <- renderUI({+ #' ui <- fluidPage( |
||
163 | -136x | +||
330 | +
- if (inherits(data_out_r(), "teal_data")) {+ #' selectInput( |
||
164 | -103x | +||
331 | +
- is_modules_ok <- check_modules_datanames(+ #' "var", |
||
165 | -103x | +||
332 | +
- modules = modules,+ #' "Select variable", |
||
166 | -103x | +||
333 | +
- datanames = ls(teal.code::get_env(data_validated()))+ #' choices = c("one", "two", "three", "four"), |
||
167 | +334 |
- )+ #' selected = "one" |
|
168 | -103x | +||
335 | +
- if (!isTRUE(is_modules_ok)) {+ #' ), |
||
169 | -8x | +||
336 | +
- tags$div(+ #' verbatimTextOutput("summary") |
||
170 | -8x | +||
337 | +
- is_modules_ok$html(+ #' ) |
||
171 | +338 |
- # Show modules prefix on message only in teal_data_module tab+ #' |
|
172 | -8x | +||
339 | +
- grepl(sprintf("data-teal_data_module-%s", id), session$ns(NULL), fixed = TRUE)+ #' server <- function(input, output) { |
||
173 | +340 |
- ),+ #' output$summary <- renderText({ |
|
174 | -8x | +||
341 | +
- class = "teal-output-warning"+ #' validate_n_levels(data[[input$var]], min_levels = 2, max_levels = 15, var_name = input$var) |
||
175 | +342 |
- )+ #' paste0( |
|
176 | +343 |
- }+ #' "Levels of selected treatment variable: ", |
|
177 | +344 |
- }+ #' paste(levels(data[[input$var]]), |
|
178 | +345 |
- })+ #' collapse = ", " |
|
179 | +346 |
-
+ #' ) |
|
180 | -121x | +||
347 | +
- data_validated+ #' ) |
||
181 | +348 |
- })+ #' }) |
|
182 | +349 |
- }+ #' } |
|
183 | +350 |
-
+ #' if (interactive()) { |
|
184 | +351 |
- #' Fallback on failure+ #' shinyApp(ui, server) |
|
185 | +352 |
- #'+ #' } |
|
186 | +353 |
- #' Function returns the previous reactive if the current reactive is invalid (throws error or returns NULL).+ validate_n_levels <- function(x, min_levels = 1, max_levels = 12, var_name) { |
|
187 | -+ | ||
354 | +! |
- #' Application: In `teal` we try to prevent the error from being thrown and instead we replace failing+ x_levels <- if (is.factor(x)) { |
|
188 | -+ | ||
355 | +! |
- #' transform module data output with data input from the previous module (or from previous `teal` reactive+ levels(x) |
|
189 | +356 |
- #' tree elements).+ } else { |
|
190 | -+ | ||
357 | +! |
- #'+ unique(x) |
|
191 | +358 |
- #' @param this (`reactive`) Current reactive.+ } |
|
192 | +359 |
- #' @param that (`reactive`) Previous reactive.+ |
|
193 | -+ | ||
360 | +! |
- #' @param label (`character`) Label for identifying problematic `teal_data_module` transform in logging.+ if (!is.null(min_levels) && !(is.null(max_levels))) { |
|
194 | -+ | ||
361 | +! |
- #' @return `reactive` `teal_data`+ validate(need( |
|
195 | -+ | ||
362 | +! |
- #' @keywords internal+ length(x_levels) >= min_levels && length(x_levels) <= max_levels, |
|
196 | -+ | ||
363 | +! |
- .fallback_on_failure <- function(this, that, label) {+ sprintf( |
|
197 | -54x | +||
364 | +! |
- assert_reactive(this)+ "%s variable needs minimum %s level(s) and maximum %s level(s).", |
|
198 | -54x | +||
365 | +! |
- assert_reactive(that)+ var_name, min_levels, max_levels |
|
199 | -54x | +||
366 | +
- checkmate::assert_string(label)+ ) |
||
200 | +367 |
-
+ )) |
|
201 | -54x | +||
368 | +! |
- reactive({+ } else if (!is.null(min_levels)) { |
|
202 | -58x | +||
369 | +! | +
+ validate(need(+ |
+ |
370 | +! |
- res <- try(this(), silent = TRUE)+ length(x_levels) >= min_levels, |
|
203 | -58x | +||
371 | +! |
- if (inherits(res, "teal_data")) {+ sprintf("%s variable needs minimum %s levels(s)", var_name, min_levels) |
|
204 | -49x | +||
372 | +
- logger::log_debug("{ label } evaluated successfully.")+ )) |
||
205 | -49x | +||
373 | +! |
- res+ } else if (!is.null(max_levels)) { |
|
206 | -+ | ||
374 | +! |
- } else {+ validate(need( |
|
207 | -9x | +||
375 | +! |
- logger::log_debug("{ label } failed, falling back to previous data.")+ length(x_levels) <= max_levels, |
|
208 | -9x | +||
376 | +! |
- that()+ sprintf("%s variable needs maximum %s level(s)", var_name, max_levels) |
|
209 | +377 |
- }+ )) |
|
210 | +378 |
- })+ } |
|
211 | +379 |
}@@ -38672,14 +39159,14 @@ teal coverage - 57.64% |
1 |
- #' Landing popup module+ #' Filter settings for `teal` applications |
||
3 |
- #' @description Creates a landing welcome popup for `teal` applications.+ #' Specify initial filter states and filtering settings for a `teal` app. |
||
5 |
- #' This module is used to display a popup dialog when the application starts.+ #' Produces a `teal_slices` object. |
||
6 |
- #' The dialog blocks access to the application and must be closed with a button before the application can be viewed.+ #' The `teal_slice` components will specify filter states that will be active when the app starts. |
||
7 |
- #'+ #' Attributes (created with the named arguments) will configure the way the app applies filters. |
||
8 |
- #' @param label (`character(1)`) Label of the module.+ #' See argument descriptions for details. |
||
9 |
- #' @param title (`character(1)`) Text to be displayed as popup title.+ #' |
||
10 |
- #' @param content (`character(1)`, `shiny.tag` or `shiny.tag.list`) with the content of the popup.+ #' @inheritParams teal.slice::teal_slices |
||
11 |
- #' Passed to `...` of `shiny::modalDialog`. See examples.+ #' |
||
12 |
- #' @param buttons (`shiny.tag` or `shiny.tag.list`) Typically a `modalButton` or `actionButton`. See examples.+ #' @param module_specific (`logical(1)`) optional, |
||
13 |
- #'+ #' - `FALSE` (default) when one filter panel applied to all modules. |
||
14 |
- #' @return A `teal_module` (extended with `teal_landing_module` class) to be used in `teal` applications.+ #' All filters will be shared by all modules. |
||
15 |
- #'+ #' - `TRUE` when filter panel module-specific. |
||
16 |
- #' @examples+ #' Modules can have different set of filters specified - see `mapping` argument. |
||
17 |
- #' app1 <- init(+ #' @param mapping `r lifecycle::badge("experimental")` |
||
18 |
- #' data = teal_data(iris = iris),+ #' _This is a new feature. Do kindly share your opinions on |
||
19 |
- #' modules = modules(+ #' [`teal`'s GitHub repository](https://github.com/insightsengineering/teal/)._ |
||
20 |
- #' example_module()+ #' |
||
21 |
- #' ),+ #' (named `list`) specifies which filters will be active in which modules on app start. |
||
22 |
- #' landing_popup = landing_popup_module(+ #' Elements should contain character vector of `teal_slice` `id`s (see [`teal.slice::teal_slice`]). |
||
23 |
- #' content = "A place for the welcome message or a disclaimer statement.",+ #' Names of the list should correspond to `teal_module` `label` set in [module()] function. |
||
24 |
- #' buttons = modalButton("Proceed")+ #' - `id`s listed under `"global_filters` will be active in all modules. |
||
25 |
- #' )+ #' - If missing, all filters will be applied to all modules. |
||
26 |
- #' )+ #' - If empty list, all filters will be available to all modules but will start inactive. |
||
27 |
- #' if (interactive()) {+ #' - If `module_specific` is `FALSE`, only `global_filters` will be active on start. |
||
28 |
- #' shinyApp(app1$ui, app1$server)+ #' @param app_id (`character(1)`) |
||
29 |
- #' }+ #' For internal use only, do not set manually. |
||
30 |
- #'+ #' Added by `init` so that a `teal_slices` can be matched to the app in which it was used. |
||
31 |
- #' app2 <- init(+ #' Used for verifying snapshots uploaded from file. See `snapshot`. |
||
32 |
- #' data = teal_data(iris = iris),+ #' |
||
33 |
- #' modules = modules(+ #' @param x (`list`) of lists to convert to `teal_slices` |
||
34 |
- #' example_module()+ #' |
||
35 |
- #' ),+ #' @return |
||
36 |
- #' landing_popup = landing_popup_module(+ #' A `teal_slices` object. |
||
37 |
- #' title = "Welcome",+ #' |
||
38 |
- #' content = tags$b(+ #' @seealso [`teal.slice::teal_slices`], [`teal.slice::teal_slice`], [slices_store()] |
||
39 |
- #' "A place for the welcome message or a disclaimer statement.",+ #' |
||
40 |
- #' style = "color: red;"+ #' @examples |
||
41 |
- #' ),+ #' filter <- teal_slices( |
||
42 |
- #' buttons = tagList(+ #' teal_slice(dataname = "iris", varname = "Species", id = "species"), |
||
43 |
- #' modalButton("Proceed"),+ #' teal_slice(dataname = "iris", varname = "Sepal.Length", id = "sepal_length"), |
||
44 |
- #' actionButton("read", "Read more",+ #' teal_slice( |
||
45 |
- #' onclick = "window.open('http://google.com', '_blank')"+ #' dataname = "iris", id = "long_petals", title = "Long petals", expr = "Petal.Length > 5" |
||
46 |
- #' ),+ #' ), |
||
47 |
- #' actionButton("close", "Reject", onclick = "window.close()")+ #' teal_slice(dataname = "mtcars", varname = "mpg", id = "mtcars_mpg"), |
||
48 |
- #' )+ #' mapping = list( |
||
49 |
- #' )+ #' module1 = c("species", "sepal_length"), |
||
50 |
- #' )+ #' module2 = c("mtcars_mpg"), |
||
51 |
- #'+ #' global_filters = "long_petals" |
||
52 |
- #' if (interactive()) {+ #' ) |
||
53 |
- #' shinyApp(app2$ui, app2$server)+ #' ) |
||
54 |
- #' }+ #' |
||
55 |
- #'+ #' app <- init( |
||
56 |
- #' @export+ #' data = teal_data(iris = iris, mtcars = mtcars), |
||
57 |
- landing_popup_module <- function(label = "Landing Popup",+ #' modules = list( |
||
58 |
- title = NULL,+ #' module("module1"), |
||
59 |
- content = NULL,+ #' module("module2") |
||
60 |
- buttons = modalButton("Accept")) {+ #' ), |
||
61 | -! | +
- checkmate::assert_string(label)+ #' filter = filter |
|
62 | -! | +
- checkmate::assert_string(title, null.ok = TRUE)+ #' ) |
|
63 | -! | +
- checkmate::assert_multi_class(+ #' |
|
64 | -! | +
- content,+ #' if (interactive()) { |
|
65 | -! | +
- classes = c("character", "shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE+ #' shinyApp(app$ui, app$server) |
|
66 |
- )+ #' } |
||
67 | -! | +
- checkmate::assert_multi_class(buttons, classes = c("shiny.tag", "shiny.tag.list"))+ #' |
|
68 |
-
+ #' @export+ |
+ ||
69 | ++ |
+ teal_slices <- function(...,+ |
+ |
70 | ++ |
+ exclude_varnames = NULL,+ |
+ |
71 | ++ |
+ include_varnames = NULL,+ |
+ |
72 | ++ |
+ count_type = NULL,+ |
+ |
73 | ++ |
+ allow_add = TRUE,+ |
+ |
74 | ++ |
+ module_specific = FALSE,+ |
+ |
75 | ++ |
+ mapping,+ |
+ |
76 | ++ |
+ app_id = NULL) {+ |
+ |
77 | +147x | +
+ shiny::isolate({+ |
+ |
78 | +147x | +
+ checkmate::assert_flag(allow_add)+ |
+ |
79 | +147x | +
+ checkmate::assert_flag(module_specific)+ |
+ |
80 | +50x | +
+ if (!missing(mapping)) checkmate::assert_list(mapping, types = c("character", "NULL"), names = "named")+ |
+ |
81 | +144x | +
+ checkmate::assert_string(app_id, null.ok = TRUE)+ |
+ |
82 | ++ | + + | +|
83 | +144x | +
+ slices <- list(...)+ |
+ |
84 | +144x | +
+ all_slice_id <- vapply(slices, `[[`, character(1L), "id")+ |
+ |
85 | ++ | + + | +|
86 | +144x | +
+ if (missing(mapping)) {+ |
+ |
87 | +97x | +
+ mapping <- if (length(all_slice_id)) {+ |
+ |
88 | +26x | +
+ list(global_filters = all_slice_id)+ |
+ |
89 | ++ |
+ } else {+ |
+ |
90 | +71x | +
+ list()+ |
+ |
91 | ++ |
+ }+ |
+ |
92 | ++ |
+ }+ |
+ |
93 | ++ | + + | +|
94 | +144x | +
+ if (!module_specific) {+ |
+ |
95 | +127x | +
+ mapping[setdiff(names(mapping), "global_filters")] <- NULL+ |
+ |
96 | ++ |
+ }+ |
+ |
97 | ++ | + + | +|
98 | +144x | +
+ failed_slice_id <- setdiff(unlist(mapping), all_slice_id)+ |
+ |
99 | +144x | +
+ if (length(failed_slice_id)) {+ |
+ |
100 | +1x | +
+ stop(sprintf(+ |
+ |
101 | +1x | +
+ "Filters in mapping don't match any available filter.\n %s not in %s",+ |
+ |
102 | +1x | +
+ toString(failed_slice_id),+ |
+ |
103 | +1x | +
+ toString(all_slice_id)+ |
+ |
104 | ++ |
+ ))+ |
+ |
105 | ++ |
+ }+ |
+ |
106 | ++ | + + | +|
107 | +143x | +
+ tss <- teal.slice::teal_slices(+ |
+ |
108 | ++ |
+ ...,+ |
+ |
109 | +143x | +
+ exclude_varnames = exclude_varnames,+ |
+ |
110 | +143x | +
+ include_varnames = include_varnames,+ |
+ |
111 | +143x | +
+ count_type = count_type, |
|
69 | -! | +||
112 | +143x |
- message("Initializing landing_popup_module")+ allow_add = allow_add |
|
70 | +113 | - - | -|
71 | -! | -
- module <- module(+ ) |
|
72 | -! | +||
114 | +143x |
- label = label,+ attr(tss, "mapping") <- mapping |
|
73 | -! | +||
115 | +143x |
- server = function(id) {+ attr(tss, "module_specific") <- module_specific |
|
74 | -! | +||
116 | +143x |
- moduleServer(id, function(input, output, session) {+ attr(tss, "app_id") <- app_id |
|
75 | -! | +||
117 | +143x |
- showModal(+ class(tss) <- c("modules_teal_slices", class(tss)) |
|
76 | -! | +||
118 | +143x |
- modalDialog(+ tss |
|
77 | -! | +||
119 | +
- id = "landingpopup",+ }) |
||
78 | -! | +||
120 | +
- title = title,+ } |
||
79 | -! | +||
121 | +
- content,+ |
||
80 | -! | +||
122 | +
- footer = buttons+ |
||
81 | +123 |
- )+ #' @rdname teal_slices |
|
82 | +124 |
- )+ #' @export |
|
83 | +125 |
- })+ #' @keywords internal |
|
84 | +126 |
- }+ #' |
|
85 | +127 |
- )+ as.teal_slices <- function(x) { # nolint: object_name. |
|
86 | -! | +||
128 | +12x |
- class(module) <- c("teal_module_landing", class(module))+ checkmate::assert_list(x) |
|
87 | -! | +||
129 | +12x |
- module+ lapply(x, checkmate::assert_list, names = "named", .var.name = "list element") |
|
88 | +130 |
- }+ |
1 | -+ | ||
131 | +12x |
- #' Data module for `teal` applications+ attrs <- attributes(unclass(x)) |
|
2 | -+ | ||
132 | +12x |
- #'+ ans <- lapply(x, function(x) if (is.teal_slice(x)) x else as.teal_slice(x)) |
|
3 | -+ | ||
133 | +12x |
- #' @description+ do.call(teal_slices, c(ans, attrs)) |
|
4 | +134 |
- #' `r lifecycle::badge("experimental")`+ } |
|
5 | +135 |
- #'+ |
|
6 | +136 |
- #' Create a `teal_data_module` object and evaluate code on it with history tracking.+ |
|
7 | +137 |
- #'+ #' @rdname teal_slices |
|
8 | +138 |
- #' @details+ #' @export |
|
9 | +139 |
- #' `teal_data_module` creates a `shiny` module to interactively supply or modify data in a `teal` application.+ #' @keywords internal |
|
10 | +140 |
- #' The module allows for running any code (creation _and_ some modification) after the app starts or reloads.+ #' |
|
11 | +141 |
- #' The body of the server function will be run in the app rather than in the global environment.+ c.teal_slices <- function(...) { |
|
12 | -+ | ||
142 | +6x |
- #' This means it will be run every time the app starts, so use sparingly.+ x <- list(...) |
|
13 | -+ | ||
143 | +6x |
- #'+ checkmate::assert_true(all(vapply(x, is.teal_slices, logical(1L))), .var.name = "all arguments are teal_slices") |
|
14 | +144 |
- #' Pass this module instead of a `teal_data` object in a call to [init()].+ |
|
15 | -+ | ||
145 | +6x |
- #' Note that the server function must always return a `teal_data` object wrapped in a reactive expression.+ all_attributes <- lapply(x, attributes) |
|
16 | -+ | ||
146 | +6x |
- #'+ all_attributes <- coalesce_r(all_attributes) |
|
17 | -+ | ||
147 | +6x |
- #' See vignette `vignette("data-as-shiny-module", package = "teal")` for more details.+ all_attributes <- all_attributes[names(all_attributes) != "class"] |
|
18 | +148 |
- #'+ |
|
19 | -+ | ||
149 | +6x |
- #' @param ui (`function(id)`)+ do.call( |
|
20 | -+ | ||
150 | +6x |
- #' `shiny` module UI function; must only take `id` argument+ teal_slices, |
|
21 | -+ | ||
151 | +6x |
- #' @param server (`function(id)`)+ c( |
|
22 | -+ | ||
152 | +6x |
- #' `shiny` module server function; must only take `id` argument;+ unique(unlist(x, recursive = FALSE)), |
|
23 | -+ | ||
153 | +6x |
- #' must return reactive expression containing `teal_data` object+ all_attributes |
|
24 | +154 |
- #' @param label (`character(1)`) Label of the module.+ ) |
|
25 | +155 |
- #' @param once (`logical(1)`)+ ) |
|
26 | +156 |
- #' If `TRUE`, the data module will be shown only once and will disappear after successful data loading.+ } |
|
27 | +157 |
- #' App user will no longer be able to interact with this module anymore.+ |
|
28 | +158 |
- #' If `FALSE`, the data module can be reused multiple times.+ |
|
29 | +159 |
- #' App user will be able to interact and change the data output from the module multiple times.+ #' Deep copy `teal_slices` |
|
30 | +160 |
#' |
|
31 | +161 |
- #' @return+ #' it's important to create a new copy of `teal_slices` when |
|
32 | +162 |
- #' `teal_data_module` returns a list of class `teal_data_module` containing two elements, `ui` and+ #' starting a new `shiny` session. Otherwise, object will be shared |
|
33 | +163 |
- #' `server` provided via arguments.+ #' by multiple users as it is created in global environment before |
|
34 | +164 |
- #'+ #' `shiny` session starts. |
|
35 | +165 |
- #' @examples+ #' @param filter (`teal_slices`) |
|
36 | +166 |
- #' tdm <- teal_data_module(+ #' @return `teal_slices` |
|
37 | +167 |
- #' ui = function(id) {+ #' @keywords internal |
|
38 | +168 |
- #' ns <- NS(id)+ deep_copy_filter <- function(filter) { |
|
39 | -+ | ||
169 | +1x |
- #' actionButton(ns("submit"), label = "Load data")+ checkmate::assert_class(filter, "teal_slices") |
|
40 | -+ | ||
170 | +1x |
- #' },+ shiny::isolate({ |
|
41 | -+ | ||
171 | +1x |
- #' server = function(id) {+ filter_copy <- lapply(filter, function(slice) { |
|
42 | -+ | ||
172 | +2x |
- #' moduleServer(id, function(input, output, session) {+ teal.slice::as.teal_slice(as.list(slice)) |
|
43 | +173 |
- #' eventReactive(input$submit, {+ }) |
|
44 | -+ | ||
174 | +1x |
- #' data <- within(+ attributes(filter_copy) <- attributes(filter)+ |
+ |
175 | +1x | +
+ filter_copy |
|
45 | +176 |
- #' teal_data(),+ }) |
|
46 | +177 |
- #' {+ } |
47 | +1 |
- #' dataset1 <- iris+ #' Include `CSS` files from `/inst/css/` package directory to application header |
|
48 | +2 |
- #' dataset2 <- mtcars+ #' |
|
49 | +3 |
- #' }+ #' `system.file` should not be used to access files in other packages, it does |
|
50 | +4 |
- #' )+ #' not work with `devtools`. Therefore, we redefine this method in each package |
|
51 | +5 |
- #' datanames(data) <- c("dataset1", "dataset2")+ #' as needed. Thus, we do not export this method. |
|
52 | +6 |
#' |
|
53 | +7 |
- #' data+ #' @param pattern (`character`) pattern of files to be included |
|
54 | +8 |
- #' })+ #' |
|
55 | +9 |
- #' })+ #' @return HTML code that includes `CSS` files. |
|
56 | +10 |
- #' }+ #' @keywords internal |
|
57 | +11 |
- #' )+ include_css_files <- function(pattern = "*") { |
|
58 | -+ | ||
12 | +! |
- #'+ css_files <- list.files( |
|
59 | -+ | ||
13 | +! |
- #' @name teal_data_module+ system.file("css", package = "teal", mustWork = TRUE), |
|
60 | -+ | ||
14 | +! |
- #' @seealso [`teal.data::teal_data-class`], [teal.code::qenv()]+ pattern = pattern, full.names = TRUE |
|
61 | +15 |
- #'+ ) |
|
62 | +16 |
- #' @export+ |
|
63 | -+ | ||
17 | +! |
- teal_data_module <- function(ui, server, label = "data module", once = TRUE) {+ singleton( |
|
64 | -32x | +||
18 | +! |
- checkmate::assert_function(ui, args = "id", nargs = 1)+ tags$head(lapply(css_files, includeCSS)) |
|
65 | -31x | +||
19 | +
- checkmate::assert_function(server, args = "id", nargs = 1)+ ) |
||
66 | -29x | +||
20 | +
- checkmate::assert_string(label)+ } |
||
67 | -29x | +||
21 | +
- checkmate::assert_flag(once)+ |
||
68 | -29x | +||
22 | +
- structure(+ #' Include `JS` files from `/inst/js/` package directory to application header |
||
69 | -29x | +||
23 | +
- list(+ #' |
||
70 | -29x | +||
24 | +
- ui = ui,+ #' `system.file` should not be used to access files in other packages, it does |
||
71 | -29x | +||
25 | +
- server = function(id) {+ #' not work with `devtools`. Therefore, we redefine this method in each package |
||
72 | -22x | +||
26 | +
- data_out <- server(id)+ #' as needed. Thus, we do not export this method |
||
73 | -21x | +||
27 | +
- decorate_err_msg(+ #' |
||
74 | -21x | +||
28 | +
- assert_reactive(data_out),+ #' @param pattern (`character`) pattern of files to be included, passed to `system.file` |
||
75 | -21x | +||
29 | +
- pre = sprintf("From: 'teal_data_module()':\nA 'teal_data_module' with \"%s\" label:", label),+ #' @param except (`character`) vector of basename filenames to be excluded |
||
76 | -21x | +||
30 | +
- post = "Please make sure that this module returns a 'reactive` object containing 'teal_data' class of object." # nolint: line_length_linter.+ #' |
||
77 | +31 |
- )+ #' @return HTML code that includes `JS` files. |
|
78 | +32 |
- }+ #' @keywords internal |
|
79 | +33 |
- ),+ include_js_files <- function(pattern = NULL, except = NULL) { |
|
80 | -29x | +||
34 | +! |
- label = label,+ checkmate::assert_character(except, min.len = 1, any.missing = FALSE, null.ok = TRUE) |
|
81 | -29x | +||
35 | +! |
- class = "teal_data_module",+ js_files <- list.files(system.file("js", package = "teal", mustWork = TRUE), pattern = pattern, full.names = TRUE) |
|
82 | -29x | +||
36 | +! |
- once = once+ js_files <- js_files[!(basename(js_files) %in% except)] # no-op if except is NULL |
|
83 | +37 |
- )+ + |
+ |
38 | +! | +
+ singleton(lapply(js_files, includeScript)) |
|
84 | +39 |
} |
|
85 | +40 | ||
86 | +41 |
- #' Data module for `teal` transformers.+ #' Run `JS` file from `/inst/js/` package directory |
|
87 | +42 |
#' |
|
88 | +43 |
- #' @description+ #' This is triggered from the server to execute on the client |
|
89 | +44 |
- #' `r lifecycle::badge("experimental")`+ #' rather than triggered directly on the client. |
|
90 | +45 |
- #'+ #' Unlike `include_js_files` which includes `JavaScript` functions, |
|
91 | +46 |
- #' Create a `teal_data_module` object for custom transformation of data for pre-processing+ #' the `run_js` actually executes `JavaScript` functions. |
|
92 | +47 |
- #' before passing the data into the module.+ #' |
|
93 | +48 |
- #'+ #' `system.file` should not be used to access files in other packages, it does |
|
94 | +49 |
- #' @details+ #' not work with `devtools`. Therefore, we redefine this method in each package |
|
95 | +50 |
- #' `teal_transform_module` creates a [`teal_data_module`] object to transform data in a `teal`+ #' as needed. Thus, we do not export this method. |
|
96 | +51 |
- #' application. This transformation happens after the data has passed through the filtering activity+ #' |
|
97 | +52 |
- #' in teal. The transformed data is then sent to the server of the [teal_module()].+ #' @param files (`character`) vector of filenames. |
|
98 | +53 |
#' |
|
99 | +54 |
- #' See vignette `vignette("data-transform-as-shiny-module", package = "teal")` for more details.+ #' @return `NULL`, invisibly. |
|
100 | +55 |
- #'+ #' @keywords internal |
|
101 | +56 |
- #'+ run_js_files <- function(files) { |
|
102 | -+ | ||
57 | +71x |
- #' @inheritParams teal_data_module+ checkmate::assert_character(files, min.len = 1, any.missing = FALSE) |
|
103 | -+ | ||
58 | +71x |
- #' @param server (`function(id, data)`)+ lapply(files, function(file) { |
|
104 | -+ | ||
59 | +71x |
- #' `shiny` module server function; that takes `id` and `data` argument,+ shinyjs::runjs(paste0(readLines(system.file("js", file, package = "teal", mustWork = TRUE)), collapse = "\n")) |
|
105 | +60 |
- #' where the `id` is the module id and `data` is the reactive `teal_data` input.+ })+ |
+ |
61 | +71x | +
+ invisible(NULL) |
|
106 | +62 |
- #' The server function must return reactive expression containing `teal_data` object.+ } |
|
107 | +63 |
- #' @param datanames (`character`)+ |
|
108 | +64 |
- #' Names of the datasets that are relevant for the module. The+ #' Code to include `teal` `CSS` and `JavaScript` files |
|
109 | +65 |
- #' filter panel will only display filters for specified `datanames`. The keyword `"all"` will show+ #' |
|
110 | +66 |
- #' filters of all datasets. `datanames` will be automatically appended to the [modules()] `datanames`.+ #' This is useful when you want to use the same `JavaScript` and `CSS` files that are |
|
111 | +67 |
- #' @examples+ #' used with the `teal` application. |
|
112 | +68 |
- #' my_transformers <- list(+ #' This is also useful for running standalone modules in `teal` with the correct |
|
113 | +69 |
- #' teal_transform_module(+ #' styles. |
|
114 | +70 |
- #' label = "Custom transform for iris",+ #' Also initializes `shinyjs` so you can use it. |
|
115 | +71 |
- #' datanames = "iris",+ #' |
|
116 | +72 |
- #' ui = function(id) {+ #' Simply add `include_teal_css_js()` as one of the UI elements. |
|
117 | +73 |
- #' ns <- NS(id)+ #' @return A `shiny.tag.list`. |
|
118 | +74 |
- #' tags$div(+ #' @keywords internal |
|
119 | +75 |
- #' numericInput(ns("n_rows"), "Subset n rows", value = 6, min = 1, max = 150, step = 1)+ include_teal_css_js <- function() {+ |
+ |
76 | +! | +
+ tagList(+ |
+ |
77 | +! | +
+ shinyjs::useShinyjs(),+ |
+ |
78 | +! | +
+ include_css_files(), |
|
120 | +79 |
- #' )+ # init.js is executed from the server+ |
+ |
80 | +! | +
+ include_js_files(except = "init.js"),+ |
+ |
81 | +! | +
+ shinyjs::hidden(icon("fas fa-gear")), # add hidden icon to load font-awesome css for icons |
|
121 | +82 |
- #' },+ ) |
|
122 | +83 |
- #' server = function(id, data) {+ } |
123 | +1 |
- #' moduleServer(id, function(input, output, session) {+ #' `teal_data` utils |
||
124 | +2 |
- #' reactive({+ #' |
||
125 | +3 |
- #' within(data(),+ #' In `teal` we need to recreate the `teal_data` object due to two operations: |
||
126 | +4 |
- #' {+ #' - we need to append filter-data code and objects which have been evaluated in `FilteredData` and |
||
127 | +5 |
- #' iris <- head(iris, num_rows)+ #' we want to avoid double-evaluation. |
||
128 | +6 |
- #' },+ #' - we need to subset `teal_data` to `datanames` used by the module, to shorten obtainable R-code |
||
129 | +7 |
- #' num_rows = input$n_rows+ #' |
||
130 | +8 |
- #' )+ #' Due to above recreation of `teal_data` object can't be done simply by using public |
||
131 | +9 |
- #' })+ #' `teal.code` and `teal.data` methods. |
||
132 | +10 |
- #' })+ #' |
||
133 | +11 |
- #' }+ #' @param data (`teal_data`) |
||
134 | +12 |
- #' )+ #' @param code (`character`) code to append to `data@code` |
||
135 | +13 |
- #' )+ #' @param objects (`list`) objects to append to `data@env` |
||
136 | +14 |
- #'+ #' @param datanames (`character`) names of the datasets |
||
137 | +15 |
- #' @name teal_transform_module+ #' @return modified `teal_data` |
||
138 | +16 |
- #'+ #' @keywords internal |
||
139 | +17 |
- #' @export+ #' @name teal_data_utilities |
||
140 | +18 |
- teal_transform_module <- function(ui = function(id) NULL,+ NULL |
||
141 | +19 |
- server = function(id, data) data,+ |
||
142 | +20 |
- label = "transform module",+ #' @rdname teal_data_utilities |
||
143 | +21 |
- datanames = "all") {+ .append_evaluated_code <- function(data, code) { |
||
144 | -16x | +22 | +73x |
- checkmate::assert_function(ui, args = "id", nargs = 1)+ checkmate::assert_class(data, "teal_data") |
145 | -16x | +23 | +73x |
- checkmate::assert_function(server, args = c("id", "data"), nargs = 2)+ data@code <- c(data@code, code) |
146 | -16x | +24 | +73x |
- checkmate::assert_string(label)+ data@id <- c(data@id, max(data@id) + 1L + seq_along(code)) |
147 | -16x | +25 | +73x |
- structure(+ data@messages <- c(data@messages, rep("", length(code))) |
148 | -16x | +26 | +73x |
- list(+ data@warnings <- c(data@warnings, rep("", length(code))) |
149 | -16x | +27 | +73x |
- ui = ui,+ methods::validObject(data) |
150 | -16x | +28 | +73x |
- server = function(id, data) {+ data+ |
+
29 | ++ |
+ }+ |
+ ||
30 | ++ | + + | +||
31 | ++ |
+ #' @rdname teal_data_utilities+ |
+ ||
32 | ++ |
+ .append_modified_data <- function(data, objects) { |
||
151 | -20x | +33 | +73x |
- data_out <- server(id, data)+ checkmate::assert_class(data, "teal_data") |
152 | -20x | +34 | +73x |
- decorate_err_msg(+ checkmate::assert_class(objects, "list") |
153 | -20x | +35 | +73x |
- assert_reactive(data_out),+ new_env <- list2env(objects, parent = .GlobalEnv) |
154 | -20x | +36 | +73x | +
+ rlang::env_coalesce(new_env, teal.code::get_env(data))+ |
+
37 | +73x | +
+ data@env <- new_env+ |
+ ||
38 | +73x |
- pre = sprintf("From: 'teal_transform_module()':\nA 'teal_transform_module' with \"%s\" label:", label),+ data |
||
155 | -20x | +|||
39 | +
- post = "Please make sure that this module returns a 'reactive` object containing 'teal_data' class of object." # nolint: line_length_linter.+ } |
|||
156 | +40 |
- )+ |
||
157 | +41 |
- }+ #' @rdname teal_data_utilities |
||
158 | +42 |
- ),+ .subset_teal_data <- function(data, datanames) { |
||
159 | -16x | +43 | +72x |
- label = label,+ checkmate::assert_class(data, "teal_data") |
160 | -16x | +44 | +72x |
- datanames = datanames,+ checkmate::assert_class(datanames, "character") |
161 | -16x | +45 | +72x |
- class = c("teal_transform_module", "teal_data_module")+ datanames_corrected <- intersect(datanames, ls(teal.code::get_env(data))) |
162 | -+ | |||
46 | +72x |
- )+ datanames_corrected_with_raw <- c(datanames_corrected, ".raw_data") |
||
163 | -+ | |||
47 | +72x |
- }+ if (!length(datanames_corrected)) {+ |
+ ||
48 | +1x | +
+ return(teal_data()) |
||
164 | +49 |
-
+ } |
||
165 | +50 | |||
166 | -+ | |||
51 | +71x |
- #' Extract all `transformers` from `modules`.+ new_data <- do.call( |
||
167 | -+ | |||
52 | +71x |
- #'+ teal.data::teal_data, |
||
168 | -+ | |||
53 | +71x |
- #' @param modules `teal_modules` or `teal_module`+ args = c( |
||
169 | -+ | |||
54 | +71x |
- #' @return A list of `teal_transform_module` nested in the same way as input `modules`.+ mget(x = datanames_corrected_with_raw, envir = teal.code::get_env(data)), |
||
170 | -+ | |||
55 | +71x |
- #' @keywords internal+ list(+ |
+ ||
56 | +71x | +
+ code = teal.data::get_code(data, datanames = datanames_corrected_with_raw),+ |
+ ||
57 | +71x | +
+ join_keys = teal.data::join_keys(data)[datanames_corrected] |
||
171 | +58 |
- extract_transformers <- function(modules) {+ ) |
||
172 | -18x | +|||
59 | +
- if (inherits(modules, "teal_module")) {+ ) |
|||
173 | -10x | +|||
60 | +
- modules$transformers+ ) |
|||
174 | -8x | +61 | +71x |
- } else if (inherits(modules, "teal_modules")) {+ new_data@verified <- data@verified |
175 | -8x | +62 | +71x |
- lapply(modules$children, extract_transformers)+ teal.data::datanames(new_data) <- datanames_corrected |
176 | -+ | |||
63 | +71x |
- }+ new_data |
||
177 | +64 |
}@@ -40539,846 +41445,671 @@ teal coverage - 57.64% |
1 |
- #' Module to transform `reactive` `teal_data`+ setOldClass("teal_data_module") |
|||
2 |
- #'+ |
|||
3 |
- #' Module calls multiple [`module_teal_data`] in sequence so that `reactive teal_data` output+ #' Evaluate code on `teal_data_module` |
|||
4 |
- #' from one module is handed over to the following module's input.+ #' |
|||
5 |
- #'+ #' @details |
|||
6 |
- #' @inheritParams module_teal_data+ #' `eval_code` evaluates given code in the environment of the `teal_data` object created by the `teal_data_module`. |
|||
7 |
- #' @inheritParams teal_modules+ #' The code is added to the `@code` slot of the `teal_data`. |
|||
8 |
- #' @return `reactive` `teal_data`+ #' |
|||
9 |
- #'+ #' @param object (`teal_data_module`) |
|||
10 |
- #'+ #' @inheritParams teal.code::eval_code |
|||
11 |
- #' @name module_transform_data+ #' |
|||
12 |
- #' @keywords internal+ #' @return |
|||
13 |
- NULL+ #' `eval_code` returns a `teal_data_module` object with a delayed evaluation of `code` when the module is run. |
|||
14 |
-
+ #' |
|||
15 |
- #' @rdname module_transform_data+ #' @examples |
|||
16 |
- ui_transform_data <- function(id, transforms, class = "well") {- |
- |||
17 | -! | -
- checkmate::assert_string(id)- |
- ||
18 | -! | -
- checkmate::assert_list(transforms, "teal_transform_module", null.ok = TRUE)- |
- ||
19 | -! | -
- ns <- NS(id)- |
- ||
20 | -! | -
- labels <- lapply(transforms, function(x) attr(x, "label"))- |
- ||
21 | -! | -
- ids <- get_unique_labels(labels)- |
- ||
22 | -! | -
- names(transforms) <- ids- |
- ||
23 | -- | - - | -||
24 | -! | -
- lapply(- |
- ||
25 | -! | -
- names(transforms),- |
- ||
26 | -! | -
- function(name) {- |
- ||
27 | -! | -
- data_mod <- transforms[[name]]- |
- ||
28 | -! | -
- wrapper_id <- ns(sprintf("wrapper_%s", name))- |
- ||
29 | -! | -
- div( # todo: accordion?+ #' eval_code(tdm, "dataset1 <- subset(dataset1, Species == 'virginica')") |
||
30 | +17 |
- # class .teal_validated changes the color of the boarder on error in ui_validate_reactive_teal_data+ #' |
||
31 | +18 |
- # For details see tealValidate.js file.- |
- ||
32 | -! | -
- class = c(class, "teal_validated"),- |
- ||
33 | -! | -
- title = attr(data_mod, "label"),- |
- ||
34 | -! | -
- tags$span(- |
- ||
35 | -! | -
- class = "text-primary mb-4",- |
- ||
36 | -! | -
- icon("fas fa-square-pen"),- |
- ||
37 | -! | -
- attr(data_mod, "label")+ #' @include teal_data_module.R |
||
38 | +19 |
- ),- |
- ||
39 | -! | -
- tags$i(- |
- ||
40 | -! | -
- class = "remove pull-right fa fa-angle-down",- |
- ||
41 | -! | -
- style = "cursor: pointer;",- |
- ||
42 | -! | -
- title = "fold/expand transform panel",- |
- ||
43 | -! | -
- onclick = sprintf("togglePanelItems(this, '%s', 'fa-angle-right', 'fa-angle-down');", wrapper_id)+ #' @name eval_code |
||
44 | +20 |
- ),- |
- ||
45 | -! | -
- div(- |
- ||
46 | -! | -
- id = wrapper_id,- |
- ||
47 | -! | -
- ui_teal_data(id = ns(name), data_module = transforms[[name]])+ #' @rdname teal_data_module |
||
48 | +21 |
- )+ #' @aliases eval_code,teal_data_module,character-method |
||
49 | +22 |
- )+ #' @aliases eval_code,teal_data_module,language-method |
||
50 | +23 |
- }+ #' @aliases eval_code,teal_data_module,expression-method |
||
51 | +24 |
- )+ #' |
||
52 | +25 |
- }+ #' @importFrom methods setMethod |
||
53 | +26 |
-
+ #' @importMethodsFrom teal.code eval_code |
||
54 | +27 |
- #' @rdname module_transform_data+ #' |
||
55 | +28 |
- srv_transform_data <- function(id, data, transforms, modules) {+ setMethod("eval_code", signature = c("teal_data_module", "character"), function(object, code) { |
||
56 | -95x | +29 | +9x |
- checkmate::assert_string(id)+ teal_data_module( |
57 | -95x | +30 | +9x |
- assert_reactive(data)+ ui = function(id) { |
58 | -95x | +31 | +1x |
- checkmate::assert_list(transforms, "teal_transform_module", null.ok = TRUE)+ ns <- NS(id) |
59 | -95x | +32 | +1x |
- checkmate::assert_class(modules, "teal_module")+ object$ui(ns("mutate_inner")) |
60 | +33 |
-
+ }, |
||
61 | -95x | +34 | +9x |
- if (length(transforms) == 0L) {+ server = function(id) { |
62 | -80x | +35 | +7x |
- return(data)+ moduleServer(id, function(input, output, session) { |
63 | -+ | |||
36 | +7x |
- }+ teal_data_rv <- object$server("mutate_inner") |
||
64 | -+ | |||
37 | +6x |
-
+ td <- eventReactive(teal_data_rv(), |
||
65 | -15x | +|||
38 | +
- labels <- lapply(transforms, function(x) attr(x, "label"))+ { |
|||
66 | -15x | +39 | +6x |
- ids <- get_unique_labels(labels)+ if (inherits(teal_data_rv(), c("teal_data", "qenv.error"))) { |
67 | -15x | +40 | +4x |
- names(transforms) <- ids+ eval_code(teal_data_rv(), code) |
68 | +41 |
-
+ } else { |
||
69 | -15x | +42 | +2x |
- moduleServer(id, function(input, output, session) {+ teal_data_rv() |
70 | -15x | +|||
43 | +
- logger::log_debug("srv_teal_data_modules initializing.")+ } |
|||
71 | -15x | +|||
44 | +
- Reduce(+ }, |
|||
72 | -15x | +45 | +6x |
- function(previous_result, name) {+ ignoreNULL = FALSE |
73 | -20x | +|||
46 | +
- srv_teal_data(+ ) |
|||
74 | -20x | +47 | +6x |
- id = name,+ td |
75 | -20x | +|||
48 | +
- data = previous_result,+ }) |
|||
76 | -20x | +|||
49 | +
- data_module = transforms[[name]],+ } |
|||
77 | -20x | +|||
50 | +
- modules = modules+ ) |
|||
78 | +51 |
- )+ }) |
||
79 | +52 |
- },+ |
||
80 | -15x | +|||
53 | +
- x = names(transforms),+ setMethod("eval_code", signature = c("teal_data_module", "language"), function(object, code) { |
|||
81 | -15x | +54 | +1x |
- init = data+ eval_code(object, code = paste(lang2calls(code), collapse = "\n")) |
82 | +55 |
- )+ }) |
||
83 | +56 |
- })+ |
||
84 | +57 |
- }+ setMethod("eval_code", signature = c("teal_data_module", "expression"), function(object, code) {+ |
+ ||
58 | +2x | +
+ eval_code(object, code = paste(lang2calls(code), collapse = "\n"))+ |
+ ||
59 | ++ |
+ }) |
1 |
- .onLoad <- function(libname, pkgname) {+ #' Create a `teal` module for previewing a report |
||
2 |
- # adapted from https://github.com/r-lib/devtools/blob/master/R/zzz.R+ #' |
||
3 |
-
+ #' @description `r lifecycle::badge("experimental")` |
||
4 | -! | +
- teal_default_options <- list(+ #' |
|
5 | -! | +
- teal.show_js_log = FALSE,+ #' This function wraps [teal.reporter::reporter_previewer_ui()] and |
|
6 | -! | +
- teal.lockfile.mode = "auto",+ #' [teal.reporter::reporter_previewer_srv()] into a `teal_module` to be |
|
7 | -! | +
- shiny.sanitize.errors = FALSE+ #' used in `teal` applications. |
|
8 |
- )+ #' |
||
9 |
-
+ #' If you are creating a `teal` application using [init()] then this |
||
10 | -! | +
- op <- options()+ #' module will be added to your application automatically if any of your `teal_modules` |
|
11 | -! | +
- toset <- !(names(teal_default_options) %in% names(op))+ #' support report generation. |
|
12 | -! | +
- if (any(toset)) options(teal_default_options[toset])+ #' |
|
13 |
-
+ #' @inheritParams teal_modules |
||
14 |
- # Set up the teal logger instance+ #' @param server_args (named `list`) |
||
15 | -! | +
- teal.logger::register_logger("teal")+ #' Arguments passed to [teal.reporter::reporter_previewer_srv()]. |
|
16 | -! | +
- teal.logger::register_handlers("teal")+ #' |
|
17 |
-
+ #' @return |
||
18 | -! | +
- invisible()+ #' `teal_module` (extended with `teal_module_previewer` class) containing the `teal.reporter` previewer functionality. |
|
19 |
- }+ #' |
||
20 |
-
+ #' @export |
||
21 |
- .onAttach <- function(libname, pkgname) {+ #' |
||
22 | -2x | +
- packageStartupMessage(+ reporter_previewer_module <- function(label = "Report previewer", server_args = list()) { |
|
23 | -2x | +7x |
- "\nYou are using teal version ",+ checkmate::assert_string(label) |
24 | -+ | 5x |
- # `system.file` uses the `shim` of `system.file` by `teal`+ checkmate::assert_list(server_args, names = "named") |
25 | -+ | 5x |
- # we avoid `desc` dependency here to get the version+ checkmate::assert_true(all(names(server_args) %in% names(formals(teal.reporter::reporter_previewer_srv)))) |
26 | -2x | +
- read.dcf(system.file("DESCRIPTION", package = "teal"))[, "Version"]+ |
|
27 | -+ | 3x |
- )+ message("Initializing reporter_previewer_module") |
28 |
- }+ |
||
29 | -+ | 3x |
-
+ srv <- function(id, reporter, ...) { |
30 | -+ | ! |
- # This one is here because setdiff_teal_slice should not be exported from teal.slice.+ teal.reporter::reporter_previewer_srv(id, reporter, ...) |
31 |
- setdiff_teal_slices <- getFromNamespace("setdiff_teal_slices", "teal.slice")+ } |
||
32 |
- # This one is here because it is needed by c.teal_slices but we don't want it exported from teal.slice.+ |
||
33 | -+ | 3x |
- coalesce_r <- getFromNamespace("coalesce_r", "teal.slice")+ ui <- function(id, ...) { |
34 | -+ | ! |
- # all *Block objects are private in teal.reporter+ teal.reporter::reporter_previewer_ui(id, ...) |
35 |
- RcodeBlock <- getFromNamespace("RcodeBlock", "teal.reporter") # nolint: object_name.+ } |
||
37 | +3x | +
+ module <- module(+ |
+ |
38 | +3x | +
+ label = "temporary label",+ |
+ |
39 | +3x | +
+ server = srv, ui = ui,+ |
+ |
40 | +3x | +
+ server_args = server_args, ui_args = list(), datanames = NULL+ |
+ |
41 |
- # Use non-exported function(s) from teal.code+ ) |
||
38 | +42 |
- # This one is here because lang2calls should not be exported from teal.code+ # Module is created with a placeholder label and the label is changed later. |
|
39 | +43 |
- lang2calls <- getFromNamespace("lang2calls", "teal.code")+ # This is to prevent another module being labeled "Report previewer".+ |
+ |
44 | +3x | +
+ class(module) <- c(class(module), "teal_module_previewer")+ |
+ |
45 | +3x | +
+ module$label <- label+ |
+ |
46 | +3x | +
+ attr(module, "teal_bookmarkable") <- TRUE+ |
+ |
47 | +3x | +
+ module+ |
+ |
48 | ++ |
+ } |
1 |
- #' Validate that dataset has a minimum number of observations+ #' Module to transform `reactive` `teal_data` |
||
3 |
- #' `r lifecycle::badge("stable")`+ #' Module calls multiple [`module_teal_data`] in sequence so that `reactive teal_data` output |
||
4 |
- #'+ #' from one module is handed over to the following module's input. |
||
5 |
- #' This function is a wrapper for `shiny::validate`.+ #' |
||
6 |
- #'+ #' @inheritParams module_teal_data |
||
7 |
- #' @param x (`data.frame`)+ #' @inheritParams teal_modules |
||
8 |
- #' @param min_nrow (`numeric(1)`) Minimum allowed number of rows in `x`.+ #' @return `reactive` `teal_data` |
||
9 |
- #' @param complete (`logical(1)`) Flag specifying whether to check only complete cases. Defaults to `FALSE`.+ #' |
||
10 |
- #' @param allow_inf (`logical(1)`) Flag specifying whether to allow infinite values. Defaults to `TRUE`.+ #' |
||
11 |
- #' @param msg (`character(1)`) Additional message to display alongside the default message.+ #' @name module_transform_data |
||
12 |
- #'+ #' @keywords internal |
||
13 |
- #' @export+ NULL |
||
14 |
- #'+ |
||
15 |
- #' @examples+ #' @rdname module_transform_data |
||
16 |
- #' library(teal)+ ui_transform_data <- function(id, transforms, class = "well") { |
||
17 | -+ | ! |
- #' ui <- fluidPage(+ checkmate::assert_string(id) |
18 | -+ | ! |
- #' sliderInput("len", "Max Length of Sepal",+ checkmate::assert_list(transforms, "teal_transform_module", null.ok = TRUE) |
19 | -+ | ! |
- #' min = 4.3, max = 7.9, value = 5+ ns <- NS(id) |
20 | -+ | ! |
- #' ),+ labels <- lapply(transforms, function(x) attr(x, "label")) |
21 | -+ | ! |
- #' plotOutput("plot")+ ids <- get_unique_labels(labels) |
22 | -+ | ! |
- #' )+ names(transforms) <- ids |
23 |
- #'+ |
||
24 | -+ | ! |
- #' server <- function(input, output) {+ lapply( |
25 | -+ | ! |
- #' output$plot <- renderPlot({+ names(transforms), |
26 | -+ | ! |
- #' iris_df <- iris[iris$Sepal.Length <= input$len, ]+ function(name) { |
27 | -+ | ! |
- #' validate_has_data(+ data_mod <- transforms[[name]] |
28 | -+ | ! |
- #' iris_df,+ wrapper_id <- ns(sprintf("wrapper_%s", name)) |
29 | -+ | ! |
- #' min_nrow = 10,+ div( # todo: accordion? |
30 |
- #' complete = FALSE,+ # class .teal_validated changes the color of the boarder on error in ui_validate_reactive_teal_data |
||
31 |
- #' msg = "Please adjust Max Length of Sepal"+ # For details see tealValidate.js file. |
||
32 | -+ | ! |
- #' )+ class = c(class, "teal_validated"), |
33 | -+ | ! |
- #'+ title = attr(data_mod, "label"), |
34 | -+ | ! |
- #' hist(iris_df$Sepal.Length, breaks = 5)+ tags$span( |
35 | -+ | ! |
- #' })+ class = "text-primary mb-4", |
36 | -+ | ! |
- #' }+ icon("fas fa-square-pen"), |
37 | -+ | ! |
- #' if (interactive()) {+ attr(data_mod, "label") |
38 |
- #' shinyApp(ui, server)+ ), |
||
39 | -+ | ! |
- #' }+ tags$i( |
40 | -+ | ! |
- #'+ class = "remove pull-right fa fa-angle-down", |
41 | -+ | ! |
- validate_has_data <- function(x,+ style = "cursor: pointer;", |
42 | -+ | ! |
- min_nrow = NULL,+ title = "fold/expand transform panel", |
43 | -+ | ! |
- complete = FALSE,+ onclick = sprintf("togglePanelItems(this, '%s', 'fa-angle-right', 'fa-angle-down');", wrapper_id) |
44 |
- allow_inf = TRUE,+ ), |
||
45 | -+ | ! |
- msg = NULL) {+ div( |
46 | -17x | +! |
- checkmate::assert_string(msg, null.ok = TRUE)+ id = wrapper_id, |
47 | -15x | +! |
- checkmate::assert_data_frame(x)+ ui_teal_data(id = ns(name), data_module = transforms[[name]]$ui) |
48 | -15x | +
- if (!is.null(min_nrow)) {+ ) |
|
49 | -15x | +
- if (complete) {+ ) |
|
50 | -5x | +
- complete_index <- stats::complete.cases(x)+ } |
|
51 | -5x | +
- validate(need(+ ) |
|
52 | -5x | +
- sum(complete_index) > 0 && nrow(x[complete_index, , drop = FALSE]) >= min_nrow,+ } |
|
53 | -5x | +
- paste(c(paste("Number of complete cases is less than:", min_nrow), msg), collapse = "\n")+ |
|
54 |
- ))+ #' @rdname module_transform_data |
||
55 |
- } else {+ srv_transform_data <- function(id, data, transforms, modules, is_transformer_failed = reactiveValues()) { |
||
56 | -10x | +95x |
- validate(need(+ checkmate::assert_string(id) |
57 | -10x | +95x |
- nrow(x) >= min_nrow,+ assert_reactive(data) |
58 | -10x | +95x |
- paste(+ checkmate::assert_list(transforms, "teal_transform_module", null.ok = TRUE) |
59 | -10x | +95x |
- c(paste("Minimum number of records not met: >=", min_nrow, "records required."), msg),+ checkmate::assert_class(modules, "teal_module") |
60 | -10x | +95x |
- collapse = "\n"+ if (length(transforms) == 0L) { |
61 | -+ | 80x |
- )+ return(data) |
62 |
- ))+ } |
||
63 | -+ | 15x |
- }+ labels <- lapply(transforms, function(x) attr(x, "label")) |
64 | -+ | 15x |
-
+ ids <- get_unique_labels(labels) |
65 | -10x | +15x |
- if (!allow_inf) {+ names(transforms) <- ids |
66 | -6x | +15x |
- validate(need(+ moduleServer(id, function(input, output, session) { |
67 | -6x | +15x |
- all(vapply(x, function(col) !is.numeric(col) || !any(is.infinite(col)), logical(1))),+ logger::log_debug("srv_teal_data_modules initializing.") |
68 | -6x | +15x |
- "Dataframe contains Inf values which is not allowed."+ Reduce( |
69 | -+ | 15x |
- ))+ function(previous_result, name) { |
70 | -+ | 18x |
- }+ srv_teal_data( |
71 | -+ | 18x |
- }+ id = name, |
72 | -+ | 18x |
- }+ data_module = function(id) transforms[[name]]$server(id, previous_result), |
73 | -+ | 18x |
-
+ modules = modules, |
74 | -+ | 18x |
- #' Validate that dataset has unique rows for key variables+ is_transformer_failed = is_transformer_failed |
75 |
- #'+ ) |
||
76 |
- #' `r lifecycle::badge("stable")`+ }, |
||
77 | -+ | 15x |
- #'+ x = names(transforms), |
78 | -+ | 15x |
- #' This function is a wrapper for `shiny::validate`.+ init = data |
79 |
- #'+ ) |
||
80 |
- #' @param x (`data.frame`)+ }) |
||
81 |
- #' @param key (`character`) Vector of ID variables from `x` that identify unique records.+ } |
82 | +1 |
- #'+ #' Store and restore `teal_slices` object |
|
83 | +2 |
- #' @export+ #' |
|
84 | +3 |
- #'+ #' Functions that write a `teal_slices` object to a file in the `JSON` format, |
|
85 | +4 |
- #' @examples+ #' and also restore the object from disk. |
|
86 | +5 |
- #' iris$id <- rep(1:50, times = 3)+ #' |
|
87 | +6 |
- #' ui <- fluidPage(+ #' Date and date time objects are stored in the following formats: |
|
88 | +7 |
- #' selectInput(+ #' |
|
89 | +8 |
- #' inputId = "species",+ #' - `Date` class is converted to the `"ISO8601"` standard (`YYYY-MM-DD`). |
|
90 | +9 |
- #' label = "Select species",+ #' - `POSIX*t` classes are converted to character by using |
|
91 | +10 |
- #' choices = c("setosa", "versicolor", "virginica"),+ #' `format.POSIX*t(usetz = TRUE, tz = "UTC")` (`YYYY-MM-DD HH:MM:SS UTC`, where |
|
92 | +11 |
- #' selected = "setosa",+ #' `UTC` is the `Coordinated Universal Time` timezone short-code). |
|
93 | +12 |
- #' multiple = TRUE+ #' |
|
94 | +13 |
- #' ),+ #' This format is assumed during `slices_restore`. All `POSIX*t` objects in |
|
95 | +14 |
- #' plotOutput("plot")+ #' `selected` or `choices` fields of `teal_slice` objects are always printed in |
|
96 | +15 |
- #' )+ #' `UTC` timezone as well. |
|
97 | +16 |
- #' server <- function(input, output) {+ #' |
|
98 | +17 |
- #' output$plot <- renderPlot({+ #' @param tss (`teal_slices`) object to be stored. |
|
99 | +18 |
- #' iris_f <- iris[iris$Species %in% input$species, ]+ #' @param file (`character(1)`) file path where `teal_slices` object will be |
|
100 | +19 |
- #' validate_one_row_per_id(iris_f, key = c("id"))+ #' saved and restored. The file extension should be `".json"`. |
|
101 | +20 |
#' |
|
102 | +21 |
- #' hist(iris_f$Sepal.Length, breaks = 5)+ #' @return `slices_store` returns `NULL`, invisibly. |
|
103 | +22 |
- #' })+ #' |
|
104 | +23 |
- #' }+ #' @seealso [teal_slices()] |
|
105 | +24 |
- #' if (interactive()) {+ #' |
|
106 | +25 |
- #' shinyApp(ui, server)+ #' @keywords internal |
|
107 | +26 |
- #' }+ #' |
|
108 | +27 |
- #'+ slices_store <- function(tss, file) {+ |
+ |
28 | +9x | +
+ checkmate::assert_class(tss, "teal_slices")+ |
+ |
29 | +9x | +
+ checkmate::assert_path_for_output(file, overwrite = TRUE, extension = "json") |
|
109 | +30 |
- validate_one_row_per_id <- function(x, key = c("USUBJID", "STUDYID")) {+ |
|
110 | -! | +||
31 | +9x |
- validate(need(!any(duplicated(x[key])), paste("Found more than one row per id.")))+ cat(format(tss, trim_lines = FALSE), "\n", file = file) |
|
111 | +32 |
} |
|
112 | +33 | ||
113 | +34 |
- #' Validates that vector includes all expected values+ #' @rdname slices_store |
|
114 | +35 |
- #'+ #' @return `slices_restore` returns a `teal_slices` object restored from the file. |
|
115 | +36 |
- #' `r lifecycle::badge("stable")`+ #' @keywords internal |
|
116 | +37 |
- #'+ slices_restore <- function(file) { |
|
117 | -+ | ||
38 | +9x |
- #' This function is a wrapper for `shiny::validate`.+ checkmate::assert_file_exists(file, access = "r", extension = "json") |
|
118 | +39 |
- #'+ |
|
119 | -+ | ||
40 | +9x |
- #' @param x Vector of values to test.+ tss_json <- jsonlite::fromJSON(file, simplifyDataFrame = FALSE) |
|
120 | -+ | ||
41 | +9x |
- #' @param choices Vector to test against.+ tss_json$slices <-+ |
+ |
42 | +9x | +
+ lapply(tss_json$slices, function(slice) {+ |
+ |
43 | +9x | +
+ for (field in c("selected", "choices")) {+ |
+ |
44 | +18x | +
+ if (!is.null(slice[[field]])) {+ |
+ |
45 | +12x | +
+ if (length(slice[[field]]) > 0) {+ |
+ |
46 | +9x | +
+ date_partial_regex <- "^[0-9]{4}-[0-9]{2}-[0-9]{2}"+ |
+ |
47 | +9x | +
+ time_stamp_regex <- paste0(date_partial_regex, "\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\sUTC$") |
|
121 | +48 |
- #' @param msg (`character(1)`) Error message to display if some elements of `x` are not elements of `choices`.+ + |
+ |
49 | +9x | +
+ slice[[field]] <-+ |
+ |
50 | +9x | +
+ if (all(grepl(paste0(date_partial_regex, "$"), slice[[field]]))) {+ |
+ |
51 | +3x | +
+ as.Date(slice[[field]])+ |
+ |
52 | +9x | +
+ } else if (all(grepl(time_stamp_regex, slice[[field]]))) {+ |
+ |
53 | +3x | +
+ as.POSIXct(slice[[field]], tz = "UTC") |
|
122 | +54 |
- #'+ } else {+ |
+ |
55 | +3x | +
+ slice[[field]] |
|
123 | +56 |
- #' @export+ } |
|
124 | +57 |
- #'+ } else {+ |
+ |
58 | +3x | +
+ slice[[field]] <- character(0) |
|
125 | +59 |
- #' @examples+ } |
|
126 | +60 |
- #' ui <- fluidPage(+ } |
|
127 | +61 |
- #' selectInput(+ }+ |
+ |
62 | +9x | +
+ slice |
|
128 | +63 |
- #' "species",+ }) |
|
129 | +64 |
- #' "Select species",+ + |
+ |
65 | +9x | +
+ tss_elements <- lapply(tss_json$slices, as.teal_slice) |
|
130 | +66 |
- #' choices = c("setosa", "versicolor", "virginica", "unknown species"),+ + |
+ |
67 | +9x | +
+ do.call(teal_slices, c(tss_elements, tss_json$attributes)) |
|
131 | +68 |
- #' selected = "setosa",+ } |
132 | +1 |
- #' multiple = FALSE+ #' Landing popup module |
|
133 | +2 |
- #' ),+ #' |
|
134 | +3 |
- #' verbatimTextOutput("summary")+ #' @description Creates a landing welcome popup for `teal` applications. |
|
135 | +4 |
- #' )+ #' |
|
136 | +5 |
- #'+ #' This module is used to display a popup dialog when the application starts. |
|
137 | +6 |
- #' server <- function(input, output) {+ #' The dialog blocks access to the application and must be closed with a button before the application can be viewed. |
|
138 | +7 |
- #' output$summary <- renderPrint({+ #' |
|
139 | +8 |
- #' validate_in(input$species, iris$Species, "Species does not exist.")+ #' @param label (`character(1)`) Label of the module. |
|
140 | +9 |
- #' nrow(iris[iris$Species == input$species, ])+ #' @param title (`character(1)`) Text to be displayed as popup title. |
|
141 | +10 |
- #' })+ #' @param content (`character(1)`, `shiny.tag` or `shiny.tag.list`) with the content of the popup. |
|
142 | +11 |
- #' }+ #' Passed to `...` of `shiny::modalDialog`. See examples. |
|
143 | +12 |
- #' if (interactive()) {+ #' @param buttons (`shiny.tag` or `shiny.tag.list`) Typically a `modalButton` or `actionButton`. See examples. |
|
144 | +13 |
- #' shinyApp(ui, server)+ #' |
|
145 | +14 |
- #' }+ #' @return A `teal_module` (extended with `teal_landing_module` class) to be used in `teal` applications. |
|
146 | +15 |
#' |
|
147 | +16 |
- validate_in <- function(x, choices, msg) {+ #' @examples |
|
148 | -! | +||
17 | +
- validate(need(length(x) > 0 && length(choices) > 0 && all(x %in% choices), msg))+ #' app1 <- init( |
||
149 | +18 |
- }+ #' data = teal_data(iris = iris), |
|
150 | +19 |
-
+ #' modules = modules( |
|
151 | +20 |
- #' Validates that vector has length greater than 0+ #' example_module() |
|
152 | +21 |
- #'+ #' ), |
|
153 | +22 |
- #' `r lifecycle::badge("stable")`+ #' landing_popup = landing_popup_module( |
|
154 | +23 |
- #'+ #' content = "A place for the welcome message or a disclaimer statement.", |
|
155 | +24 |
- #' This function is a wrapper for `shiny::validate`.+ #' buttons = modalButton("Proceed") |
|
156 | +25 |
- #'+ #' ) |
|
157 | +26 |
- #' @param x vector+ #' ) |
|
158 | +27 |
- #' @param msg message to display+ #' if (interactive()) { |
|
159 | +28 |
- #'+ #' shinyApp(app1$ui, app1$server) |
|
160 | +29 |
- #' @export+ #' } |
|
161 | +30 |
#' |
|
162 | +31 |
- #' @examples+ #' app2 <- init( |
|
163 | +32 |
- #' data <- data.frame(+ #' data = teal_data(iris = iris), |
|
164 | +33 |
- #' id = c(1:10, 11:20, 1:10),+ #' modules = modules( |
|
165 | +34 |
- #' strata = rep(c("A", "B"), each = 15)+ #' example_module() |
|
166 | +35 |
- #' )+ #' ), |
|
167 | +36 |
- #' ui <- fluidPage(+ #' landing_popup = landing_popup_module( |
|
168 | +37 |
- #' selectInput("ref1", "Select strata1 to compare",+ #' title = "Welcome", |
|
169 | +38 |
- #' choices = c("A", "B", "C"), selected = "A"+ #' content = tags$b( |
|
170 | +39 |
- #' ),+ #' "A place for the welcome message or a disclaimer statement.", |
|
171 | +40 |
- #' selectInput("ref2", "Select strata2 to compare",+ #' style = "color: red;" |
|
172 | +41 |
- #' choices = c("A", "B", "C"), selected = "B"+ #' ), |
|
173 | +42 |
- #' ),+ #' buttons = tagList( |
|
174 | +43 |
- #' verbatimTextOutput("arm_summary")+ #' modalButton("Proceed"), |
|
175 | +44 |
- #' )+ #' actionButton("read", "Read more", |
|
176 | +45 |
- #'+ #' onclick = "window.open('http://google.com', '_blank')" |
|
177 | +46 |
- #' server <- function(input, output) {+ #' ), |
|
178 | +47 |
- #' output$arm_summary <- renderText({+ #' actionButton("close", "Reject", onclick = "window.close()") |
|
179 | +48 |
- #' sample_1 <- data$id[data$strata == input$ref1]+ #' ) |
|
180 | +49 |
- #' sample_2 <- data$id[data$strata == input$ref2]+ #' ) |
|
181 | +50 |
- #'+ #' ) |
|
182 | +51 |
- #' validate_has_elements(sample_1, "No subjects in strata1.")+ #' |
|
183 | +52 |
- #' validate_has_elements(sample_2, "No subjects in strata2.")+ #' if (interactive()) { |
|
184 | +53 |
- #'+ #' shinyApp(app2$ui, app2$server) |
|
185 | +54 |
- #' paste0(+ #' } |
|
186 | +55 |
- #' "Number of samples in: strata1=", length(sample_1),+ #' |
|
187 | +56 |
- #' " comparions strata2=", length(sample_2)+ #' @export |
|
188 | +57 |
- #' )+ landing_popup_module <- function(label = "Landing Popup", |
|
189 | +58 |
- #' })+ title = NULL, |
|
190 | +59 |
- #' }+ content = NULL, |
|
191 | +60 |
- #' if (interactive()) {+ buttons = modalButton("Accept")) { |
|
192 | -+ | ||
61 | +! |
- #' shinyApp(ui, server)+ checkmate::assert_string(label) |
|
193 | -+ | ||
62 | +! |
- #' }+ checkmate::assert_string(title, null.ok = TRUE) |
|
194 | -+ | ||
63 | +! |
- validate_has_elements <- function(x, msg) {+ checkmate::assert_multi_class( |
|
195 | +64 | ! |
- validate(need(length(x) > 0, msg))+ content,+ |
+
65 | +! | +
+ classes = c("character", "shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE |
|
196 | +66 |
- }+ )+ |
+ |
67 | +! | +
+ checkmate::assert_multi_class(buttons, classes = c("shiny.tag", "shiny.tag.list")) |
|
197 | +68 | ||
198 | -+ | ||
69 | +! |
- #' Validates no intersection between two vectors+ message("Initializing landing_popup_module") |
|
199 | +70 |
- #'+ |
|
200 | -+ | ||
71 | +! |
- #' `r lifecycle::badge("stable")`+ module <- module( |
|
201 | -+ | ||
72 | +! |
- #'+ label = label, |
|
202 | -+ | ||
73 | +! |
- #' This function is a wrapper for `shiny::validate`.+ server = function(id) { |
|
203 | -+ | ||
74 | +! |
- #'+ moduleServer(id, function(input, output, session) { |
|
204 | -+ | ||
75 | +! |
- #' @param x vector+ showModal( |
|
205 | -+ | ||
76 | +! |
- #' @param y vector+ modalDialog( |
|
206 | -+ | ||
77 | +! |
- #' @param msg (`character(1)`) message to display if `x` and `y` intersect+ id = "landingpopup", |
|
207 | -+ | ||
78 | +! |
- #'+ title = title, |
|
208 | -+ | ||
79 | +! |
- #' @export+ content, |
|
209 | -+ | ||
80 | +! |
- #'+ footer = buttons |
|
210 | +81 |
- #' @examples+ ) |
|
211 | +82 |
- #' data <- data.frame(+ ) |
|
212 | +83 |
- #' id = c(1:10, 11:20, 1:10),+ }) |
|
213 | +84 |
- #' strata = rep(c("A", "B", "C"), each = 10)+ } |
|
214 | +85 |
- #' )+ ) |
|
215 | -+ | ||
86 | +! |
- #'+ class(module) <- c("teal_module_landing", class(module)) |
|
216 | -+ | ||
87 | +! |
- #' ui <- fluidPage(+ module |
|
217 | +88 |
- #' selectInput("ref1", "Select strata1 to compare",+ } |
218 | +1 |
- #' choices = c("A", "B", "C"),+ #' Data Module for teal |
|
219 | +2 |
- #' selected = "A"+ #' |
|
220 | +3 |
- #' ),+ #' This module manages the `data` argument for `srv_teal`. The `teal` framework uses [teal_data()], |
|
221 | +4 |
- #' selectInput("ref2", "Select strata2 to compare",+ #' which can be provided in various ways: |
|
222 | +5 |
- #' choices = c("A", "B", "C"),+ #' 1. Directly as a [teal.data::teal_data()] object. This will automatically convert it into a `reactive` `teal_data`. |
|
223 | +6 |
- #' selected = "B"+ #' 2. As a `reactive` object that returns a [teal.data::teal_data()] object. |
|
224 | +7 |
- #' ),+ #' |
|
225 | +8 |
- #' verbatimTextOutput("summary")+ #' @details |
|
226 | +9 |
- #' )+ #' ## Reactive `teal_data`: |
|
227 | +10 |
#' |
|
228 | +11 |
- #' server <- function(input, output) {+ #' The data in the application can be reactively updated, prompting [srv_teal()] to rebuild the |
|
229 | +12 |
- #' output$summary <- renderText({+ #' content accordingly. There are two methods for creating interactive `teal_data`: |
|
230 | +13 |
- #' sample_1 <- data$id[data$strata == input$ref1]+ #' 1. Using a `reactive` object provided from outside the `teal` application. In this scenario, |
|
231 | +14 |
- #' sample_2 <- data$id[data$strata == input$ref2]+ #' reactivity is controlled by an external module, and `srv_teal` responds to changes. |
|
232 | +15 |
- #'+ #' 2. Using [teal_data_module()], which is embedded within the `teal` application, allowing data to |
|
233 | +16 |
- #' validate_no_intersection(+ #' be resubmitted by the user as needed. |
|
234 | +17 |
- #' sample_1, sample_2,+ #' |
|
235 | +18 |
- #' "subjects within strata1 and strata2 cannot overlap"+ #' Since the server of [teal_data_module()] must return a `reactive` `teal_data` object, both |
|
236 | +19 |
- #' )+ #' methods (1 and 2) produce the same reactive behavior within a `teal` application. The distinction |
|
237 | +20 |
- #' paste0(+ #' lies in data control: the first method involves external control, while the second method |
|
238 | +21 |
- #' "Number of subject in: reference treatment=", length(sample_1),+ #' involves control from a custom module within the app. |
|
239 | +22 |
- #' " comparions treatment=", length(sample_2)+ #' |
|
240 | +23 |
- #' )+ #' For more details, see [`module_teal_data`]. |
|
241 | +24 |
- #' })+ #' |
|
242 | +25 |
- #' }+ #' @inheritParams init |
|
243 | +26 |
- #' if (interactive()) {+ #' |
|
244 | +27 |
- #' shinyApp(ui, server)+ #' @param data (`teal_data`, `teal_data_module`, or `reactive` returning `teal_data`) |
|
245 | +28 |
- #' }+ #' The data which application will depend on. |
|
246 | +29 |
#' |
|
247 | +30 |
- validate_no_intersection <- function(x, y, msg) {+ #' @return A `reactive` object that returns: |
|
248 | -! | +||
31 | +
- validate(need(length(intersect(x, y)) == 0, msg))+ #' Output of the `data`. If `data` fails then returned error is handled (after [tryCatch()]) so that |
||
249 | +32 |
- }+ #' rest of the application can respond to this respectively. |
|
250 | +33 |
-
+ #' |
|
251 | +34 |
-
+ #' @rdname module_init_data |
|
252 | +35 |
- #' Validates that dataset contains specific variable+ #' @name module_init_data |
|
253 | +36 |
- #'+ #' @keywords internal |
|
254 | +37 |
- #' `r lifecycle::badge("stable")`+ NULL |
|
255 | +38 |
- #'+ |
|
256 | +39 |
- #' This function is a wrapper for `shiny::validate`.+ #' @rdname module_init_data |
|
257 | +40 |
- #'+ ui_init_data <- function(id) { |
|
258 | -+ | ||
41 | +8x |
- #' @param data (`data.frame`)+ ns <- shiny::NS(id) |
|
259 | -+ | ||
42 | +8x |
- #' @param varname (`character(1)`) name of variable to check for in `data`+ shiny::div( |
|
260 | -+ | ||
43 | +8x |
- #' @param msg (`character(1)`) message to display if `data` does not include `varname`+ id = ns("content"), |
|
261 | -+ | ||
44 | +8x |
- #'+ style = "display: inline-block; width: 100%;", |
|
262 | -+ | ||
45 | +8x |
- #' @export+ uiOutput(ns("data")) |
|
263 | +46 |
- #'+ ) |
|
264 | +47 |
- #' @examples+ } |
|
265 | +48 |
- #' data <- data.frame(+ |
|
266 | +49 |
- #' one = rep("a", length.out = 20),+ #' @rdname module_init_data |
|
267 | +50 |
- #' two = rep(c("a", "b"), length.out = 20)+ srv_init_data <- function(id, data) { |
|
268 | -+ | ||
51 | +71x |
- #' )+ checkmate::assert_character(id, max.len = 1, any.missing = FALSE) |
|
269 | -+ | ||
52 | +71x |
- #' ui <- fluidPage(+ checkmate::assert_multi_class(data, c("teal_data", "teal_data_module", "reactive")) |
|
270 | +53 |
- #' selectInput(+ |
|
271 | -+ | ||
54 | +71x |
- #' "var",+ moduleServer(id, function(input, output, session) { |
|
272 | -+ | ||
55 | +71x |
- #' "Select variable",+ logger::log_debug("srv_data initializing.") |
|
273 | +56 |
- #' choices = c("one", "two", "three", "four"),+ # data_rv contains teal_data object |
|
274 | +57 |
- #' selected = "one"+ # either passed to teal::init or returned from teal_data_module |
|
275 | -+ | ||
58 | +71x |
- #' ),+ data_out <- if (inherits(data, "teal_data_module")) { |
|
276 | -+ | ||
59 | +9x |
- #' verbatimTextOutput("summary")+ output$data <- renderUI(data$ui(id = session$ns("teal_data_module"))) |
|
277 | -+ | ||
60 | +9x |
- #' )+ data$server("teal_data_module") |
|
278 | -+ | ||
61 | +71x |
- #'+ } else if (inherits(data, "teal_data")) { |
|
279 | -+ | ||
62 | +35x |
- #' server <- function(input, output) {+ reactiveVal(data) |
|
280 | -+ | ||
63 | +71x |
- #' output$summary <- renderText({+ } else if (test_reactive(data)) { |
|
281 | -+ | ||
64 | +27x |
- #' validate_has_variable(data, input$var)+ data |
|
282 | +65 |
- #' paste0("Selected treatment variables: ", paste(input$var, collapse = ", "))+ } |
|
283 | +66 |
- #' })+ |
|
284 | -+ | ||
67 | +70x |
- #' }+ data_handled <- reactive({ |
|
285 | -+ | ||
68 | +61x |
- #' if (interactive()) {+ tryCatch(data_out(), error = function(e) e) |
|
286 | +69 |
- #' shinyApp(ui, server)+ }) |
|
287 | +70 |
- #' }+ |
|
288 | +71 |
- validate_has_variable <- function(data, varname, msg) {+ # We want to exclude teal_data_module elements from bookmarking as they might have some secrets |
|
289 | -! | +||
72 | +70x |
- if (length(varname) != 0) {+ observeEvent(data_handled(), { |
|
290 | -! | +||
73 | +61x |
- has_vars <- varname %in% names(data)+ if (inherits(data_handled(), "teal_data")) { |
|
291 | -+ | ||
74 | +57x |
-
+ app_session <- .subset2(shiny::getDefaultReactiveDomain(), "parent") |
|
292 | -! | +||
75 | +57x |
- if (!all(has_vars)) {+ setBookmarkExclude( |
|
293 | -! | +||
76 | +57x |
- if (missing(msg)) {+ session$ns( |
|
294 | -! | +||
77 | +57x |
- msg <- sprintf(+ grep( |
|
295 | -! | +||
78 | +57x |
- "%s does not have the required variables: %s.",+ pattern = "teal_data_module-", |
|
296 | -! | +||
79 | +57x |
- deparse(substitute(data)),+ x = names(reactiveValuesToList(input)), |
|
297 | -! | +||
80 | +57x |
- toString(varname[!has_vars])+ value = TRUE |
|
298 | +81 |
- )+ ) |
|
299 | +82 |
- }+ ), |
|
300 | -! | +||
83 | +57x |
- validate(need(FALSE, msg))+ session = app_session |
|
301 | +84 |
- }+ ) |
|
302 | +85 |
- }+ } |
|
303 | +86 |
- }+ }) |
|
304 | +87 | ||
305 | -+ | ||
88 | +70x |
- #' Validate that variables has expected number of levels+ data_handled |
|
306 | +89 |
- #'+ }) |
|
307 | +90 |
- #' `r lifecycle::badge("stable")`+ } |
|
308 | +91 |
- #'+ |
|
309 | +92 |
- #' If the number of levels of `x` is less than `min_levels`+ #' Adds signature protection to the `datanames` in the data |
|
310 | +93 |
- #' or greater than `max_levels` the validation will fail.+ #' @param data (`teal_data`) |
|
311 | +94 |
- #' This function is a wrapper for `shiny::validate`.+ #' @return `teal_data` with additional code that has signature of the `datanames` |
|
312 | +95 |
- #'+ #' @keywords internal |
|
313 | +96 |
- #' @param x variable name. If `x` is not a factor, the unique values+ .add_signature_to_data <- function(data) { |
|
314 | -+ | ||
97 | +57x |
- #' are treated as levels.+ hashes <- .get_hashes_code(data) |
|
315 | +98 |
- #' @param min_levels cutoff for minimum number of levels of `x`+ |
|
316 | -+ | ||
99 | +57x |
- #' @param max_levels cutoff for maximum number of levels of `x`+ tdata <- do.call( |
|
317 | -+ | ||
100 | +57x |
- #' @param var_name name of variable being validated for use in+ teal.data::teal_data, |
|
318 | -+ | ||
101 | +57x |
- #' validation message+ c( |
|
319 | -+ | ||
102 | +57x |
- #'+ list(code = trimws(c(teal.code::get_code(data), hashes), which = "right")), |
|
320 | -+ | ||
103 | +57x |
- #' @export+ list(join_keys = teal.data::join_keys(data)),+ |
+ |
104 | +57x | +
+ sapply(+ |
+ |
105 | +57x | +
+ ls(teal.code::get_env(data)),+ |
+ |
106 | +57x | +
+ teal.code::get_var,+ |
+ |
107 | +57x | +
+ object = data,+ |
+ |
108 | +57x | +
+ simplify = FALSE |
|
321 | +109 |
- #' @examples+ ) |
|
322 | +110 |
- #' data <- data.frame(+ ) |
|
323 | +111 |
- #' one = rep("a", length.out = 20),+ ) |
|
324 | +112 |
- #' two = rep(c("a", "b"), length.out = 20),+ + |
+ |
113 | +57x | +
+ tdata@verified <- data@verified+ |
+ |
114 | +57x | +
+ tdata |
|
325 | +115 |
- #' three = rep(c("a", "b", "c"), length.out = 20),+ } |
|
326 | +116 |
- #' four = rep(c("a", "b", "c", "d"), length.out = 20),+ |
|
327 | +117 |
- #' stringsAsFactors = TRUE+ #' Get code that tests the integrity of the reproducible data |
|
328 | +118 |
- #' )+ #' |
|
329 | +119 |
- #' ui <- fluidPage(+ #' @param data (`teal_data`) object holding the data |
|
330 | +120 |
- #' selectInput(+ #' @param datanames (`character`) names of `datasets` |
|
331 | +121 |
- #' "var",+ #' |
|
332 | +122 |
- #' "Select variable",+ #' @return A character vector with the code lines. |
|
333 | +123 |
- #' choices = c("one", "two", "three", "four"),+ #' @keywords internal |
|
334 | +124 |
- #' selected = "one"+ #' |
|
335 | +125 |
- #' ),+ .get_hashes_code <- function(data, datanames = ls(teal.code::get_env(data))) { |
|
336 | -+ | ||
126 | +57x |
- #' verbatimTextOutput("summary")+ vapply( |
|
337 | -+ | ||
127 | +57x |
- #' )+ datanames, |
|
338 | -+ | ||
128 | +57x |
- #'+ function(dataname, datasets) { |
|
339 | -+ | ||
129 | +102x |
- #' server <- function(input, output) {+ hash <- rlang::hash(data[[dataname]]) |
|
340 | -+ | ||
130 | +102x |
- #' output$summary <- renderText({+ sprintf( |
|
341 | -+ | ||
131 | +102x |
- #' validate_n_levels(data[[input$var]], min_levels = 2, max_levels = 15, var_name = input$var)+ "stopifnot(%s == %s) # @linksto %s", |
|
342 | -+ | ||
132 | +102x |
- #' paste0(+ deparse1(bquote(rlang::hash(.(as.name(dataname))))), |
|
343 | -+ | ||
133 | +102x |
- #' "Levels of selected treatment variable: ",+ deparse1(hash), |
|
344 | -+ | ||
134 | +102x |
- #' paste(levels(data[[input$var]]),+ dataname |
|
345 | +135 |
- #' collapse = ", "+ ) |
|
346 | +136 |
- #' )+ }, |
|
347 | -+ | ||
137 | +57x |
- #' )+ character(1L), |
|
348 | -+ | ||
138 | +57x |
- #' })+ USE.NAMES = TRUE |
|
349 | +139 |
- #' }+ ) |
|
350 | +140 |
- #' if (interactive()) {+ } |
351 | +1 |
- #' shinyApp(ui, server)+ #' Generates library calls from current session info |
|
352 | +2 |
- #' }+ #' |
|
353 | +3 |
- validate_n_levels <- function(x, min_levels = 1, max_levels = 12, var_name) {- |
- |
354 | -! | -
- x_levels <- if (is.factor(x)) {- |
- |
355 | -! | -
- levels(x)+ #' Function to create multiple library calls out of current session info to ensure reproducible code works. |
|
356 | +4 |
- } else {+ #' |
|
357 | -! | +||
5 | +
- unique(x)+ #' @return Character vector of `library(<package>)` calls. |
||
358 | +6 |
- }+ #' @keywords internal |
|
359 | +7 |
-
+ get_rcode_libraries <- function() { |
|
360 | -! | +||
8 | +1x |
- if (!is.null(min_levels) && !(is.null(max_levels))) {+ libraries <- vapply( |
|
361 | -! | +||
9 | +1x |
- validate(need(+ utils::sessionInfo()$otherPkgs, |
|
362 | -! | +||
10 | +1x |
- length(x_levels) >= min_levels && length(x_levels) <= max_levels,+ function(x) { |
|
363 | -! | +||
11 | +6x |
- sprintf(+ paste0("library(", x$Package, ")") |
|
364 | -! | +||
12 | +
- "%s variable needs minimum %s level(s) and maximum %s level(s).",+ }, |
||
365 | -! | +||
13 | +1x |
- var_name, min_levels, max_levels+ character(1) |
|
366 | +14 |
- )+ ) |
|
367 | -+ | ||
15 | +1x |
- ))+ paste0(paste0(rev(libraries), sep = "\n"), collapse = "") |
|
368 | -! | +||
16 | +
- } else if (!is.null(min_levels)) {+ } |
||
369 | -! | +||
17 | +
- validate(need(+ |
||
370 | -! | +||
18 | +
- length(x_levels) >= min_levels,+ |
||
371 | -! | +||
19 | +
- sprintf("%s variable needs minimum %s levels(s)", var_name, min_levels)+ #' @noRd |
||
372 | +20 |
- ))+ #' @keywords internal |
|
373 | -! | +||
21 | +
- } else if (!is.null(max_levels)) {+ get_rcode_str_install <- function() { |
||
374 | -! | +||
22 | +5x |
- validate(need(+ code_string <- getOption("teal.load_nest_code") |
|
375 | -! | +||
23 | +5x |
- length(x_levels) <= max_levels,+ if (is.character(code_string)) { |
|
376 | -! | +||
24 | +2x |
- sprintf("%s variable needs maximum %s level(s)", var_name, max_levels)+ code_string |
|
377 | +25 |
- ))+ } else {+ |
+ |
26 | +3x | +
+ "# Add any code to install/load your NEST environment here\n" |
|
378 | +27 |
} |
|
379 | +28 |
}@@ -44071,14 +45071,14 @@ teal coverage - 57.64% |
1 |
- #' Generates library calls from current session info+ #' Show `R` code modal |
||
3 |
- #' Function to create multiple library calls out of current session info to ensure reproducible code works.+ #' @description `r lifecycle::badge("deprecated")` |
||
5 |
- #' @return Character vector of `library(<package>)` calls.+ #' Use the [shiny::showModal()] function to show the `R` code inside. |
||
6 |
- #' @keywords internal+ #' |
||
7 |
- get_rcode_libraries <- function() {+ #' @param title (`character(1)`) |
||
8 | -1x | +
- libraries <- vapply(+ #' Title of the modal, displayed in the first comment of the `R` code. |
|
9 | -1x | +
- utils::sessionInfo()$otherPkgs,+ #' @param rcode (`character`) |
|
10 | -1x | +
- function(x) {+ #' vector with `R` code to show inside the modal. |
|
11 | -6x | +
- paste0("library(", x$Package, ")")+ #' @param session (`ShinySession`) optional |
|
12 |
- },+ #' `shiny` session object, defaults to [shiny::getDefaultReactiveDomain()]. |
||
13 | -1x | +
- character(1)+ #' |
|
14 |
- )+ #' @references [shiny::showModal()] |
||
15 | -1x | +
- paste0(paste0(rev(libraries), sep = "\n"), collapse = "")+ #' @export |
|
16 |
- }+ show_rcode_modal <- function(title = NULL, rcode, session = getDefaultReactiveDomain()) { |
||
17 | +! | +
+ lifecycle::deprecate_soft(+ |
+ |
18 | +! | +
+ when = "0.16",+ |
+ |
19 | +! | +
+ what = "show_rcode_modal()",+ |
+ |
20 | +! | +
+ details = "This function will be removed in the next release."+ |
+ |
21 |
-
+ ) |
||
18 | +22 | ||
23 | +! | +
+ rcode <- paste(rcode, collapse = "\n")+ |
+ |
19 | +24 |
- #' @noRd+ + |
+ |
25 | +! | +
+ ns <- session$ns+ |
+ |
26 | +! | +
+ showModal(modalDialog(+ |
+ |
27 | +! | +
+ tagList(+ |
+ |
28 | +! | +
+ tags$div(+ |
+ |
29 | +! | +
+ actionButton(ns("copyRCode"), "Copy to Clipboard", `data-clipboard-target` = paste0("#", ns("r_code"))),+ |
+ |
30 | +! | +
+ modalButton("Dismiss"),+ |
+ |
31 | +! | +
+ style = "mb-4" |
|
20 | +32 |
- #' @keywords internal+ ),+ |
+ |
33 | +! | +
+ tags$div(tags$pre(id = ns("r_code"), rcode)), |
|
21 | +34 |
- get_rcode_str_install <- function() {+ ), |
|
22 | -5x | +||
35 | +! | +
+ title = title,+ |
+ |
36 | +! |
- code_string <- getOption("teal.load_nest_code")+ footer = tagList( |
|
23 | -5x | +||
37 | +! |
- if (is.character(code_string)) {+ actionButton(ns("copyRCode"), "Copy to Clipboard", `data-clipboard-target` = paste0("#", ns("r_code"))), |
|
24 | -2x | +||
38 | +! |
- code_string+ modalButton("Dismiss") |
|
25 | +39 |
- } else {+ ), |
|
26 | -3x | +||
40 | +! |
- "# Add any code to install/load your NEST environment here\n"+ size = "l",+ |
+ |
41 | +! | +
+ easyClose = TRUE |
|
27 | +42 |
- }+ )) |
|
28 | +43 |
}@@ -44573,196 +45678,196 @@ teal coverage - 57.64% |
1 |
- #' UI and server modules of `teal`+ .onLoad <- function(libname, pkgname) { |
||
2 |
- #'+ # adapted from https://github.com/r-lib/devtools/blob/master/R/zzz.R |
||
3 |
- #' @description `r lifecycle::badge("deprecated")`+ |
||
4 | -+ | ! |
- #' Please use [`module_teal`] instead.+ teal_default_options <- list( |
5 | -+ | ! |
- #'+ teal.show_js_log = FALSE, |
6 | -+ | ! |
- #' @inheritParams ui_teal+ teal.lockfile.mode = "auto", |
7 | -+ | ! |
- #' @inheritParams srv_teal+ shiny.sanitize.errors = FALSE |
8 |
- #'+ ) |
||
9 |
- #' @return+ |
||
10 | -+ | ! |
- #' Returns a `reactive` expression containing a `teal_data` object when data is loaded or `NULL` when it is not.+ op <- options() |
11 | -+ | ! |
- #' @name module_teal_with_splash+ toset <- !(names(teal_default_options) %in% names(op)) |
12 | -+ | ! |
- #'+ if (any(toset)) options(teal_default_options[toset]) |
13 |
- NULL+ |
||
14 |
-
+ # Set up the teal logger instance |
||
15 | -+ | ! |
- #' @export+ teal.logger::register_logger("teal") |
16 | -+ | ! |
- #' @rdname module_teal_with_splash+ teal.logger::register_handlers("teal") |
17 |
- ui_teal_with_splash <- function(id,+ |
||
18 | -+ | ! |
- data,+ invisible() |
19 |
- title = build_app_title(),+ } |
||
20 |
- header = tags$p(),+ |
||
21 |
- footer = tags$p()) {+ .onAttach <- function(libname, pkgname) { |
||
22 | -! | +2x |
- lifecycle::deprecate_soft(+ packageStartupMessage( |
23 | -! | +2x |
- when = "0.16",+ "\nYou are using teal version ", |
24 | -! | +
- what = "ui_teal_with_splash()",+ # `system.file` uses the `shim` of `system.file` by `teal` |
|
25 | -! | +
- details = "Deprecated, please use `ui_teal` instead"+ # we avoid `desc` dependency here to get the version |
|
26 | -+ | 2x |
- )+ read.dcf(system.file("DESCRIPTION", package = "teal"))[, "Version"] |
27 | -! | +
- ui_teal(id = id, data = data, title = title, header = header, footer = footer)+ ) |
|
30 |
- #' @export+ # This one is here because setdiff_teal_slice should not be exported from teal.slice. |
||
31 |
- #' @rdname module_teal_with_splash+ setdiff_teal_slices <- getFromNamespace("setdiff_teal_slices", "teal.slice") |
||
32 |
- srv_teal_with_splash <- function(id, data, modules, filter = teal_slices()) {+ # This one is here because it is needed by c.teal_slices but we don't want it exported from teal.slice. |
||
33 | -! | +
- lifecycle::deprecate_soft(+ coalesce_r <- getFromNamespace("coalesce_r", "teal.slice") |
|
34 | -! | +
- when = "0.16",+ # all *Block objects are private in teal.reporter |
|
35 | -! | +
- what = "srv_teal_with_splash()",+ RcodeBlock <- getFromNamespace("RcodeBlock", "teal.reporter") # nolint: object_name. |
|
36 | -! | +
- details = "Deprecated, please use `srv_teal` instead"+ |
|
37 |
- )+ # Use non-exported function(s) from teal.code |
||
38 | -! | +
- srv_teal(id = id, data = data, modules = modules, filter = filter)+ # This one is here because lang2calls should not be exported from teal.code |
|
39 |
- }+ lang2calls <- getFromNamespace("lang2calls", "teal.code") |
||
7 | -885x | +940x |
if (!isTRUE(checkmate::test_class(x, classes = "reactive", null.ok = null.ok))) { |
@@ -44955,7 +46060,7 @@
15 | -882x | +937x |
return(TRUE) |
@@ -45088,21 +46193,21 @@
34 | -41x | +39x |
tryCatch( |
35 | -41x | +39x |
x, |
36 | -41x | +39x |
error = function(e) { |
@@ -45186,7 +46291,7 @@
48 | -38x | +36x |
x |
@@ -45201,313 +46306,6 @@
1 | -- |
- #' Show `R` code modal- |
-
2 | -- |
- #'- |
-
3 | -- |
- #' @description `r lifecycle::badge("deprecated")`- |
-
4 | -- |
- #'- |
-
5 | -- |
- #' Use the [shiny::showModal()] function to show the `R` code inside.- |
-
6 | -- |
- #'- |
-
7 | -- |
- #' @param title (`character(1)`)- |
-
8 | -- |
- #' Title of the modal, displayed in the first comment of the `R` code.- |
-
9 | -- |
- #' @param rcode (`character`)- |
-
10 | -- |
- #' vector with `R` code to show inside the modal.- |
-
11 | -- |
- #' @param session (`ShinySession`) optional- |
-
12 | -- |
- #' `shiny` session object, defaults to [shiny::getDefaultReactiveDomain()].- |
-
13 | -- |
- #'- |
-
14 | -- |
- #' @references [shiny::showModal()]- |
-
15 | -- |
- #' @export- |
-
16 | -- |
- show_rcode_modal <- function(title = NULL, rcode, session = getDefaultReactiveDomain()) {- |
-
17 | -! | -
- lifecycle::deprecate_soft(- |
-
18 | -! | -
- when = "0.16",- |
-
19 | -! | -
- what = "show_rcode_modal()",- |
-
20 | -! | -
- details = "This function will be removed in the next release."- |
-
21 | -- |
- )- |
-
22 | -- | - - | -
23 | -! | -
- rcode <- paste(rcode, collapse = "\n")- |
-
24 | -- | - - | -
25 | -! | -
- ns <- session$ns- |
-
26 | -! | -
- showModal(modalDialog(- |
-
27 | -! | -
- tagList(- |
-
28 | -! | -
- tags$div(- |
-
29 | -! | -
- actionButton(ns("copyRCode"), "Copy to Clipboard", `data-clipboard-target` = paste0("#", ns("r_code"))),- |
-
30 | -! | -
- modalButton("Dismiss"),- |
-
31 | -! | -
- style = "mb-4"- |
-
32 | -- |
- ),- |
-
33 | -! | -
- tags$div(tags$pre(id = ns("r_code"), rcode)),- |
-
34 | -- |
- ),- |
-
35 | -! | -
- title = title,- |
-
36 | -! | -
- footer = tagList(- |
-
37 | -! | -
- actionButton(ns("copyRCode"), "Copy to Clipboard", `data-clipboard-target` = paste0("#", ns("r_code"))),- |
-
38 | -! | -
- modalButton("Dismiss")- |
-
39 | -- |
- ),- |
-
40 | -! | -
- size = "l",- |
-
41 | -! | -
- easyClose = TRUE- |
-
42 | -- |
- ))- |
-
43 | -- |
- }- |
-