1 |
#' Filter panel module in teal |
|
2 |
#' |
|
3 |
#' Creates filter panel module from `teal_data` object and returns `teal_data`. It is build in a way |
|
4 |
#' that filter panel changes and anything what happens before (e.g. [`module_init_data`]) is triggering |
|
5 |
#' further reactive events only if something has changed and if the module is visible. Thanks to |
|
6 |
#' this special implementation all modules' data are recalculated only for those modules which are |
|
7 |
#' currently displayed. |
|
8 |
#' |
|
9 |
#' @return A `eventReactive` containing `teal_data` containing filtered objects and filter code. |
|
10 |
#' `eventReactive` triggers only if all conditions are met: |
|
11 |
#' - tab is selected (`is_active`) |
|
12 |
#' - when filters are changed (`get_filter_expr` is different than previous) |
|
13 |
#' |
|
14 |
#' @inheritParams module_teal_module |
|
15 |
#' @param active_datanames (`reactive` returning `character`) this module's data names |
|
16 |
#' @name module_filter_data |
|
17 |
#' @keywords internal |
|
18 |
NULL |
|
19 | ||
20 |
#' @rdname module_filter_data |
|
21 |
ui_filter_data <- function(id) { |
|
22 | ! |
ns <- shiny::NS(id) |
23 | ! |
uiOutput(ns("panel")) |
24 |
} |
|
25 | ||
26 |
#' @rdname module_filter_data |
|
27 |
srv_filter_data <- function(id, datasets, active_datanames, data, is_active) { |
|
28 | 86x |
assert_reactive(datasets) |
29 | 86x |
moduleServer(id, function(input, output, session) { |
30 | 86x |
active_corrected <- reactive(intersect(active_datanames(), datasets()$datanames())) |
31 | ||
32 | 86x |
output$panel <- renderUI({ |
33 | 88x |
req(inherits(datasets(), "FilteredData")) |
34 | 88x |
isolate({ |
35 |
# render will be triggered only when FilteredData object changes (not when filters change) |
|
36 |
# technically it means that teal_data_module needs to be refreshed |
|
37 | 88x |
logger::log_debug("srv_filter_panel rendering filter panel.") |
38 | 88x |
if (length(active_corrected())) { |
39 | 86x |
datasets()$srv_active("filters", active_datanames = active_corrected) |
40 | 86x |
datasets()$ui_active(session$ns("filters"), active_datanames = active_corrected) |
41 |
} |
|
42 |
}) |
|
43 |
}) |
|
44 | ||
45 | 86x |
trigger_data <- .observe_active_filter_changed(datasets, is_active, active_corrected, data) |
46 | ||
47 | 86x |
eventReactive(trigger_data(), { |
48 | 89x |
.make_filtered_teal_data(modules, data = data(), datasets = datasets(), datanames = active_corrected()) |
49 |
}) |
|
50 |
}) |
|
51 |
} |
|
52 | ||
53 |
#' @rdname module_filter_data |
|
54 |
.make_filtered_teal_data <- function(modules, data, datasets = NULL, datanames) { |
|
55 | 89x |
data <- eval_code( |
56 | 89x |
data, |
57 | 89x |
paste0( |
58 | 89x |
".raw_data <- list2env(list(", |
59 | 89x |
toString(sprintf("%1$s = %1$s", sapply(datanames, as.name))), |
60 | 89x |
"))\n", |
61 | 89x |
"lockEnvironment(.raw_data) # @linksto .raw_data" # this is environment and it is shared by qenvs. CAN'T MODIFY! |
62 |
) |
|
63 |
) |
|
64 | 89x |
filtered_code <- .get_filter_expr(datasets = datasets, datanames = datanames) |
65 | 89x |
filtered_teal_data <- .append_evaluated_code(data, filtered_code) |
66 | 89x |
filtered_datasets <- sapply(datanames, function(x) datasets$get_data(x, filtered = TRUE), simplify = FALSE) |
67 | 89x |
filtered_teal_data <- .append_modified_data(filtered_teal_data, filtered_datasets) |
68 | 89x |
filtered_teal_data |
69 |
} |
|
70 | ||
71 |
#' @rdname module_filter_data |
|
72 |
.observe_active_filter_changed <- function(datasets, is_active, active_datanames, data) { |
|
73 | 86x |
previous_signature <- reactiveVal(NULL) |
74 | 86x |
filter_changed <- reactive({ |
75 | 195x |
req(inherits(datasets(), "FilteredData")) |
76 | 195x |
new_signature <- c( |
77 | 195x |
teal.code::get_code(data()), |
78 | 195x |
.get_filter_expr(datasets = datasets(), datanames = active_datanames()) |
79 |
) |
|
80 | 195x |
if (!identical(previous_signature(), new_signature)) { |
81 | 94x |
previous_signature(new_signature) |
82 | 94x |
TRUE |
83 |
} else { |
|
84 | 101x |
FALSE |
85 |
} |
|
86 |
}) |
|
87 | ||
88 | 86x |
trigger_data <- reactiveVal(NULL) |
89 | 86x |
observe({ |
90 | 208x |
if (isTRUE(is_active() && filter_changed())) { |
91 | 94x |
isolate({ |
92 | 94x |
if (is.null(trigger_data())) { |
93 | 86x |
trigger_data(0) |
94 |
} else { |
|
95 | 8x |
trigger_data(trigger_data() + 1) |
96 |
} |
|
97 |
}) |
|
98 |
} |
|
99 |
}) |
|
100 | ||
101 | 86x |
trigger_data |
102 |
} |
|
103 | ||
104 |
#' @rdname module_filter_data |
|
105 |
.get_filter_expr <- function(datasets, datanames) { |
|
106 | 284x |
if (length(datanames)) { |
107 | 278x |
teal.slice::get_filter_expr(datasets = datasets, datanames = datanames) |
108 |
} else { |
|
109 | 6x |
NULL |
110 |
} |
|
111 |
} |
1 |
#' `teal` main module |
|
2 |
#' |
|
3 |
#' @description |
|
4 |
#' `r lifecycle::badge("stable")` |
|
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` |
|
7 |
#' automatically, which is not a case when calling `ui/srv_teal` directly. |
|
8 |
#' |
|
9 |
#' @details |
|
10 |
#' |
|
11 |
#' Module is responsible for creating the main `shiny` app layout and initializing all the necessary |
|
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 |
|
14 |
#' possibly keeps all input settings the same so the user can continue where one left off. |
|
15 |
#' |
|
16 |
#' ## data flow in `teal` application |
|
17 |
#' |
|
18 |
#' This module supports multiple data inputs but eventually, they are all converted to `reactive` |
|
19 |
#' returning `teal_data` in this module. On this `reactive teal_data` object several actions are |
|
20 |
#' performed: |
|
21 |
#' - data loading in [`module_init_data`] |
|
22 |
#' - data filtering in [`module_filter_data`] |
|
23 |
#' - data transformation in [`module_transform_data`] |
|
24 |
#' |
|
25 |
#' ## Fallback on failure |
|
26 |
#' |
|
27 |
#' `teal` is designed in such way that app will never crash if the error is introduced in any |
|
28 |
#' custom `shiny` module provided by app developer (e.g. [teal_data_module()], [teal_transform_module()]). |
|
29 |
#' If any module returns a failing object, the app will halt the evaluation and display a warning message. |
|
30 |
#' App user should always have a chance to fix the improper input and continue without restarting the session. |
|
31 |
#' |
|
32 |
#' @rdname module_teal |
|
33 |
#' @name module_teal |
|
34 |
#' |
|
35 |
#' @inheritParams module_init_data |
|
36 |
#' @inheritParams init |
|
37 |
#' |
|
38 |
#' @return `NULL` invisibly |
|
39 |
NULL |
|
40 | ||
41 |
#' @rdname module_teal |
|
42 |
#' @export |
|
43 |
ui_teal <- function(id, |
|
44 |
modules, |
|
45 |
title = build_app_title(), |
|
46 |
header = tags$p(), |
|
47 |
footer = tags$p()) { |
|
48 | ! |
checkmate::assert_character(id, max.len = 1, any.missing = FALSE) |
49 | ! |
checkmate::assert( |
50 | ! |
.var.name = "title", |
51 | ! |
checkmate::check_string(title), |
52 | ! |
checkmate::check_multi_class(title, c("shiny.tag", "shiny.tag.list", "html")) |
53 |
) |
|
54 | ! |
checkmate::assert( |
55 | ! |
.var.name = "header", |
56 | ! |
checkmate::check_string(header), |
57 | ! |
checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html")) |
58 |
) |
|
59 | ! |
checkmate::assert( |
60 | ! |
.var.name = "footer", |
61 | ! |
checkmate::check_string(footer), |
62 | ! |
checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html")) |
63 |
) |
|
64 | ||
65 | ! |
if (is.character(title)) { |
66 | ! |
title <- build_app_title(title) |
67 |
} else { |
|
68 | ! |
validate_app_title_tag(title) |
69 |
} |
|
70 | ||
71 | ! |
if (checkmate::test_string(header)) { |
72 | ! |
header <- tags$p(header) |
73 |
} |
|
74 | ||
75 | ! |
if (checkmate::test_string(footer)) { |
76 | ! |
footer <- tags$p(footer) |
77 |
} |
|
78 | ||
79 | ! |
ns <- NS(id) |
80 | ||
81 |
# show busy icon when `shiny` session is busy computing stuff |
|
82 |
# based on https://stackoverflow.com/questions/17325521/r-shiny-display-loading-message-while-function-is-running/22475216#22475216 # nolint: line_length. |
|
83 | ! |
shiny_busy_message_panel <- conditionalPanel( |
84 | ! |
condition = "(($('html').hasClass('shiny-busy')) && (document.getElementById('shiny-notification-panel') == null))", # nolint: line_length. |
85 | ! |
tags$div( |
86 | ! |
icon("arrows-rotate", class = "fa-spin", prefer_type = "solid"), |
87 | ! |
"Computing ...", |
88 |
# CSS defined in `custom.css` |
|
89 | ! |
class = "shinybusymessage" |
90 |
) |
|
91 |
) |
|
92 | ||
93 | ! |
fluidPage( |
94 | ! |
id = id, |
95 | ! |
title = title, |
96 | ! |
theme = get_teal_bs_theme(), |
97 | ! |
include_teal_css_js(), |
98 | ! |
tags$header(header), |
99 | ! |
tags$hr(class = "my-2"), |
100 | ! |
shiny_busy_message_panel, |
101 | ! |
tags$div( |
102 | ! |
id = ns("tabpanel_wrapper"), |
103 | ! |
class = "teal-body", |
104 | ! |
ui_teal_module(id = ns("teal_modules"), modules = modules) |
105 |
), |
|
106 | ! |
tags$div( |
107 | ! |
id = ns("options_buttons"), |
108 | ! |
style = "position: absolute; right: 10px;", |
109 | ! |
ui_bookmark_panel(ns("bookmark_manager"), modules), |
110 | ! |
tags$button( |
111 | ! |
class = "btn action-button filter_hamburger", # see sidebar.css for style filter_hamburger |
112 | ! |
href = "javascript:void(0)", |
113 | ! |
onclick = sprintf("toggleFilterPanel('%s');", ns("tabpanel_wrapper")), |
114 | ! |
title = "Toggle filter panel", |
115 | ! |
icon("fas fa-bars") |
116 |
), |
|
117 | ! |
ui_snapshot_manager_panel(ns("snapshot_manager_panel")), |
118 | ! |
ui_filter_manager_panel(ns("filter_manager_panel")) |
119 |
), |
|
120 | ! |
tags$script( |
121 | ! |
HTML( |
122 | ! |
sprintf( |
123 |
" |
|
124 | ! |
$(document).ready(function() { |
125 | ! |
$('#%s').appendTo('#%s'); |
126 |
}); |
|
127 |
", |
|
128 | ! |
ns("options_buttons"), |
129 | ! |
ns("teal_modules-active_tab") |
130 |
) |
|
131 |
) |
|
132 |
), |
|
133 | ! |
tags$hr(), |
134 | ! |
tags$footer( |
135 | ! |
tags$div( |
136 | ! |
footer, |
137 | ! |
teal.widgets::verbatim_popup_ui(ns("sessionInfo"), "Session Info", type = "link"), |
138 | ! |
br(), |
139 | ! |
ui_teal_lockfile(ns("lockfile")), |
140 | ! |
textOutput(ns("identifier")) |
141 |
) |
|
142 |
) |
|
143 |
) |
|
144 |
} |
|
145 | ||
146 |
#' @rdname module_teal |
|
147 |
#' @export |
|
148 |
srv_teal <- function(id, data, modules, filter = teal_slices()) { |
|
149 | 89x |
checkmate::assert_character(id, max.len = 1, any.missing = FALSE) |
150 | 89x |
checkmate::assert_multi_class(data, c("teal_data", "teal_data_module", "reactive")) |
151 | 88x |
checkmate::assert_class(modules, "teal_modules") |
152 | 88x |
checkmate::assert_class(filter, "teal_slices") |
153 | ||
154 | 88x |
moduleServer(id, function(input, output, session) { |
155 | 88x |
logger::log_debug("srv_teal initializing.") |
156 | ||
157 | 88x |
if (getOption("teal.show_js_log", default = FALSE)) { |
158 | ! |
shinyjs::showLog() |
159 |
} |
|
160 | ||
161 | 88x |
srv_teal_lockfile("lockfile") |
162 | ||
163 | 88x |
output$identifier <- renderText( |
164 | 88x |
paste0("Pid:", Sys.getpid(), " Token:", substr(session$token, 25, 32)) |
165 |
) |
|
166 | ||
167 | 88x |
teal.widgets::verbatim_popup_srv( |
168 | 88x |
"sessionInfo", |
169 | 88x |
verbatim_content = utils::capture.output(utils::sessionInfo()), |
170 | 88x |
title = "SessionInfo" |
171 |
) |
|
172 | ||
173 |
# `JavaScript` code |
|
174 | 88x |
run_js_files(files = "init.js") |
175 | ||
176 |
# set timezone in shiny app |
|
177 |
# timezone is set in the early beginning so it will be available also |
|
178 |
# for `DDL` and all shiny modules |
|
179 | 88x |
get_client_timezone(session$ns) |
180 | 88x |
observeEvent( |
181 | 88x |
eventExpr = input$timezone, |
182 | 88x |
once = TRUE, |
183 | 88x |
handlerExpr = { |
184 | ! |
session$userData$timezone <- input$timezone |
185 | ! |
logger::log_debug("srv_teal@1 Timezone set to client's timezone: { input$timezone }.") |
186 |
} |
|
187 |
) |
|
188 | ||
189 | 88x |
data_handled <- srv_init_data("data", data = data) |
190 | ||
191 | 87x |
validate_ui <- tags$div( |
192 | 87x |
id = session$ns("validate_messages"), |
193 | 87x |
class = "teal_validated", |
194 | 87x |
ui_check_class_teal_data(session$ns("class_teal_data")), |
195 | 87x |
ui_validate_error(session$ns("silent_error")), |
196 | 87x |
ui_check_module_datanames(session$ns("datanames_warning")) |
197 |
) |
|
198 | 87x |
srv_check_class_teal_data("class_teal_data", data_handled) |
199 | 87x |
srv_validate_error("silent_error", data_handled, validate_shiny_silent_error = FALSE) |
200 | 87x |
srv_check_module_datanames("datanames_warning", data_handled, modules) |
201 | ||
202 | 87x |
data_validated <- .trigger_on_success(data_handled) |
203 | ||
204 | 87x |
data_signatured <- reactive({ |
205 | 152x |
req(inherits(data_validated(), "teal_data")) |
206 | 75x |
is_filter_ok <- check_filter_datanames(filter, names(data_validated())) |
207 | 75x |
if (!isTRUE(is_filter_ok)) { |
208 | 2x |
showNotification( |
209 | 2x |
"Some filters were not applied because of incompatibility with data. Contact app developer.", |
210 | 2x |
type = "warning", |
211 | 2x |
duration = 10 |
212 |
) |
|
213 | 2x |
warning(is_filter_ok) |
214 |
} |
|
215 | 75x |
.add_signature_to_data(data_validated()) |
216 |
}) |
|
217 | ||
218 | 87x |
data_load_status <- reactive({ |
219 | 80x |
if (inherits(data_handled(), "teal_data")) { |
220 | 75x |
"ok" |
221 | 5x |
} else if (inherits(data, "teal_data_module")) { |
222 | 5x |
"teal_data_module failed" |
223 |
} else { |
|
224 | ! |
"external failed" |
225 |
} |
|
226 |
}) |
|
227 | ||
228 | 87x |
datasets_rv <- if (!isTRUE(attr(filter, "module_specific"))) { |
229 | 76x |
eventReactive(data_signatured(), { |
230 | 66x |
req(inherits(data_signatured(), "teal_data")) |
231 | 66x |
logger::log_debug("srv_teal@1 initializing FilteredData") |
232 | 66x |
teal_data_to_filtered_data(data_signatured()) |
233 |
}) |
|
234 |
} |
|
235 | ||
236 | ||
237 | ||
238 | 87x |
if (inherits(data, "teal_data_module")) { |
239 | 9x |
setBookmarkExclude(c("teal_modules-active_tab")) |
240 | 9x |
shiny::insertTab( |
241 | 9x |
inputId = "teal_modules-active_tab", |
242 | 9x |
position = "before", |
243 | 9x |
select = TRUE, |
244 | 9x |
tabPanel( |
245 | 9x |
title = icon("fas fa-database"), |
246 | 9x |
value = "teal_data_module", |
247 | 9x |
tags$div( |
248 | 9x |
ui_init_data(session$ns("data")), |
249 | 9x |
validate_ui |
250 |
) |
|
251 |
) |
|
252 |
) |
|
253 | ||
254 | 9x |
if (attr(data, "once")) { |
255 | 9x |
observeEvent(data_signatured(), once = TRUE, { |
256 | 4x |
logger::log_debug("srv_teal@2 removing data tab.") |
257 |
# when once = TRUE we pull data once and then remove data tab |
|
258 | 4x |
removeTab("teal_modules-active_tab", target = "teal_data_module") |
259 |
}) |
|
260 |
} |
|
261 |
} else { |
|
262 |
# when no teal_data_module then we want to display messages above tabsetPanel (because there is no data-tab) |
|
263 | 78x |
insertUI( |
264 | 78x |
selector = sprintf("#%s", session$ns("tabpanel_wrapper")), |
265 | 78x |
where = "beforeBegin", |
266 | 78x |
ui = tags$div(validate_ui, tags$br()) |
267 |
) |
|
268 |
} |
|
269 | ||
270 | 87x |
module_labels <- unlist(module_labels(modules), use.names = FALSE) |
271 | 87x |
slices_global <- methods::new(".slicesGlobal", filter, module_labels) |
272 | 87x |
modules_output <- srv_teal_module( |
273 | 87x |
id = "teal_modules", |
274 | 87x |
data = data_signatured, |
275 | 87x |
datasets = datasets_rv, |
276 | 87x |
modules = modules, |
277 | 87x |
slices_global = slices_global, |
278 | 87x |
data_load_status = data_load_status |
279 |
) |
|
280 | 87x |
mapping_table <- srv_filter_manager_panel("filter_manager_panel", slices_global = slices_global) |
281 | 87x |
snapshots <- srv_snapshot_manager_panel("snapshot_manager_panel", slices_global = slices_global) |
282 | 87x |
srv_bookmark_panel("bookmark_manager", modules) |
283 |
}) |
|
284 | ||
285 | 87x |
invisible(NULL) |
286 |
} |
1 |
#' Filter settings for `teal` applications |
|
2 |
#' |
|
3 |
#' Specify initial filter states and filtering settings for a `teal` app. |
|
4 |
#' |
|
5 |
#' Produces a `teal_slices` object. |
|
6 |
#' 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 |
#' See argument descriptions for details. |
|
9 |
#' |
|
10 |
#' @inheritParams teal.slice::teal_slices |
|
11 |
#' |
|
12 |
#' @param module_specific (`logical(1)`) optional, |
|
13 |
#' - `FALSE` (default) when one filter panel applied to all modules. |
|
14 |
#' All filters will be shared by all modules. |
|
15 |
#' - `TRUE` when filter panel module-specific. |
|
16 |
#' Modules can have different set of filters specified - see `mapping` argument. |
|
17 |
#' @param mapping `r lifecycle::badge("experimental")` |
|
18 |
#' _This is a new feature. Do kindly share your opinions on |
|
19 |
#' [`teal`'s GitHub repository](https://github.com/insightsengineering/teal/)._ |
|
20 |
#' |
|
21 |
#' (named `list`) specifies which filters will be active in which modules on app start. |
|
22 |
#' Elements should contain character vector of `teal_slice` `id`s (see [`teal.slice::teal_slice`]). |
|
23 |
#' Names of the list should correspond to `teal_module` `label` set in [module()] function. |
|
24 |
#' - `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 `module_specific` is `FALSE`, only `global_filters` will be active on start. |
|
28 |
#' @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 |
#' Used for verifying snapshots uploaded from file. See `snapshot`. |
|
32 |
#' |
|
33 |
#' @param x (`list`) of lists to convert to `teal_slices` |
|
34 |
#' |
|
35 |
#' @return |
|
36 |
#' A `teal_slices` object. |
|
37 |
#' |
|
38 |
#' @seealso [`teal.slice::teal_slices`], [`teal.slice::teal_slice`], [slices_store()] |
|
39 |
#' |
|
40 |
#' @examples |
|
41 |
#' filter <- teal_slices( |
|
42 |
#' teal_slice(dataname = "iris", varname = "Species", id = "species"), |
|
43 |
#' teal_slice(dataname = "iris", varname = "Sepal.Length", id = "sepal_length"), |
|
44 |
#' teal_slice( |
|
45 |
#' dataname = "iris", id = "long_petals", title = "Long petals", expr = "Petal.Length > 5" |
|
46 |
#' ), |
|
47 |
#' 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 |
#' ) |
|
53 |
#' ) |
|
54 |
#' |
|
55 |
#' app <- init( |
|
56 |
#' data = teal_data(iris = iris, mtcars = mtcars), |
|
57 |
#' modules = list( |
|
58 |
#' module("module1"), |
|
59 |
#' module("module2") |
|
60 |
#' ), |
|
61 |
#' filter = filter |
|
62 |
#' ) |
|
63 |
#' |
|
64 |
#' if (interactive()) { |
|
65 |
#' shinyApp(app$ui, app$server) |
|
66 |
#' } |
|
67 |
#' |
|
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 | 170x |
shiny::isolate({ |
78 | 170x |
checkmate::assert_flag(allow_add) |
79 | 170x |
checkmate::assert_flag(module_specific) |
80 | 53x |
if (!missing(mapping)) checkmate::assert_list(mapping, types = c("character", "NULL"), names = "named") |
81 | 167x |
checkmate::assert_string(app_id, null.ok = TRUE) |
82 | ||
83 | 167x |
slices <- list(...) |
84 | 167x |
all_slice_id <- vapply(slices, `[[`, character(1L), "id") |
85 | ||
86 | 167x |
if (missing(mapping)) { |
87 | 117x |
mapping <- if (length(all_slice_id)) { |
88 | 26x |
list(global_filters = all_slice_id) |
89 |
} else { |
|
90 | 91x |
list() |
91 |
} |
|
92 |
} |
|
93 | ||
94 | 167x |
if (!module_specific) { |
95 | 148x |
mapping[setdiff(names(mapping), "global_filters")] <- NULL |
96 |
} |
|
97 | ||
98 | 167x |
failed_slice_id <- setdiff(unlist(mapping), all_slice_id) |
99 | 167x |
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 | 166x |
tss <- teal.slice::teal_slices( |
108 |
..., |
|
109 | 166x |
exclude_varnames = exclude_varnames, |
110 | 166x |
include_varnames = include_varnames, |
111 | 166x |
count_type = count_type, |
112 | 166x |
allow_add = allow_add |
113 |
) |
|
114 | 166x |
attr(tss, "mapping") <- mapping |
115 | 166x |
attr(tss, "module_specific") <- module_specific |
116 | 166x |
attr(tss, "app_id") <- app_id |
117 | 166x |
class(tss) <- c("modules_teal_slices", class(tss)) |
118 | 166x |
tss |
119 |
}) |
|
120 |
} |
|
121 | ||
122 | ||
123 |
#' @rdname teal_slices |
|
124 |
#' @export |
|
125 |
#' @keywords internal |
|
126 |
#' |
|
127 |
as.teal_slices <- function(x) { # nolint: object_name. |
|
128 | 15x |
checkmate::assert_list(x) |
129 | 15x |
lapply(x, checkmate::assert_list, names = "named", .var.name = "list element") |
130 | ||
131 | 15x |
attrs <- attributes(unclass(x)) |
132 | 15x |
ans <- lapply(x, function(x) if (is.teal_slice(x)) x else as.teal_slice(x)) |
133 | 15x |
do.call(teal_slices, c(ans, attrs)) |
134 |
} |
|
135 | ||
136 | ||
137 |
#' @rdname teal_slices |
|
138 |
#' @export |
|
139 |
#' @keywords internal |
|
140 |
#' |
|
141 |
c.teal_slices <- function(...) { |
|
142 | 6x |
x <- list(...) |
143 | 6x |
checkmate::assert_true(all(vapply(x, is.teal_slices, logical(1L))), .var.name = "all arguments are teal_slices") |
144 | ||
145 | 6x |
all_attributes <- lapply(x, attributes) |
146 | 6x |
all_attributes <- coalesce_r(all_attributes) |
147 | 6x |
all_attributes <- all_attributes[names(all_attributes) != "class"] |
148 | ||
149 | 6x |
do.call( |
150 | 6x |
teal_slices, |
151 | 6x |
c( |
152 | 6x |
unique(unlist(x, recursive = FALSE)), |
153 | 6x |
all_attributes |
154 |
) |
|
155 |
) |
|
156 |
} |
|
157 | ||
158 | ||
159 |
#' Deep copy `teal_slices` |
|
160 |
#' |
|
161 |
#' it's important to create a new copy of `teal_slices` when |
|
162 |
#' starting a new `shiny` session. Otherwise, object will be shared |
|
163 |
#' by multiple users as it is created in global environment before |
|
164 |
#' `shiny` session starts. |
|
165 |
#' @param filter (`teal_slices`) |
|
166 |
#' @return `teal_slices` |
|
167 |
#' @keywords internal |
|
168 |
deep_copy_filter <- function(filter) { |
|
169 | 1x |
checkmate::assert_class(filter, "teal_slices") |
170 | 1x |
shiny::isolate({ |
171 | 1x |
filter_copy <- lapply(filter, function(slice) { |
172 | 2x |
teal.slice::as.teal_slice(as.list(slice)) |
173 |
}) |
|
174 | 1x |
attributes(filter_copy) <- attributes(filter) |
175 | 1x |
filter_copy |
176 |
}) |
|
177 |
} |
1 |
#' Data module for `teal` transformations and output customization |
|
2 |
#' |
|
3 |
#' @description |
|
4 |
#' `r lifecycle::badge("experimental")` |
|
5 |
#' |
|
6 |
#' `teal_transform_module` provides a `shiny` module that enables data transformations within a `teal` application |
|
7 |
#' and allows for customization of outputs generated by modules. |
|
8 |
#' |
|
9 |
#' # Transforming Module Inputs in `teal` |
|
10 |
#' |
|
11 |
#' Data transformations occur after data has been filtered in `teal`. |
|
12 |
#' The transformed data is then passed to the `server` of [`teal_module()`] and managed by `teal`'s internal processes. |
|
13 |
#' The primary advantage of `teal_transform_module` over custom modules is in its error handling, where all warnings and |
|
14 |
#' errors are managed by `teal`, allowing developers to focus on transformation logic. |
|
15 |
#' |
|
16 |
#' For more details, see the vignette: `vignette("data-transform-as-shiny-module", package = "teal")`. |
|
17 |
#' |
|
18 |
#' # Customizing Module Outputs |
|
19 |
#' |
|
20 |
#' `teal_transform_module` also allows developers to modify any object created within [`teal.data::teal_data`]. |
|
21 |
#' This means you can use it to customize not only datasets but also tables, listings, and graphs. |
|
22 |
#' Some [`teal_modules`] permit developers to inject custom `shiny` modules to enhance displayed outputs. |
|
23 |
#' To manage these `decorators` within your module, use [`ui_transform_teal_data()`] and [`srv_transform_teal_data()`]. |
|
24 |
#' (For further guidance on managing decorators, refer to `ui_args` and `srv_args` in the vignette documentation.) |
|
25 |
#' |
|
26 |
#' See the vignette `vignette("decorate-modules-output", package = "teal")` for additional examples. |
|
27 |
#' |
|
28 |
#' # `server` as a language |
|
29 |
#' |
|
30 |
#' The `server` function in `teal_transform_module` must return a reactive [`teal.data::teal_data`] object. |
|
31 |
#' For simple transformations without complex reactivity, the `server` function might look like this:s |
|
32 |
#' |
|
33 |
#' ``` |
|
34 |
#' function(id, data) { |
|
35 |
#' moduleServer(id, function(input, output, session) { |
|
36 |
#' reactive({ |
|
37 |
#' within( |
|
38 |
#' data(), |
|
39 |
#' expr = x <- subset(x, col == level), |
|
40 |
#' level = input$level |
|
41 |
#' ) |
|
42 |
#' }) |
|
43 |
#' }) |
|
44 |
#' } |
|
45 |
#' ``` |
|
46 |
#' |
|
47 |
#' The example above can be simplified using `make_teal_transform_server`, where `level` is automatically matched to the |
|
48 |
#' corresponding `input` parameter: |
|
49 |
#' |
|
50 |
#' ``` |
|
51 |
#' make_teal_transform_server(expr = expression(x <- subset(x, col == level))) |
|
52 |
#' ``` |
|
53 |
#' @inheritParams teal_data_module |
|
54 |
#' @param server (`function(id, data)` or `expression`) |
|
55 |
#' A `shiny` module server function that takes `id` and `data` as arguments, where `id` is the module id and `data` |
|
56 |
#' is the reactive `teal_data` input. The `server` function must return a reactive expression containing a `teal_data` |
|
57 |
#' object. For simplified syntax, use [`make_teal_transform_server()`]. |
|
58 |
#' @param datanames (`character`) |
|
59 |
#' Specifies the names of datasets relevant to the module. Only filters for the specified `datanames` will be displayed |
|
60 |
#' in the filter panel. The keyword `"all"` can be used to display filters for all datasets. `datanames` are |
|
61 |
#' automatically appended to the [`modules()`] `datanames`. |
|
62 |
#' |
|
63 |
#' |
|
64 |
#' @examples |
|
65 |
#' data_transformators <- list( |
|
66 |
#' teal_transform_module( |
|
67 |
#' label = "Static transformator for iris", |
|
68 |
#' datanames = "iris", |
|
69 |
#' server = function(id, data) { |
|
70 |
#' moduleServer(id, function(input, output, session) { |
|
71 |
#' reactive({ |
|
72 |
#' within(data(), { |
|
73 |
#' iris <- head(iris, 5) |
|
74 |
#' }) |
|
75 |
#' }) |
|
76 |
#' }) |
|
77 |
#' } |
|
78 |
#' ), |
|
79 |
#' teal_transform_module( |
|
80 |
#' label = "Interactive transformator for iris", |
|
81 |
#' datanames = "iris", |
|
82 |
#' ui = function(id) { |
|
83 |
#' ns <- NS(id) |
|
84 |
#' tags$div( |
|
85 |
#' numericInput(ns("n_cols"), "Show n columns", value = 5, min = 1, max = 5, step = 1) |
|
86 |
#' ) |
|
87 |
#' }, |
|
88 |
#' server = function(id, data) { |
|
89 |
#' moduleServer(id, function(input, output, session) { |
|
90 |
#' reactive({ |
|
91 |
#' within(data(), |
|
92 |
#' { |
|
93 |
#' iris <- iris[, 1:n_cols] |
|
94 |
#' }, |
|
95 |
#' n_cols = input$n_cols |
|
96 |
#' ) |
|
97 |
#' }) |
|
98 |
#' }) |
|
99 |
#' } |
|
100 |
#' ) |
|
101 |
#' ) |
|
102 |
#' |
|
103 |
#' output_decorator <- teal_transform_module( |
|
104 |
#' server = make_teal_transform_server( |
|
105 |
#' expression( |
|
106 |
#' object <- rev(object) |
|
107 |
#' ) |
|
108 |
#' ) |
|
109 |
#' ) |
|
110 |
#' |
|
111 |
#' app <- init( |
|
112 |
#' data = teal_data(iris = iris), |
|
113 |
#' modules = example_module( |
|
114 |
#' transformators = data_transformators, |
|
115 |
#' decorators = list(output_decorator) |
|
116 |
#' ) |
|
117 |
#' ) |
|
118 |
#' if (interactive()) { |
|
119 |
#' shinyApp(app$ui, app$server) |
|
120 |
#' } |
|
121 |
#' |
|
122 |
#' @name teal_transform_module |
|
123 |
#' |
|
124 |
#' @export |
|
125 |
teal_transform_module <- function(ui = NULL, |
|
126 |
server = function(id, data) data, |
|
127 |
label = "transform module", |
|
128 |
datanames = "all") { |
|
129 | 25x |
structure( |
130 | 25x |
list( |
131 | 25x |
ui = ui, |
132 | 25x |
server = function(id, data) { |
133 | 26x |
data_out <- server(id, data) |
134 | ||
135 | 26x |
if (inherits(data_out, "reactive.event")) { |
136 |
# This warning message partially detects when `eventReactive` is used in `data_module`. |
|
137 | 1x |
warning( |
138 | 1x |
"teal_transform_module() ", |
139 | 1x |
"Using eventReactive in teal_transform module server code should be avoided as it ", |
140 | 1x |
"may lead to unexpected behavior. See the vignettes for more information ", |
141 | 1x |
"(`vignette(\"data-transform-as-shiny-module\", package = \"teal\")`).", |
142 | 1x |
call. = FALSE |
143 |
) |
|
144 |
} |
|
145 | ||
146 | ||
147 | 26x |
decorate_err_msg( |
148 | 26x |
assert_reactive(data_out), |
149 | 26x |
pre = sprintf("From: 'teal_transform_module()':\nA 'teal_transform_module' with \"%s\" label:", label), |
150 | 26x |
post = "Please make sure that this module returns a 'reactive` object containing 'teal_data' class of object." # nolint: line_length_linter. |
151 |
) |
|
152 |
} |
|
153 |
), |
|
154 | 25x |
label = label, |
155 | 25x |
datanames = datanames, |
156 | 25x |
class = c("teal_transform_module", "teal_data_module") |
157 |
) |
|
158 |
} |
|
159 | ||
160 |
#' Make teal_transform_module's server |
|
161 |
#' |
|
162 |
#' A factory function to simplify creation of a [`teal_transform_module`]'s server. Specified `expr` |
|
163 |
#' is wrapped in a shiny module function and output can be passed to the `server` argument in |
|
164 |
#' [teal_transform_module()] call. Such a server function can be linked with ui and values from the |
|
165 |
#' inputs can be used in the expression. Object names specified in the expression will be substituted |
|
166 |
#' with the value of the respective input (matched by the name) - for example in |
|
167 |
#' `expression(graph <- graph + ggtitle(title))` object `title` will be replaced with the value of |
|
168 |
#' `input$title`. |
|
169 |
#' @param expr (`language`) |
|
170 |
#' An R call which will be evaluated within [`teal.data::teal_data`] environment. |
|
171 |
#' @return `function(id, data)` returning `shiny` module |
|
172 |
#' @examples |
|
173 |
#' |
|
174 |
#' trim_iris <- teal_transform_module( |
|
175 |
#' label = "Simplified interactive transformator for iris", |
|
176 |
#' datanames = "iris", |
|
177 |
#' ui = function(id) { |
|
178 |
#' ns <- NS(id) |
|
179 |
#' numericInput(ns("n_rows"), "Subset n rows", value = 6, min = 1, max = 150, step = 1) |
|
180 |
#' }, |
|
181 |
#' server = make_teal_transform_server(expression(iris <- head(iris, n_rows))) |
|
182 |
#' ) |
|
183 |
#' |
|
184 |
#' app <- init( |
|
185 |
#' data = teal_data(iris = iris), |
|
186 |
#' modules = example_module(transformators = trim_iris) |
|
187 |
#' ) |
|
188 |
#' if (interactive()) { |
|
189 |
#' shinyApp(app$ui, app$server) |
|
190 |
#' } |
|
191 |
#' |
|
192 |
#' @export |
|
193 |
make_teal_transform_server <- function(expr) { |
|
194 | 3x |
if (is.call(expr)) { |
195 | 1x |
expr <- as.expression(expr) |
196 |
} |
|
197 | 3x |
checkmate::assert_multi_class(expr, c("call", "expression")) |
198 | ||
199 | 3x |
function(id, data) { |
200 | 3x |
moduleServer(id, function(input, output, session) { |
201 | 3x |
list_env <- reactive( |
202 | 3x |
lapply(rlang::set_names(names(input)), function(x) input[[x]]) |
203 |
) |
|
204 | ||
205 | 3x |
reactive({ |
206 | 4x |
call_with_inputs <- lapply(expr, function(x) { |
207 | 4x |
do.call(what = substitute, args = list(expr = x, env = list_env())) |
208 |
}) |
|
209 | 4x |
eval_code(object = data(), code = as.expression(call_with_inputs)) |
210 |
}) |
|
211 |
}) |
|
212 |
} |
|
213 |
} |
|
214 | ||
215 |
#' Extract all `transformators` from `modules`. |
|
216 |
#' |
|
217 |
#' @param modules `teal_modules` or `teal_module` |
|
218 |
#' @return A list of `teal_transform_module` nested in the same way as input `modules`. |
|
219 |
#' @keywords internal |
|
220 |
extract_transformators <- function(modules) { |
|
221 | 10x |
if (inherits(modules, "teal_module")) { |
222 | 5x |
modules$transformators |
223 | 5x |
} else if (inherits(modules, "teal_modules")) { |
224 | 5x |
lapply(modules$children, extract_transformators) |
225 |
} |
|
226 |
} |
1 |
#' Filter state snapshot management |
|
2 |
#' |
|
3 |
#' Capture and restore snapshots of the global (app) filter state. |
|
4 |
#' |
|
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, |
|
7 |
#' as well as to save it to file in order to share it with an app developer or other users, |
|
8 |
#' who in turn can upload it to their own session. |
|
9 |
#' |
|
10 |
#' The snapshot manager is accessed with the camera icon in the tabset bar. |
|
11 |
#' At the beginning of a session it presents three icons: a camera, an upload, and an circular arrow. |
|
12 |
#' Clicking the camera captures a snapshot, clicking the upload adds a snapshot from a file |
|
13 |
#' and applies the filter states therein, and clicking the arrow resets initial application state. |
|
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 |
#' |
|
16 |
#' @section Server logic: |
|
17 |
#' Snapshots are basically `teal_slices` objects, however, since each module is served by a separate instance |
|
18 |
#' of `FilteredData` and these objects require shared state, `teal_slice` is a `reactiveVal` so `teal_slices` |
|
19 |
#' cannot be stored as is. Therefore, `teal_slices` are reversibly converted to a list of lists representation |
|
20 |
#' (attributes are maintained). |
|
21 |
#' |
|
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. |
|
24 |
#' |
|
25 |
#' For every snapshot except the initial one, a piece of UI is generated that contains |
|
26 |
#' the snapshot name, a select button to restore that snapshot, and a save button to save it to a file. |
|
27 |
#' The initial snapshot is restored by a separate "reset" button. |
|
28 |
#' It cannot be saved directly but a user is welcome to capture the initial state as a snapshot and save that. |
|
29 |
#' |
|
30 |
#' @section Snapshot mechanics: |
|
31 |
#' When a snapshot is captured, the user is prompted to name it. |
|
32 |
#' Names are displayed as is but since they are used to create button ids, |
|
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. |
|
36 |
#' |
|
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. |
|
39 |
#' Once a name has been accepted, `slices_global` is converted to a list of lists - a snapshot. |
|
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, |
|
42 |
#' so `mapping_matrix` is transformed to obtain the current mapping, i.e. a list that, |
|
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. |
|
45 |
#' |
|
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 |
|
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`. |
|
50 |
#' |
|
51 |
#' To save a snapshot, the snapshot is retrieved and reassembled just like for restoring, |
|
52 |
#' and then saved to file with [slices_store()]. |
|
53 |
#' |
|
54 |
#' When a snapshot is uploaded, it will first be added to storage just like a newly created one, |
|
55 |
#' and then used to restore app state much like a snapshot taken from storage. |
|
56 |
#' Upon clicking the upload icon the user will be prompted for a file to upload |
|
57 |
#' and may choose to name the new snapshot. The name defaults to the name of the file (the extension is dropped) |
|
58 |
#' and normal naming rules apply. Loading the file yields a `teal_slices` object, |
|
59 |
#' which is disassembled for storage and used directly for restoring app state. |
|
60 |
#' |
|
61 |
#' @section Transferring snapshots: |
|
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 |
|
66 |
#' of the current app state and only if the match is the snapshot admitted to the session. |
|
67 |
#' |
|
68 |
#' @section Bookmarks: |
|
69 |
#' An `onBookmark` callback creates a snapshot of the current filter state. |
|
70 |
#' This is done on the app session, not the module session. |
|
71 |
#' (The snapshot will be retrieved by `module_teal` in order to set initial app state in a restored app.) |
|
72 |
#' Then that snapshot, and the previous snapshot history are dumped into the `values.rds` file in `<bookmark_dir>`. |
|
73 |
#' |
|
74 |
#' @param id (`character(1)`) `shiny` module instance id. |
|
75 |
#' @param slices_global (`reactiveVal`) that contains a `teal_slices` object |
|
76 |
#' containing all `teal_slice`s existing in the app, both active and inactive. |
|
77 |
#' |
|
78 |
#' @return `list` containing the snapshot history, where each element is an unlisted `teal_slices` object. |
|
79 |
#' |
|
80 |
#' @name module_snapshot_manager |
|
81 |
#' @rdname module_snapshot_manager |
|
82 |
#' |
|
83 |
#' @author Aleksander Chlebowski |
|
84 |
#' @keywords internal |
|
85 |
NULL |
|
86 | ||
87 |
#' @rdname module_snapshot_manager |
|
88 |
ui_snapshot_manager_panel <- function(id) { |
|
89 | ! |
ns <- NS(id) |
90 | ! |
tags$button( |
91 | ! |
id = ns("show_snapshot_manager"), |
92 | ! |
class = "btn action-button wunder_bar_button", |
93 | ! |
title = "View filter mapping", |
94 | ! |
suppressMessages(icon("fas fa-camera")) |
95 |
) |
|
96 |
} |
|
97 | ||
98 |
#' @rdname module_snapshot_manager |
|
99 |
srv_snapshot_manager_panel <- function(id, slices_global) { |
|
100 | 87x |
moduleServer(id, function(input, output, session) { |
101 | 87x |
logger::log_debug("srv_snapshot_manager_panel initializing") |
102 | 87x |
setBookmarkExclude(c("show_snapshot_manager")) |
103 | 87x |
observeEvent(input$show_snapshot_manager, { |
104 | ! |
logger::log_debug("srv_snapshot_manager_panel@1 show_snapshot_manager button has been clicked.") |
105 | ! |
showModal( |
106 | ! |
modalDialog( |
107 | ! |
ui_snapshot_manager(session$ns("module")), |
108 | ! |
class = "snapshot_manager_modal", |
109 | ! |
size = "m", |
110 | ! |
footer = NULL, |
111 | ! |
easyClose = TRUE |
112 |
) |
|
113 |
) |
|
114 |
}) |
|
115 | 87x |
srv_snapshot_manager("module", slices_global = slices_global) |
116 |
}) |
|
117 |
} |
|
118 | ||
119 |
#' @rdname module_snapshot_manager |
|
120 |
ui_snapshot_manager <- function(id) { |
|
121 | ! |
ns <- NS(id) |
122 | ! |
tags$div( |
123 | ! |
class = "manager_content", |
124 | ! |
tags$div( |
125 | ! |
class = "manager_table_row", |
126 | ! |
tags$span(tags$b("Snapshot manager")), |
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 |
131 |
), |
|
132 | ! |
uiOutput(ns("snapshot_list")) |
133 |
) |
|
134 |
} |
|
135 | ||
136 |
#' @rdname module_snapshot_manager |
|
137 |
srv_snapshot_manager <- function(id, slices_global) { |
|
138 | 87x |
checkmate::assert_character(id) |
139 | ||
140 | 87x |
moduleServer(id, function(input, output, session) { |
141 | 87x |
logger::log_debug("srv_snapshot_manager initializing") |
142 | ||
143 |
# Set up bookmarking callbacks ---- |
|
144 |
# Register bookmark exclusions (all buttons and text fields). |
|
145 | 87x |
setBookmarkExclude(c( |
146 | 87x |
"snapshot_add", "snapshot_load", "snapshot_reset", |
147 | 87x |
"snapshot_name_accept", "snaphot_file_accept", |
148 | 87x |
"snapshot_name", "snapshot_file" |
149 |
)) |
|
150 |
# Add snapshot history to bookmark. |
|
151 | 87x |
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? |
154 |
}) |
|
155 | ||
156 | 87x |
ns <- session$ns |
157 | ||
158 |
# Track global filter states ---- |
|
159 | 87x |
snapshot_history <- reactiveVal({ |
160 |
# Restore directly from bookmarked state, if applicable. |
|
161 | 87x |
restoreValue( |
162 | 87x |
ns("snapshot_history"), |
163 | 87x |
list("Initial application state" = shiny::isolate(as.list(slices_global$all_slices(), recursive = TRUE))) |
164 |
) |
|
165 |
}) |
|
166 | ||
167 |
# Snapshot current application state ---- |
|
168 |
# Name snaphsot. |
|
169 | 87x |
observeEvent(input$snapshot_add, { |
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"), |
174 | ! |
footer = tagList( |
175 | ! |
actionButton(ns("snapshot_name_accept"), "Accept", icon = icon("far fa-thumbs-up")), |
176 | ! |
modalButton(label = "Cancel", icon = icon("far fa-thumbs-down")) |
177 |
), |
|
178 | ! |
size = "s" |
179 |
) |
|
180 |
) |
|
181 |
}) |
|
182 |
# Store snaphsot. |
|
183 | 87x |
observeEvent(input$snapshot_name_accept, { |
184 | ! |
logger::log_debug("srv_snapshot_manager: snapshot_name_accept button clicked") |
185 | ! |
snapshot_name <- trimws(input$snapshot_name) |
186 | ! |
if (identical(snapshot_name, "")) { |
187 | ! |
logger::log_debug("srv_snapshot_manager: snapshot name rejected") |
188 | ! |
showNotification( |
189 | ! |
"Please name the snapshot.", |
190 | ! |
type = "message" |
191 |
) |
|
192 | ! |
updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name") |
193 | ! |
} else if (is.element(make.names(snapshot_name), make.names(names(snapshot_history())))) { |
194 | ! |
logger::log_debug("srv_snapshot_manager: snapshot name rejected") |
195 | ! |
showNotification( |
196 | ! |
"This name is in conflict with other snapshot names. Please choose a different one.", |
197 | ! |
type = "message" |
198 |
) |
|
199 | ! |
updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name") |
200 |
} else { |
|
201 | ! |
logger::log_debug("srv_snapshot_manager: snapshot name accepted, adding snapshot") |
202 | ! |
snapshot <- as.list(slices_global$all_slices(), recursive = TRUE) |
203 | ! |
snapshot_update <- c(snapshot_history(), list(snapshot)) |
204 | ! |
names(snapshot_update)[length(snapshot_update)] <- snapshot_name |
205 | ! |
snapshot_history(snapshot_update) |
206 | ! |
removeModal() |
207 |
# Reopen filter manager modal by clicking button in the main application. |
|
208 | ! |
shinyjs::click(id = "teal-wunder_bar-show_snapshot_manager", asis = TRUE) |
209 |
} |
|
210 |
}) |
|
211 | ||
212 |
# Upload a snapshot file ---- |
|
213 |
# Select file. |
|
214 | 87x |
observeEvent(input$snapshot_load, { |
215 | ! |
logger::log_debug("srv_snapshot_manager: snapshot_load button clicked") |
216 | ! |
showModal( |
217 | ! |
modalDialog( |
218 | ! |
fileInput(ns("snapshot_file"), "Choose snapshot file", accept = ".json", width = "100%"), |
219 | ! |
textInput( |
220 | ! |
ns("snapshot_name"), |
221 | ! |
"Name the snapshot (optional)", |
222 | ! |
width = "100%", |
223 | ! |
placeholder = "Meaningful, unique name" |
224 |
), |
|
225 | ! |
footer = tagList( |
226 | ! |
actionButton(ns("snaphot_file_accept"), "Accept", icon = icon("far fa-thumbs-up")), |
227 | ! |
modalButton(label = "Cancel", icon = icon("far fa-thumbs-down")) |
228 |
) |
|
229 |
) |
|
230 |
) |
|
231 |
}) |
|
232 |
# Store new snapshot to list and restore filter states. |
|
233 | 87x |
observeEvent(input$snaphot_file_accept, { |
234 | ! |
logger::log_debug("srv_snapshot_manager: snapshot_file_accept button clicked") |
235 | ! |
snapshot_name <- trimws(input$snapshot_name) |
236 | ! |
if (identical(snapshot_name, "")) { |
237 | ! |
logger::log_debug("srv_snapshot_manager: no snapshot name provided, naming after file") |
238 | ! |
snapshot_name <- tools::file_path_sans_ext(input$snapshot_file$name) |
239 |
} |
|
240 | ! |
if (is.element(make.names(snapshot_name), make.names(names(snapshot_history())))) { |
241 | ! |
logger::log_debug("srv_snapshot_manager: snapshot name rejected") |
242 | ! |
showNotification( |
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") |
247 |
} else { |
|
248 |
# Restore snapshot and verify app compatibility. |
|
249 | ! |
logger::log_debug("srv_snapshot_manager: snapshot name accepted, loading snapshot") |
250 | ! |
snapshot_state <- try(slices_restore(input$snapshot_file$datapath)) |
251 | ! |
if (!inherits(snapshot_state, "modules_teal_slices")) { |
252 | ! |
logger::log_debug("srv_snapshot_manager: snapshot file corrupt") |
253 | ! |
showNotification( |
254 | ! |
"File appears to be corrupt.", |
255 | ! |
type = "error" |
256 |
) |
|
257 | ! |
} else if (!identical(attr(snapshot_state, "app_id"), attr(slices_global$all_slices(), "app_id"))) { |
258 | ! |
logger::log_debug("srv_snapshot_manager: snapshot not compatible with app") |
259 | ! |
showNotification( |
260 | ! |
"This snapshot file is not compatible with the app and cannot be loaded.", |
261 | ! |
type = "warning" |
262 |
) |
|
263 |
} else { |
|
264 |
# Add to snapshot history. |
|
265 | ! |
logger::log_debug("srv_snapshot_manager: snapshot loaded, adding to history") |
266 | ! |
snapshot <- as.list(slices_global$all_slices(), recursive = TRUE) |
267 | ! |
snapshot_update <- c(snapshot_history(), list(snapshot)) |
268 | ! |
names(snapshot_update)[length(snapshot_update)] <- snapshot_name |
269 | ! |
snapshot_history(snapshot_update) |
270 |
### Begin simplified restore procedure. ### |
|
271 | ! |
logger::log_debug("srv_snapshot_manager: restoring snapshot") |
272 | ! |
slices_global$slices_set(snapshot_state) |
273 | ! |
removeModal() |
274 |
### End simplified restore procedure. ### |
|
275 |
} |
|
276 |
} |
|
277 |
}) |
|
278 |
# Apply newly added snapshot. |
|
279 | ||
280 |
# Restore initial state ---- |
|
281 | 87x |
observeEvent(input$snapshot_reset, { |
282 | 2x |
logger::log_debug("srv_snapshot_manager: snapshot_reset button clicked, restoring snapshot") |
283 | 2x |
s <- "Initial application state" |
284 |
### Begin restore procedure. ### |
|
285 | 2x |
snapshot <- snapshot_history()[[s]] |
286 | 2x |
snapshot_state <- as.teal_slices(snapshot) |
287 | 2x |
slices_global$slices_set(snapshot_state) |
288 | 2x |
removeModal() |
289 |
### End restore procedure. ### |
|
290 |
}) |
|
291 | ||
292 |
# Build snapshot table ---- |
|
293 |
# Create UI elements and server logic for the snapshot table. |
|
294 |
# Observers must be tracked to avoid duplication and excess reactivity. |
|
295 |
# Remaining elements are tracked likewise for consistency and a slight speed margin. |
|
296 | 87x |
observers <- reactiveValues() |
297 | 87x |
handlers <- reactiveValues() |
298 | 87x |
divs <- reactiveValues() |
299 | ||
300 | 87x |
observeEvent(snapshot_history(), { |
301 | 77x |
logger::log_debug("srv_snapshot_manager: snapshot history modified, updating snapshot list") |
302 | 77x |
lapply(names(snapshot_history())[-1L], function(s) { |
303 | ! |
id_pickme <- sprintf("pickme_%s", make.names(s)) |
304 | ! |
id_saveme <- sprintf("saveme_%s", make.names(s)) |
305 | ! |
id_rowme <- sprintf("rowme_%s", make.names(s)) |
306 | ||
307 |
# Observer for restoring snapshot. |
|
308 | ! |
if (!is.element(id_pickme, names(observers))) { |
309 | ! |
observers[[id_pickme]] <- observeEvent(input[[id_pickme]], { |
310 |
### Begin restore procedure. ### |
|
311 | ! |
snapshot <- snapshot_history()[[s]] |
312 | ! |
snapshot_state <- as.teal_slices(snapshot) |
313 | ||
314 | ! |
slices_global$slices_set(snapshot_state) |
315 | ! |
removeModal() |
316 |
### End restore procedure. ### |
|
317 |
}) |
|
318 |
} |
|
319 |
# Create handler for downloading snapshot. |
|
320 | ! |
if (!is.element(id_saveme, names(handlers))) { |
321 | ! |
output[[id_saveme]] <- downloadHandler( |
322 | ! |
filename = function() { |
323 | ! |
sprintf("teal_snapshot_%s_%s.json", s, Sys.Date()) |
324 |
}, |
|
325 | ! |
content = function(file) { |
326 | ! |
snapshot <- snapshot_history()[[s]] |
327 | ! |
snapshot_state <- as.teal_slices(snapshot) |
328 | ! |
slices_store(tss = snapshot_state, file = file) |
329 |
} |
|
330 |
) |
|
331 | ! |
handlers[[id_saveme]] <- id_saveme |
332 |
} |
|
333 |
# Create a row for the snapshot table. |
|
334 | ! |
if (!is.element(id_rowme, names(divs))) { |
335 | ! |
divs[[id_rowme]] <- tags$div( |
336 | ! |
class = "manager_table_row", |
337 | ! |
tags$span(tags$h5(s)), |
338 | ! |
actionLink(inputId = ns(id_pickme), label = icon("far fa-circle-check"), title = "select"), |
339 | ! |
downloadLink(outputId = ns(id_saveme), label = icon("far fa-save"), title = "save to file") |
340 |
) |
|
341 |
} |
|
342 |
}) |
|
343 |
}) |
|
344 | ||
345 |
# Create table to display list of snapshots and their actions. |
|
346 | 87x |
output$snapshot_list <- renderUI({ |
347 | 77x |
rows <- rev(reactiveValuesToList(divs)) |
348 | 77x |
if (length(rows) == 0L) { |
349 | 77x |
tags$div( |
350 | 77x |
class = "manager_placeholder", |
351 | 77x |
"Snapshots will appear here." |
352 |
) |
|
353 |
} else { |
|
354 | ! |
rows |
355 |
} |
|
356 |
}) |
|
357 | ||
358 | 87x |
snapshot_history |
359 |
}) |
|
360 |
} |
1 |
#' Calls all `modules` |
|
2 |
#' |
|
3 |
#' On the UI side each `teal_modules` is translated to a `tabsetPanel` and each `teal_module` is a |
|
4 |
#' `tabPanel`. Both, UI and server are called recursively so that each tab is a separate module and |
|
5 |
#' reflect nested structure of `modules` argument. |
|
6 |
#' |
|
7 |
#' @name module_teal_module |
|
8 |
#' |
|
9 |
#' @inheritParams module_teal |
|
10 |
#' |
|
11 |
#' @param data (`reactive` returning `teal_data`) |
|
12 |
#' |
|
13 |
#' @param slices_global (`reactiveVal` returning `modules_teal_slices`) |
|
14 |
#' see [`module_filter_manager`] |
|
15 |
#' |
|
16 |
#' @param depth (`integer(1)`) |
|
17 |
#' number which helps to determine depth of the modules nesting. |
|
18 |
#' |
|
19 |
#' @param datasets (`reactive` returning `FilteredData` or `NULL`) |
|
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". |
|
22 |
#' |
|
23 |
#' @param data_load_status (`reactive` returning `character`) |
|
24 |
#' Determines action dependent on a data loading status: |
|
25 |
#' - `"ok"` when `teal_data` is returned from the data loading. |
|
26 |
#' - `"teal_data_module failed"` when [teal_data_module()] didn't return `teal_data`. Disables tabs buttons. |
|
27 |
#' - `"external failed"` when a `reactive` passed to `srv_teal(data)` didn't return `teal_data`. Hides the whole tab |
|
28 |
#' panel. |
|
29 |
#' |
|
30 |
#' @return |
|
31 |
#' output of currently active module. |
|
32 |
#' - `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 |
#' |
|
35 |
#' @keywords internal |
|
36 |
NULL |
|
37 | ||
38 |
#' @rdname module_teal_module |
|
39 |
ui_teal_module <- function(id, modules, depth = 0L) { |
|
40 | ! |
checkmate::assert_multi_class(modules, c("teal_modules", "teal_module", "shiny.tag")) |
41 | ! |
checkmate::assert_count(depth) |
42 | ! |
UseMethod("ui_teal_module", modules) |
43 |
} |
|
44 | ||
45 |
#' @rdname module_teal_module |
|
46 |
#' @export |
|
47 |
ui_teal_module.default <- function(id, modules, depth = 0L) { |
|
48 | ! |
stop("Modules class not supported: ", paste(class(modules), collapse = " ")) |
49 |
} |
|
50 | ||
51 |
#' @rdname module_teal_module |
|
52 |
#' @export |
|
53 |
ui_teal_module.teal_modules <- function(id, modules, depth = 0L) { |
|
54 | ! |
ns <- NS(id) |
55 | ! |
tags$div( |
56 | ! |
id = ns("wrapper"), |
57 | ! |
do.call( |
58 | ! |
tabsetPanel, |
59 | ! |
c( |
60 |
# by giving an id, we can reactively respond to tab changes |
|
61 | ! |
list( |
62 | ! |
id = ns("active_tab"), |
63 | ! |
type = if (modules$label == "root") "pills" else "tabs" |
64 |
), |
|
65 | ! |
lapply( |
66 | ! |
names(modules$children), |
67 | ! |
function(module_id) { |
68 | ! |
module_label <- modules$children[[module_id]]$label |
69 | ! |
if (is.null(module_label)) { |
70 | ! |
module_label <- icon("fas fa-database") |
71 |
} |
|
72 | ! |
tabPanel( |
73 | ! |
title = module_label, |
74 | ! |
value = module_id, # when clicked this tab value changes input$<tabset panel id> |
75 | ! |
ui_teal_module( |
76 | ! |
id = ns(module_id), |
77 | ! |
modules = modules$children[[module_id]], |
78 | ! |
depth = depth + 1L |
79 |
) |
|
80 |
) |
|
81 |
} |
|
82 |
) |
|
83 |
) |
|
84 |
) |
|
85 |
) |
|
86 |
} |
|
87 | ||
88 |
#' @rdname module_teal_module |
|
89 |
#' @export |
|
90 |
ui_teal_module.teal_module <- function(id, modules, depth = 0L) { |
|
91 | ! |
ns <- NS(id) |
92 | ! |
args <- c(list(id = ns("module")), modules$ui_args) |
93 | ||
94 | ! |
ui_teal <- tagList( |
95 | ! |
shinyjs::hidden( |
96 | ! |
tags$div( |
97 | ! |
id = ns("transform_failure_info"), |
98 | ! |
class = "teal_validated", |
99 | ! |
div( |
100 | ! |
class = "teal-output-warning", |
101 | ! |
"One of transformators failed. Please check its inputs." |
102 |
) |
|
103 |
) |
|
104 |
), |
|
105 | ! |
tags$div( |
106 | ! |
id = ns("teal_module_ui"), |
107 | ! |
tags$div( |
108 | ! |
class = "teal_validated", |
109 | ! |
ui_check_module_datanames(ns("validate_datanames")) |
110 |
), |
|
111 | ! |
do.call(modules$ui, args) |
112 |
) |
|
113 |
) |
|
114 | ||
115 | ! |
div( |
116 | ! |
id = id, |
117 | ! |
class = "teal_module", |
118 | ! |
uiOutput(ns("data_reactive"), inline = TRUE), |
119 | ! |
tagList( |
120 | ! |
if (depth >= 2L) tags$div(style = "mt-6"), |
121 | ! |
if (!is.null(modules$datanames)) { |
122 | ! |
fluidRow( |
123 | ! |
column(width = 9, ui_teal, class = "teal_primary_col"), |
124 | ! |
column( |
125 | ! |
width = 3, |
126 | ! |
ui_data_summary(ns("data_summary")), |
127 | ! |
ui_filter_data(ns("filter_panel")), |
128 | ! |
ui_transform_teal_data(ns("data_transform"), transformators = modules$transformators, class = "well"), |
129 | ! |
class = "teal_secondary_col" |
130 |
) |
|
131 |
) |
|
132 |
} else { |
|
133 | ! |
ui_teal |
134 |
} |
|
135 |
) |
|
136 |
) |
|
137 |
} |
|
138 | ||
139 |
#' @rdname module_teal_module |
|
140 |
srv_teal_module <- function(id, |
|
141 |
data, |
|
142 |
modules, |
|
143 |
datasets = NULL, |
|
144 |
slices_global, |
|
145 |
reporter = teal.reporter::Reporter$new(), |
|
146 |
data_load_status = reactive("ok"), |
|
147 |
is_active = reactive(TRUE)) { |
|
148 | 199x |
checkmate::assert_string(id) |
149 | 199x |
assert_reactive(data) |
150 | 199x |
checkmate::assert_multi_class(modules, c("teal_modules", "teal_module")) |
151 | 199x |
assert_reactive(datasets, null.ok = TRUE) |
152 | 199x |
checkmate::assert_class(slices_global, ".slicesGlobal") |
153 | 199x |
checkmate::assert_class(reporter, "Reporter") |
154 | 199x |
assert_reactive(data_load_status) |
155 | 199x |
UseMethod("srv_teal_module", modules) |
156 |
} |
|
157 | ||
158 |
#' @rdname module_teal_module |
|
159 |
#' @export |
|
160 |
srv_teal_module.default <- function(id, |
|
161 |
data, |
|
162 |
modules, |
|
163 |
datasets = NULL, |
|
164 |
slices_global, |
|
165 |
reporter = teal.reporter::Reporter$new(), |
|
166 |
data_load_status = reactive("ok"), |
|
167 |
is_active = reactive(TRUE)) { |
|
168 | ! |
stop("Modules class not supported: ", paste(class(modules), collapse = " ")) |
169 |
} |
|
170 | ||
171 |
#' @rdname module_teal_module |
|
172 |
#' @export |
|
173 |
srv_teal_module.teal_modules <- function(id, |
|
174 |
data, |
|
175 |
modules, |
|
176 |
datasets = NULL, |
|
177 |
slices_global, |
|
178 |
reporter = teal.reporter::Reporter$new(), |
|
179 |
data_load_status = reactive("ok"), |
|
180 |
is_active = reactive(TRUE)) { |
|
181 | 87x |
moduleServer(id = id, module = function(input, output, session) { |
182 | 87x |
logger::log_debug("srv_teal_module.teal_modules initializing the module { deparse1(modules$label) }.") |
183 | ||
184 | 87x |
observeEvent(data_load_status(), { |
185 | 80x |
tabs_selector <- sprintf("#%s li a", session$ns("active_tab")) |
186 | 80x |
if (identical(data_load_status(), "ok")) { |
187 | 75x |
logger::log_debug("srv_teal_module@1 enabling modules tabs.") |
188 | 75x |
shinyjs::show("wrapper") |
189 | 75x |
shinyjs::enable(selector = tabs_selector) |
190 | 5x |
} else if (identical(data_load_status(), "teal_data_module failed")) { |
191 | 5x |
logger::log_debug("srv_teal_module@1 disabling modules tabs.") |
192 | 5x |
shinyjs::disable(selector = tabs_selector) |
193 | ! |
} else if (identical(data_load_status(), "external failed")) { |
194 | ! |
logger::log_debug("srv_teal_module@1 hiding modules tabs.") |
195 | ! |
shinyjs::hide("wrapper") |
196 |
} |
|
197 |
}) |
|
198 | ||
199 | 87x |
modules_output <- sapply( |
200 | 87x |
names(modules$children), |
201 | 87x |
function(module_id) { |
202 | 112x |
srv_teal_module( |
203 | 112x |
id = module_id, |
204 | 112x |
data = data, |
205 | 112x |
modules = modules$children[[module_id]], |
206 | 112x |
datasets = datasets, |
207 | 112x |
slices_global = slices_global, |
208 | 112x |
reporter = reporter, |
209 | 112x |
is_active = reactive( |
210 | 112x |
is_active() && |
211 | 112x |
input$active_tab == module_id && |
212 | 112x |
identical(data_load_status(), "ok") |
213 |
) |
|
214 |
) |
|
215 |
}, |
|
216 | 87x |
simplify = FALSE |
217 |
) |
|
218 | ||
219 | 87x |
modules_output |
220 |
}) |
|
221 |
} |
|
222 | ||
223 |
#' @rdname module_teal_module |
|
224 |
#' @export |
|
225 |
srv_teal_module.teal_module <- function(id, |
|
226 |
data, |
|
227 |
modules, |
|
228 |
datasets = NULL, |
|
229 |
slices_global, |
|
230 |
reporter = teal.reporter::Reporter$new(), |
|
231 |
data_load_status = reactive("ok"), |
|
232 |
is_active = reactive(TRUE)) { |
|
233 | 112x |
logger::log_debug("srv_teal_module.teal_module initializing the module: { deparse1(modules$label) }.") |
234 | 112x |
moduleServer(id = id, module = function(input, output, session) { |
235 | 112x |
module_out <- reactiveVal() |
236 | ||
237 | 112x |
active_datanames <- reactive({ |
238 | 89x |
.resolve_module_datanames(data = data(), modules = modules) |
239 |
}) |
|
240 | 112x |
if (is.null(datasets)) { |
241 | 20x |
datasets <- eventReactive(data(), { |
242 | 16x |
req(inherits(data(), "teal_data")) |
243 | 16x |
logger::log_debug("srv_teal_module@1 initializing module-specific FilteredData") |
244 | 16x |
teal_data_to_filtered_data(data(), datanames = active_datanames()) |
245 |
}) |
|
246 |
} |
|
247 | ||
248 |
# manage module filters on the module level |
|
249 |
# important: |
|
250 |
# filter_manager_module_srv needs to be called before filter_panel_srv |
|
251 |
# Because available_teal_slices is used in FilteredData$srv_available_slices (via srv_filter_panel) |
|
252 |
# and if it is not set, then it won't be available in the srv_filter_panel |
|
253 | 112x |
srv_module_filter_manager(modules$label, module_fd = datasets, slices_global = slices_global) |
254 | ||
255 | 112x |
call_once_when(is_active(), { |
256 | 86x |
filtered_teal_data <- srv_filter_data( |
257 | 86x |
"filter_panel", |
258 | 86x |
datasets = datasets, |
259 | 86x |
active_datanames = active_datanames, |
260 | 86x |
data = data, |
261 | 86x |
is_active = is_active |
262 |
) |
|
263 | 86x |
is_transform_failed <- reactiveValues() |
264 | 86x |
transformed_teal_data <- srv_transform_teal_data( |
265 | 86x |
"data_transform", |
266 | 86x |
data = filtered_teal_data, |
267 | 86x |
transformators = modules$transformators, |
268 | 86x |
modules = modules, |
269 | 86x |
is_transform_failed = is_transform_failed |
270 |
) |
|
271 | 86x |
any_transform_failed <- reactive({ |
272 | 86x |
any(unlist(reactiveValuesToList(is_transform_failed))) |
273 |
}) |
|
274 | ||
275 | 86x |
observeEvent(any_transform_failed(), { |
276 | 86x |
if (isTRUE(any_transform_failed())) { |
277 | 6x |
shinyjs::hide("teal_module_ui") |
278 | 6x |
shinyjs::show("transform_failure_info") |
279 |
} else { |
|
280 | 80x |
shinyjs::show("teal_module_ui") |
281 | 80x |
shinyjs::hide("transform_failure_info") |
282 |
} |
|
283 |
}) |
|
284 | ||
285 | 86x |
module_teal_data <- reactive({ |
286 | 94x |
req(inherits(transformed_teal_data(), "teal_data")) |
287 | 88x |
all_teal_data <- transformed_teal_data() |
288 | 88x |
module_datanames <- .resolve_module_datanames(data = all_teal_data, modules = modules) |
289 | 88x |
all_teal_data[c(module_datanames, ".raw_data")] |
290 |
}) |
|
291 | ||
292 | 86x |
srv_check_module_datanames( |
293 | 86x |
"validate_datanames", |
294 | 86x |
data = module_teal_data, |
295 | 86x |
modules = modules |
296 |
) |
|
297 | ||
298 | 86x |
summary_table <- srv_data_summary("data_summary", module_teal_data) |
299 | ||
300 |
# Call modules. |
|
301 | 86x |
if (!inherits(modules, "teal_module_previewer")) { |
302 | 86x |
obs_module <- call_once_when( |
303 | 86x |
!is.null(module_teal_data()), |
304 | 86x |
ignoreNULL = TRUE, |
305 | 86x |
handlerExpr = { |
306 | 80x |
module_out(.call_teal_module(modules, datasets, module_teal_data, reporter)) |
307 |
} |
|
308 |
) |
|
309 |
} else { |
|
310 |
# Report previewer must be initiated on app start for report cards to be included in bookmarks. |
|
311 |
# When previewer is delayed, cards are bookmarked only if previewer has been initiated (visited). |
|
312 | ! |
module_out(.call_teal_module(modules, datasets, module_teal_data, reporter)) |
313 |
} |
|
314 |
}) |
|
315 | ||
316 | 112x |
module_out |
317 |
}) |
|
318 |
} |
|
319 | ||
320 |
# This function calls a module server function. |
|
321 |
.call_teal_module <- function(modules, datasets, data, reporter) { |
|
322 | 80x |
assert_reactive(data) |
323 | ||
324 |
# collect arguments to run teal_module |
|
325 | 80x |
args <- c(list(id = "module"), modules$server_args) |
326 | 80x |
if (is_arg_used(modules$server, "reporter")) { |
327 | 1x |
args <- c(args, list(reporter = reporter)) |
328 |
} |
|
329 | ||
330 | 80x |
if (is_arg_used(modules$server, "datasets")) { |
331 | 1x |
args <- c(args, datasets = datasets()) |
332 | 1x |
warning("datasets argument is not reactive and therefore it won't be updated when data is refreshed.") |
333 |
} |
|
334 | ||
335 | 80x |
if (is_arg_used(modules$server, "data")) { |
336 | 76x |
args <- c(args, data = list(data)) |
337 |
} |
|
338 | ||
339 | 80x |
if (is_arg_used(modules$server, "filter_panel_api")) { |
340 | 1x |
args <- c(args, filter_panel_api = teal.slice::FilterPanelAPI$new(datasets())) |
341 |
} |
|
342 | ||
343 | 80x |
if (is_arg_used(modules$server, "id")) { |
344 | 80x |
do.call(modules$server, args) |
345 |
} else { |
|
346 | ! |
do.call(callModule, c(args, list(module = modules$server))) |
347 |
} |
|
348 |
} |
|
349 | ||
350 |
.resolve_module_datanames <- function(data, modules) { |
|
351 | 177x |
stopifnot("data must be teal_data object." = inherits(data, "teal_data")) |
352 | 177x |
if (is.null(modules$datanames) || identical(modules$datanames, "all")) { |
353 | 145x |
names(data) |
354 |
} else { |
|
355 | 32x |
intersect( |
356 | 32x |
names(data), # Keep topological order from teal.data::names() |
357 | 32x |
.include_parent_datanames(modules$datanames, teal.data::join_keys(data)) |
358 |
) |
|
359 |
} |
|
360 |
} |
|
361 | ||
362 |
#' Calls expression when condition is met |
|
363 |
#' |
|
364 |
#' Function postpones `handlerExpr` to the moment when `eventExpr` (condition) returns `TRUE`, |
|
365 |
#' otherwise nothing happens. |
|
366 |
#' @param eventExpr A (quoted or unquoted) logical expression that represents the event; |
|
367 |
#' this can be a simple reactive value like input$click, a call to a reactive expression |
|
368 |
#' like dataset(), or even a complex expression inside curly braces. |
|
369 |
#' @param ... additional arguments passed to `observeEvent` with the exception of `eventExpr` that is not allowed. |
|
370 |
#' @inheritParams shiny::observeEvent |
|
371 |
#' |
|
372 |
#' @return An observer. |
|
373 |
#' |
|
374 |
#' @keywords internal |
|
375 |
call_once_when <- function(eventExpr, # nolint: object_name. |
|
376 |
handlerExpr, # nolint: object_name. |
|
377 |
event.env = parent.frame(), # nolint: object_name. |
|
378 |
handler.env = parent.frame(), # nolint: object_name. |
|
379 |
...) { |
|
380 | 198x |
event_quo <- rlang::new_quosure(substitute(eventExpr), env = event.env) |
381 | 198x |
handler_quo <- rlang::new_quosure(substitute(handlerExpr), env = handler.env) |
382 | ||
383 |
# When `condExpr` is TRUE, then `handlerExpr` is evaluated once. |
|
384 | 198x |
activator <- reactive({ |
385 | 198x |
if (isTRUE(rlang::eval_tidy(event_quo))) { |
386 | 166x |
TRUE |
387 |
} |
|
388 |
}) |
|
389 | ||
390 | 198x |
observeEvent( |
391 | 198x |
eventExpr = activator(), |
392 | 198x |
once = TRUE, |
393 | 198x |
handlerExpr = rlang::eval_tidy(handler_quo), |
394 |
... |
|
395 |
) |
|
396 |
} |
1 |
#' Data summary |
|
2 |
#' @description |
|
3 |
#' Module and its utils to display the number of rows and subjects in the filtered and unfiltered data. |
|
4 |
#' |
|
5 |
#' @details Handling different data classes: |
|
6 |
#' `get_filter_overview()` is a pseudo S3 method which has variants for: |
|
7 |
#' - `array` (`data.frame`, `DataFrame`, `array`, `Matrix` and `SummarizedExperiment`): Method variant |
|
8 |
#' can be applied to any two-dimensional objects on which [ncol()] can be used. |
|
9 |
#' - `MultiAssayExperiment`: for which summary contains counts for `colData` and all `experiments`. |
|
10 |
#' - For other data types module displays data name with warning icon and no more details. |
|
11 |
#' |
|
12 |
#' Module includes also "Show/Hide unsupported" button to toggle rows of the summary table |
|
13 |
#' containing datasets where number of observations are not calculated. |
|
14 |
#' |
|
15 |
#' @param id (`character(1)`) `shiny` module instance id. |
|
16 |
#' @param teal_data (`reactive` returning `teal_data`) |
|
17 |
#' |
|
18 |
#' @name module_data_summary |
|
19 |
#' @rdname module_data_summary |
|
20 |
#' @keywords internal |
|
21 |
#' @return `NULL`. |
|
22 |
NULL |
|
23 | ||
24 |
#' @rdname module_data_summary |
|
25 |
ui_data_summary <- function(id) { |
|
26 | ! |
ns <- NS(id) |
27 | ! |
content_id <- ns("filters_overview_contents") |
28 | ! |
tags$div( |
29 | ! |
id = id, |
30 | ! |
class = "well", |
31 | ! |
tags$div( |
32 | ! |
class = "row", |
33 | ! |
tags$div( |
34 | ! |
class = "col-sm-9", |
35 | ! |
tags$label("Active Filter Summary", class = "text-primary mb-4") |
36 |
), |
|
37 | ! |
tags$div( |
38 | ! |
class = "col-sm-3", |
39 | ! |
tags$i( |
40 | ! |
class = "remove pull-right fa fa-angle-down", |
41 | ! |
style = "cursor: pointer;", |
42 | ! |
title = "fold/expand data summary panel", |
43 | ! |
onclick = sprintf("togglePanelItems(this, '%s', 'fa-angle-right', 'fa-angle-down');", content_id) |
44 |
) |
|
45 |
) |
|
46 |
), |
|
47 | ! |
tags$div( |
48 | ! |
id = content_id, |
49 | ! |
tags$div( |
50 | ! |
class = "teal_active_summary_filter_panel", |
51 | ! |
tableOutput(ns("table")) |
52 |
) |
|
53 |
) |
|
54 |
) |
|
55 |
} |
|
56 | ||
57 |
#' @rdname module_data_summary |
|
58 |
srv_data_summary <- function(id, data) { |
|
59 | 86x |
assert_reactive(data) |
60 | 86x |
moduleServer( |
61 | 86x |
id = id, |
62 | 86x |
function(input, output, session) { |
63 | 86x |
logger::log_debug("srv_data_summary initializing") |
64 | ||
65 | 86x |
summary_table <- reactive({ |
66 | 94x |
req(inherits(data(), "teal_data")) |
67 | 88x |
if (!length(data())) { |
68 | ! |
return(NULL) |
69 |
} |
|
70 | 88x |
get_filter_overview_wrapper(data) |
71 |
}) |
|
72 | ||
73 | 86x |
output$table <- renderUI({ |
74 | 94x |
summary_table_out <- try(summary_table(), silent = TRUE) |
75 | 94x |
if (inherits(summary_table_out, "try-error")) { |
76 |
# Ignore silent shiny error |
|
77 | 6x |
if (!inherits(attr(summary_table_out, "condition"), "shiny.silent.error")) { |
78 | ! |
stop("Error occurred during data processing. See details in the main panel.") |
79 |
} |
|
80 | 88x |
} else if (is.null(summary_table_out)) { |
81 | 2x |
"no datasets to show" |
82 |
} else { |
|
83 | 86x |
is_unsupported <- apply(summary_table(), 1, function(x) all(is.na(x[-1]))) |
84 | 86x |
summary_table_out[is.na(summary_table_out)] <- "" |
85 | 86x |
body_html <- apply( |
86 | 86x |
summary_table_out, |
87 | 86x |
1, |
88 | 86x |
function(x) { |
89 | 162x |
is_supported <- !all(x[-1] == "") |
90 | 162x |
if (is_supported) { |
91 | 153x |
tags$tr( |
92 | 153x |
tagList( |
93 | 153x |
tags$td(x[1]), |
94 | 153x |
lapply(x[-1], tags$td) |
95 |
) |
|
96 |
) |
|
97 |
} |
|
98 |
} |
|
99 |
) |
|
100 | ||
101 | 86x |
header_labels <- tools::toTitleCase(names(summary_table_out)) |
102 | 86x |
header_labels[header_labels == "Dataname"] <- "Data Name" |
103 | 86x |
header_html <- tags$tr(tagList(lapply(header_labels, tags$td))) |
104 | ||
105 | 86x |
table_html <- tags$table( |
106 | 86x |
class = "table custom-table", |
107 | 86x |
tags$thead(header_html), |
108 | 86x |
tags$tbody(body_html) |
109 |
) |
|
110 | 86x |
div( |
111 | 86x |
table_html, |
112 | 86x |
if (any(is_unsupported)) { |
113 | 9x |
p( |
114 | 9x |
class = c("pull-right", "float-right", "text-secondary"), |
115 | 9x |
style = "font-size: 0.8em;", |
116 | 9x |
sprintf("And %s more unfilterable object(s)", sum(is_unsupported)), |
117 | 9x |
icon( |
118 | 9x |
name = "far fa-circle-question", |
119 | 9x |
title = paste( |
120 | 9x |
sep = "", |
121 | 9x |
collapse = "\n", |
122 | 9x |
shQuote(summary_table()[is_unsupported, "dataname"]), |
123 |
" (", |
|
124 | 9x |
vapply( |
125 | 9x |
summary_table()[is_unsupported, "dataname"], |
126 | 9x |
function(x) class(data()[[x]])[1], |
127 | 9x |
character(1L) |
128 |
), |
|
129 |
")" |
|
130 |
) |
|
131 |
) |
|
132 |
) |
|
133 |
} |
|
134 |
) |
|
135 |
} |
|
136 |
}) |
|
137 | ||
138 | 86x |
NULL |
139 |
} |
|
140 |
) |
|
141 |
} |
|
142 | ||
143 |
#' @rdname module_data_summary |
|
144 |
get_filter_overview_wrapper <- function(teal_data) { |
|
145 |
# Sort datanames in topological order |
|
146 | 88x |
datanames <- names(teal_data()) |
147 | 88x |
joinkeys <- teal.data::join_keys(teal_data()) |
148 | ||
149 | 88x |
current_data_objs <- sapply( |
150 | 88x |
datanames, |
151 | 88x |
function(name) teal_data()[[name]], |
152 | 88x |
simplify = FALSE |
153 |
) |
|
154 | 88x |
initial_data_objs <- teal_data()[[".raw_data"]] |
155 | ||
156 | 88x |
out <- lapply( |
157 | 88x |
datanames, |
158 | 88x |
function(dataname) { |
159 | 157x |
parent <- teal.data::parent(joinkeys, dataname) |
160 | 157x |
subject_keys <- if (length(parent) > 0) { |
161 | 8x |
names(joinkeys[dataname, parent]) |
162 |
} else { |
|
163 | 149x |
joinkeys[dataname, dataname] |
164 |
} |
|
165 | 157x |
get_filter_overview( |
166 | 157x |
current_data = current_data_objs[[dataname]], |
167 | 157x |
initial_data = initial_data_objs[[dataname]], |
168 | 157x |
dataname = dataname, |
169 | 157x |
subject_keys = subject_keys |
170 |
) |
|
171 |
} |
|
172 |
) |
|
173 | ||
174 | 88x |
do.call(.smart_rbind, out) |
175 |
} |
|
176 | ||
177 | ||
178 |
#' @rdname module_data_summary |
|
179 |
#' @param current_data (`object`) current object (after filtering and transforming). |
|
180 |
#' @param initial_data (`object`) initial object. |
|
181 |
#' @param dataname (`character(1)`) |
|
182 |
#' @param subject_keys (`character`) names of the columns which determine a single unique subjects |
|
183 |
get_filter_overview <- function(current_data, initial_data, dataname, subject_keys) { |
|
184 | 162x |
if (inherits(current_data, c("data.frame", "DataFrame", "array", "Matrix", "SummarizedExperiment"))) { |
185 | 152x |
get_filter_overview_array(current_data, initial_data, dataname, subject_keys) |
186 | 10x |
} else if (inherits(current_data, "MultiAssayExperiment")) { |
187 | 1x |
get_filter_overview_MultiAssayExperiment(current_data, initial_data, dataname) |
188 |
} else { |
|
189 | 9x |
data.frame(dataname = dataname) |
190 |
} |
|
191 |
} |
|
192 | ||
193 |
#' @rdname module_data_summary |
|
194 |
get_filter_overview_array <- function(current_data, |
|
195 |
initial_data, |
|
196 |
dataname, |
|
197 |
subject_keys) { |
|
198 | 152x |
if (length(subject_keys) == 0) { |
199 | 138x |
data.frame( |
200 | 138x |
dataname = dataname, |
201 | 138x |
obs = if (!is.null(initial_data)) { |
202 | 127x |
sprintf("%s/%s", nrow(current_data), nrow(initial_data)) |
203 |
} else { |
|
204 | 11x |
nrow(current_data) |
205 |
} |
|
206 |
) |
|
207 |
} else { |
|
208 | 14x |
data.frame( |
209 | 14x |
dataname = dataname, |
210 | 14x |
obs = if (!is.null(initial_data)) { |
211 | 13x |
sprintf("%s/%s", nrow(current_data), nrow(initial_data)) |
212 |
} else { |
|
213 | 1x |
nrow(current_data) |
214 |
}, |
|
215 | 14x |
subjects = if (!is.null(initial_data)) { |
216 | 13x |
sprintf("%s/%s", nrow(unique(current_data[subject_keys])), nrow(unique(initial_data[subject_keys]))) |
217 |
} else { |
|
218 | 1x |
nrow(unique(current_data[subject_keys])) |
219 |
} |
|
220 |
) |
|
221 |
} |
|
222 |
} |
|
223 | ||
224 |
#' @rdname module_data_summary |
|
225 |
get_filter_overview_MultiAssayExperiment <- function(current_data, # nolint: object_length, object_name. |
|
226 |
initial_data, |
|
227 |
dataname) { |
|
228 | 1x |
experiment_names <- names(current_data) |
229 | 1x |
mae_info <- data.frame( |
230 | 1x |
dataname = dataname, |
231 | 1x |
subjects = if (!is.null(initial_data)) { |
232 | ! |
sprintf("%s/%s", nrow(current_data@colData), nrow(initial_data@colData)) |
233 |
} else { |
|
234 | 1x |
nrow(current_data@colData) |
235 |
} |
|
236 |
) |
|
237 | ||
238 | 1x |
experiment_obs_info <- do.call("rbind", lapply( |
239 | 1x |
experiment_names, |
240 | 1x |
function(experiment_name) { |
241 | 5x |
transform( |
242 | 5x |
get_filter_overview( |
243 | 5x |
current_data[[experiment_name]], |
244 | 5x |
initial_data[[experiment_name]], |
245 | 5x |
dataname = experiment_name, |
246 | 5x |
subject_keys = join_keys() # empty join keys |
247 |
), |
|
248 | 5x |
dataname = paste0(" - ", experiment_name) |
249 |
) |
|
250 |
} |
|
251 |
)) |
|
252 | ||
253 | 1x |
get_experiment_keys <- function(mae, experiment) { |
254 | 5x |
sample_subset <- mae@sampleMap[mae@sampleMap$colname %in% colnames(experiment), ] |
255 | 5x |
length(unique(sample_subset$primary)) |
256 |
} |
|
257 | ||
258 | 1x |
experiment_subjects_info <- do.call("rbind", lapply( |
259 | 1x |
experiment_names, |
260 | 1x |
function(experiment_name) { |
261 | 5x |
data.frame( |
262 | 5x |
subjects = if (!is.null(initial_data)) { |
263 | ! |
sprintf( |
264 | ! |
"%s/%s", |
265 | ! |
get_experiment_keys(current_data, current_data[[experiment_name]]), |
266 | ! |
get_experiment_keys(current_data, initial_data[[experiment_name]]) |
267 |
) |
|
268 |
} else { |
|
269 | 5x |
get_experiment_keys(current_data, current_data[[experiment_name]]) |
270 |
} |
|
271 |
) |
|
272 |
} |
|
273 |
)) |
|
274 | ||
275 | 1x |
experiment_info <- cbind(experiment_obs_info, experiment_subjects_info) |
276 | 1x |
.smart_rbind(mae_info, experiment_info) |
277 |
} |
1 |
#' Generate lockfile for application's environment reproducibility |
|
2 |
#' |
|
3 |
#' @param lockfile_path (`character`) path to the lockfile. |
|
4 |
#' |
|
5 |
#' @section Different ways of creating lockfile: |
|
6 |
#' `teal` leverages [renv::snapshot()], which offers multiple methods for lockfile creation. |
|
7 |
#' |
|
8 |
#' - **Working directory lockfile**: `teal`, by default, will create an `implicit` type lockfile that uses |
|
9 |
#' `renv::dependencies()` to detect all R packages in the current project's working directory. |
|
10 |
#' - **`DESCRIPTION`-based lockfile**: To generate a lockfile based on a `DESCRIPTION` file in your working |
|
11 |
#' directory, set `renv::settings$snapshot.type("explicit")`. The naming convention for `type` follows |
|
12 |
#' `renv::snapshot()`. For the `"explicit"` type, refer to `renv::settings$package.dependency.fields()` for the |
|
13 |
#' `DESCRIPTION` fields included in the lockfile. |
|
14 |
#' - **Custom files-based lockfile**: To specify custom files as the basis for the lockfile, set |
|
15 |
#' `renv::settings$snapshot.type("custom")` and configure the `renv.snapshot.filter` option. |
|
16 |
#' |
|
17 |
#' @section lockfile usage: |
|
18 |
#' After creating the lockfile, you can restore the application's environment using `renv::restore()`. |
|
19 |
#' |
|
20 |
#' @seealso [renv::snapshot()], [renv::restore()]. |
|
21 |
#' |
|
22 |
#' @return `NULL` |
|
23 |
#' |
|
24 |
#' @name module_teal_lockfile |
|
25 |
#' @rdname module_teal_lockfile |
|
26 |
#' |
|
27 |
#' @keywords internal |
|
28 |
NULL |
|
29 | ||
30 |
#' @rdname module_teal_lockfile |
|
31 |
ui_teal_lockfile <- function(id) { |
|
32 | ! |
ns <- NS(id) |
33 | ! |
shiny::tagList( |
34 | ! |
tags$span("", id = ns("lockFileStatus")), |
35 | ! |
shinyjs::disabled(downloadLink(ns("lockFileLink"), "Download lockfile")) |
36 |
) |
|
37 |
} |
|
38 | ||
39 |
#' @rdname module_teal_lockfile |
|
40 |
srv_teal_lockfile <- function(id) { |
|
41 | 88x |
moduleServer(id, function(input, output, session) { |
42 | 88x |
logger::log_debug("Initialize srv_teal_lockfile.") |
43 | 88x |
enable_lockfile_download <- function() { |
44 | ! |
shinyjs::html("lockFileStatus", "Application lockfile ready.") |
45 | ! |
shinyjs::hide("lockFileStatus", anim = TRUE) |
46 | ! |
shinyjs::enable("lockFileLink") |
47 | ! |
output$lockFileLink <- shiny::downloadHandler( |
48 | ! |
filename = function() { |
49 | ! |
"renv.lock" |
50 |
}, |
|
51 | ! |
content = function(file) { |
52 | ! |
file.copy(lockfile_path, file) |
53 | ! |
file |
54 |
}, |
|
55 | ! |
contentType = "application/json" |
56 |
) |
|
57 |
} |
|
58 | 88x |
disable_lockfile_download <- function() { |
59 | ! |
warning("Lockfile creation failed.", call. = FALSE) |
60 | ! |
shinyjs::html("lockFileStatus", "Lockfile creation failed.") |
61 | ! |
shinyjs::hide("lockFileLink") |
62 |
} |
|
63 | ||
64 | 88x |
shiny::onStop(function() { |
65 | 88x |
if (file.exists(lockfile_path) && !shiny::isRunning()) { |
66 | 1x |
logger::log_debug("Removing lockfile after shutting down the app") |
67 | 1x |
file.remove(lockfile_path) |
68 |
} |
|
69 |
}) |
|
70 | ||
71 | 88x |
lockfile_path <- "teal_app.lock" |
72 | 88x |
mode <- getOption("teal.lockfile.mode", default = "") |
73 | ||
74 | 88x |
if (!(mode %in% c("auto", "enabled", "disabled"))) { |
75 | ! |
stop("'teal.lockfile.mode' option can only be one of \"auto\", \"disabled\" or \"disabled\". ") |
76 |
} |
|
77 | ||
78 | 88x |
if (mode == "disabled") { |
79 | 1x |
logger::log_debug("'teal.lockfile.mode' option is set to 'disabled'. Hiding lockfile download button.") |
80 | 1x |
shinyjs::hide("lockFileLink") |
81 | 1x |
return(NULL) |
82 |
} |
|
83 | ||
84 | 87x |
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 |
} |
|
89 | ||
90 | 87x |
if (mode == "auto" && .is_disabled_lockfile_scenario()) { |
91 | 86x |
logger::log_debug( |
92 | 86x |
"Automatic lockfile creation disabled. Execution scenario satisfies teal:::.is_disabled_lockfile_scenario()." |
93 |
) |
|
94 | 86x |
shinyjs::hide("lockFileLink") |
95 | 86x |
return(NULL) |
96 |
} |
|
97 | ||
98 | 1x |
if (!.is_lockfile_deps_installed()) { |
99 | ! |
warning("Automatic lockfile creation disabled. `mirai` and `renv` packages must be installed.") |
100 | ! |
shinyjs::hide("lockFileLink") |
101 | ! |
return(NULL) |
102 |
} |
|
103 | ||
104 |
# - 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 |
# want to make a "teal_app.renv" then. This is why we copy only during active session. |
|
107 | 1x |
process <- .teal_lockfile_process_invoke(lockfile_path) |
108 | 1x |
observeEvent(process$status(), { |
109 | ! |
if (process$status() %in% c("initial", "running")) { |
110 | ! |
shinyjs::html("lockFileStatus", "Creating lockfile...") |
111 | ! |
} 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 | ! |
if (any(grepl("(WARNING|ERROR):", result$out))) { |
116 | ! |
warning("Lockfile created with warning(s) or error(s):", call. = FALSE) |
117 | ! |
for (i in result$out) { |
118 | ! |
warning(i, call. = FALSE) |
119 |
} |
|
120 |
} |
|
121 | ! |
enable_lockfile_download() |
122 |
} else { |
|
123 | ! |
disable_lockfile_download() |
124 |
} |
|
125 | ! |
} else if (process$status() == "error") { |
126 | ! |
disable_lockfile_download() |
127 |
} |
|
128 |
}) |
|
129 | ||
130 | 1x |
NULL |
131 |
}) |
|
132 |
} |
|
133 | ||
134 |
utils::globalVariables(c("opts", "sysenv", "libpaths", "wd", "lockfilepath", "run")) # needed for mirai call |
|
135 |
#' @rdname module_teal_lockfile |
|
136 |
.teal_lockfile_process_invoke <- function(lockfile_path) { |
|
137 | 1x |
mirai_obj <- NULL |
138 | 1x |
process <- shiny::ExtendedTask$new(function() { |
139 | 1x |
m <- mirai::mirai( |
140 |
{ |
|
141 | 1x |
options(opts) |
142 | 1x |
do.call(Sys.setenv, sysenv) |
143 | 1x |
.libPaths(libpaths) |
144 | 1x |
setwd(wd) |
145 | 1x |
run(lockfile_path = lockfile_path) |
146 |
}, |
|
147 | 1x |
run = .renv_snapshot, |
148 | 1x |
lockfile_path = lockfile_path, |
149 | 1x |
opts = options(), |
150 | 1x |
libpaths = .libPaths(), |
151 | 1x |
sysenv = as.list(Sys.getenv()), |
152 | 1x |
wd = getwd() |
153 |
) |
|
154 | 1x |
mirai_obj <<- m |
155 | 1x |
m |
156 |
}) |
|
157 | ||
158 | 1x |
shiny::onStop(function() { |
159 | 1x |
if (mirai::unresolved(mirai_obj)) { |
160 | ! |
logger::log_debug("Terminating a running lockfile process...") |
161 | ! |
mirai::stop_mirai(mirai_obj) # this doesn't stop running - renv will be created even if session is closed |
162 |
} |
|
163 |
}) |
|
164 | ||
165 | 1x |
suppressWarnings({ # 'package:stats' may not be available when loading |
166 | 1x |
process$invoke() |
167 |
}) |
|
168 | ||
169 | 1x |
logger::log_debug("Lockfile creation started based on { getwd() }.") |
170 | ||
171 | 1x |
process |
172 |
} |
|
173 | ||
174 |
#' @rdname module_teal_lockfile |
|
175 |
.renv_snapshot <- function(lockfile_path) { |
|
176 | 1x |
out <- utils::capture.output( |
177 | 1x |
res <- renv::snapshot( |
178 | 1x |
lockfile = lockfile_path, |
179 | 1x |
prompt = FALSE, |
180 | 1x |
force = TRUE, |
181 | 1x |
type = renv::settings$snapshot.type() # see the section "Different ways of creating lockfile" above here |
182 |
) |
|
183 |
) |
|
184 | ||
185 | 1x |
list(out = out, res = res) |
186 |
} |
|
187 | ||
188 |
#' @rdname module_teal_lockfile |
|
189 |
.is_lockfile_deps_installed <- function() { |
|
190 | 1x |
requireNamespace("mirai", quietly = TRUE) && requireNamespace("renv", quietly = TRUE) |
191 |
} |
|
192 | ||
193 |
#' @rdname module_teal_lockfile |
|
194 |
.is_disabled_lockfile_scenario <- function() { |
|
195 | 86x |
identical(Sys.getenv("CALLR_IS_RUNNING"), "true") || # inside callr process |
196 | 86x |
identical(Sys.getenv("TESTTHAT"), "true") || # inside devtools::test |
197 | 86x |
!identical(Sys.getenv("QUARTO_PROJECT_ROOT"), "") || # inside Quarto process |
198 |
( |
|
199 | 86x |
("CheckExEnv" %in% search()) || any(c("_R_CHECK_TIMINGS_", "_R_CHECK_LICENSE_") %in% names(Sys.getenv())) |
200 | 86x |
) # inside R CMD CHECK |
201 |
} |
1 |
#' Send input validation messages to output |
|
2 |
#' |
|
3 |
#' Captures messages from `InputValidator` objects and collates them |
|
4 |
#' into one message passed to `validate`. |
|
5 |
#' |
|
6 |
#' `shiny::validate` is used to withhold rendering of an output element until |
|
7 |
#' certain conditions are met and to print a validation message in place |
|
8 |
#' of the output element. |
|
9 |
#' `shinyvalidate::InputValidator` allows to validate input elements |
|
10 |
#' and to display specific messages in their respective input widgets. |
|
11 |
#' `validate_inputs` provides a hybrid solution. |
|
12 |
#' Given an `InputValidator` object, messages corresponding to inputs that fail validation |
|
13 |
#' are extracted and placed in one validation message that is passed to a `validate`/`need` call. |
|
14 |
#' This way the input `validator` messages are repeated in the output. |
|
15 |
#' |
|
16 |
#' The `...` argument accepts any number of `InputValidator` objects |
|
17 |
#' or a nested list of such objects. |
|
18 |
#' If `validators` are passed directly, all their messages are printed together |
|
19 |
#' under one (optional) header message specified by `header`. If a list is passed, |
|
20 |
#' messages are grouped by `validator`. The list's names are used as headers |
|
21 |
#' for their respective message groups. |
|
22 |
#' If neither of the nested list elements is named, a header message is taken from `header`. |
|
23 |
#' |
|
24 |
#' @param ... either any number of `InputValidator` objects |
|
25 |
#' or an optionally named, possibly nested `list` of `InputValidator` |
|
26 |
#' objects, see `Details` |
|
27 |
#' @param header (`character(1)`) generic validation message; set to NULL to omit |
|
28 |
#' |
|
29 |
#' @return |
|
30 |
#' Returns NULL if the final validation call passes and a `shiny.silent.error` if it fails. |
|
31 |
#' |
|
32 |
#' @seealso [`shinyvalidate::InputValidator`], [`shiny::validate`] |
|
33 |
#' |
|
34 |
#' @examplesIf require("shinyvalidate") |
|
35 |
#' library(shiny) |
|
36 |
#' library(shinyvalidate) |
|
37 |
#' |
|
38 |
#' ui <- fluidPage( |
|
39 |
#' selectInput("method", "validation method", c("sequential", "combined", "grouped")), |
|
40 |
#' sidebarLayout( |
|
41 |
#' sidebarPanel( |
|
42 |
#' selectInput("letter", "select a letter:", c(letters[1:3], LETTERS[4:6])), |
|
43 |
#' selectInput("number", "select a number:", 1:6), |
|
44 |
#' tags$br(), |
|
45 |
#' selectInput("color", "select a color:", |
|
46 |
#' c("black", "indianred2", "springgreen2", "cornflowerblue"), |
|
47 |
#' multiple = TRUE |
|
48 |
#' ), |
|
49 |
#' sliderInput("size", "select point size:", |
|
50 |
#' min = 0.1, max = 4, value = 0.25 |
|
51 |
#' ) |
|
52 |
#' ), |
|
53 |
#' mainPanel(plotOutput("plot")) |
|
54 |
#' ) |
|
55 |
#' ) |
|
56 |
#' |
|
57 |
#' server <- function(input, output) { |
|
58 |
#' # set up input validation |
|
59 |
#' iv <- InputValidator$new() |
|
60 |
#' iv$add_rule("letter", sv_in_set(LETTERS, "choose a capital letter")) |
|
61 |
#' iv$add_rule("number", function(x) { |
|
62 |
#' if (as.integer(x) %% 2L == 1L) "choose an even number" |
|
63 |
#' }) |
|
64 |
#' iv$enable() |
|
65 |
#' # more input validation |
|
66 |
#' iv_par <- InputValidator$new() |
|
67 |
#' iv_par$add_rule("color", sv_required(message = "choose a color")) |
|
68 |
#' iv_par$add_rule("color", function(x) { |
|
69 |
#' if (length(x) > 1L) "choose only one color" |
|
70 |
#' }) |
|
71 |
#' iv_par$add_rule( |
|
72 |
#' "size", |
|
73 |
#' sv_between( |
|
74 |
#' left = 0.5, right = 3, |
|
75 |
#' message_fmt = "choose a value between {left} and {right}" |
|
76 |
#' ) |
|
77 |
#' ) |
|
78 |
#' iv_par$enable() |
|
79 |
#' |
|
80 |
#' output$plot <- renderPlot({ |
|
81 |
#' # validate output |
|
82 |
#' switch(input[["method"]], |
|
83 |
#' "sequential" = { |
|
84 |
#' validate_inputs(iv) |
|
85 |
#' validate_inputs(iv_par, header = "Set proper graphical parameters") |
|
86 |
#' }, |
|
87 |
#' "combined" = validate_inputs(iv, iv_par), |
|
88 |
#' "grouped" = validate_inputs(list( |
|
89 |
#' "Some inputs require attention" = iv, |
|
90 |
#' "Set proper graphical parameters" = iv_par |
|
91 |
#' )) |
|
92 |
#' ) |
|
93 |
#' |
|
94 |
#' plot(faithful$eruptions ~ faithful$waiting, |
|
95 |
#' las = 1, pch = 16, |
|
96 |
#' col = input[["color"]], cex = input[["size"]] |
|
97 |
#' ) |
|
98 |
#' }) |
|
99 |
#' } |
|
100 |
#' |
|
101 |
#' if (interactive()) { |
|
102 |
#' shinyApp(ui, server) |
|
103 |
#' } |
|
104 |
#' |
|
105 |
#' @export |
|
106 |
#' |
|
107 |
validate_inputs <- function(..., header = "Some inputs require attention") { |
|
108 | 36x |
dots <- list(...) |
109 | 2x |
if (!is_validators(dots)) stop("validate_inputs accepts validators or a list thereof") |
110 | ||
111 | 34x |
messages <- extract_validator(dots, header) |
112 | 34x |
failings <- if (!any_names(dots)) { |
113 | 29x |
add_header(messages, header) |
114 |
} else { |
|
115 | 5x |
unlist(messages) |
116 |
} |
|
117 | ||
118 | 34x |
shiny::validate(shiny::need(is.null(failings), failings)) |
119 |
} |
|
120 | ||
121 |
### internal functions |
|
122 | ||
123 |
#' @noRd |
|
124 |
#' @keywords internal |
|
125 |
# recursive object type test |
|
126 |
# returns logical of length 1 |
|
127 |
is_validators <- function(x) { |
|
128 | 118x |
all(if (is.list(x)) unlist(lapply(x, is_validators)) else inherits(x, "InputValidator")) |
129 |
} |
|
130 | ||
131 |
#' @noRd |
|
132 |
#' @keywords internal |
|
133 |
# test if an InputValidator object is enabled |
|
134 |
# returns logical of length 1 |
|
135 |
# official method requested at https://github.com/rstudio/shinyvalidate/issues/64 |
|
136 |
validator_enabled <- function(x) { |
|
137 | 49x |
x$.__enclos_env__$private$enabled |
138 |
} |
|
139 | ||
140 |
#' Recursively extract messages from validator list |
|
141 |
#' @return A character vector or a list of character vectors, possibly nested and named. |
|
142 |
#' @noRd |
|
143 |
#' @keywords internal |
|
144 |
extract_validator <- function(iv, header) { |
|
145 | 113x |
if (inherits(iv, "InputValidator")) { |
146 | 49x |
add_header(gather_messages(iv), header) |
147 |
} else { |
|
148 | 58x |
if (is.null(names(iv))) names(iv) <- rep("", length(iv)) |
149 | 64x |
mapply(extract_validator, iv = iv, header = names(iv), SIMPLIFY = FALSE) |
150 |
} |
|
151 |
} |
|
152 | ||
153 |
#' Collate failing messages from validator. |
|
154 |
#' @return `list` |
|
155 |
#' @noRd |
|
156 |
#' @keywords internal |
|
157 |
gather_messages <- function(iv) { |
|
158 | 49x |
if (validator_enabled(iv)) { |
159 | 46x |
status <- iv$validate() |
160 | 46x |
failing_inputs <- Filter(Negate(is.null), status) |
161 | 46x |
unique(lapply(failing_inputs, function(x) x[["message"]])) |
162 |
} else { |
|
163 | 3x |
warning("Validator is disabled and will be omitted.") |
164 | 3x |
list() |
165 |
} |
|
166 |
} |
|
167 | ||
168 |
#' Add optional header to failing messages |
|
169 |
#' @noRd |
|
170 |
#' @keywords internal |
|
171 |
add_header <- function(messages, header = "") { |
|
172 | 78x |
ans <- unlist(messages) |
173 | 78x |
if (length(ans) != 0L && isTRUE(nchar(header) > 0L)) { |
174 | 31x |
ans <- c(paste0(header, "\n"), ans, "\n") |
175 |
} |
|
176 | 78x |
ans |
177 |
} |
|
178 | ||
179 |
#' Recursively check if the object contains a named list |
|
180 |
#' @noRd |
|
181 |
#' @keywords internal |
|
182 |
any_names <- function(x) { |
|
183 | 103x |
any( |
184 | 103x |
if (is.list(x)) { |
185 | 58x |
if (!is.null(names(x)) && any(names(x) != "")) TRUE else unlist(lapply(x, any_names)) |
186 |
} else { |
|
187 | 40x |
FALSE |
188 |
} |
|
189 |
) |
|
190 |
} |
1 |
#' Execute and validate `teal_data_module` |
|
2 |
#' |
|
3 |
#' This is a low level module to handle `teal_data_module` execution and validation. |
|
4 |
#' [teal_transform_module()] inherits from [teal_data_module()] so it is handled by this module too. |
|
5 |
#' [srv_teal()] accepts various `data` objects and eventually they are all transformed to `reactive` |
|
6 |
#' [teal.data::teal_data()] which is a standard data class in whole `teal` framework. |
|
7 |
#' |
|
8 |
#' @section data validation: |
|
9 |
#' |
|
10 |
#' Executed [teal_data_module()] is validated and output is validated for consistency. |
|
11 |
#' Output `data` is invalid if: |
|
12 |
#' 1. [teal_data_module()] is invalid if server doesn't return `reactive`. **Immediately crashes an app!** |
|
13 |
#' 2. `reactive` throws a `shiny.error` - happens when module creating [teal.data::teal_data()] fails. |
|
14 |
#' 3. `reactive` returns `qenv.error` - happens when [teal.data::teal_data()] evaluates a failing code. |
|
15 |
#' 4. `reactive` object doesn't return [teal.data::teal_data()]. |
|
16 |
#' 5. [teal.data::teal_data()] object lacks any `datanames` specified in the `modules` argument. |
|
17 |
#' |
|
18 |
#' `teal` (observers in `srv_teal`) always waits to render an app until `reactive` `teal_data` is |
|
19 |
#' returned. If error 2-4 occurs, relevant error message is displayed to the app user. Once the issue is |
|
20 |
#' resolved, the app will continue to run. `teal` guarantees that errors in data don't crash the app |
|
21 |
#' (except error 1). |
|
22 |
#' |
|
23 |
#' @param id (`character(1)`) Module id |
|
24 |
#' @param data (`reactive teal_data`) |
|
25 |
#' @param data_module (`teal_data_module`) |
|
26 |
#' @param modules (`teal_modules` or `teal_module`) For `datanames` validation purpose |
|
27 |
#' @param validate_shiny_silent_error (`logical`) If `TRUE`, then `shiny.silent.error` is validated and |
|
28 |
#' @param is_transform_failed (`reactiveValues`) contains `logical` flags named after each transformator. |
|
29 |
#' Help to determine if any previous transformator failed, so that following transformators can be disabled |
|
30 |
#' and display a generic failure message. |
|
31 |
#' |
|
32 |
#' @return `reactive` `teal_data` |
|
33 |
#' |
|
34 |
#' @rdname module_teal_data |
|
35 |
#' @name module_teal_data |
|
36 |
#' @keywords internal |
|
37 |
NULL |
|
38 | ||
39 |
#' @rdname module_teal_data |
|
40 |
#' @aliases ui_teal_data |
|
41 |
#' @note |
|
42 |
#' `ui_teal_data_module` was renamed from `ui_teal_data`. |
|
43 |
ui_teal_data_module <- function(id, data_module = function(id) NULL) { |
|
44 | ! |
checkmate::assert_string(id) |
45 | ! |
checkmate::assert_function(data_module, args = "id") |
46 | ! |
ns <- NS(id) |
47 | ||
48 | ! |
shiny::tagList( |
49 | ! |
tags$div(id = ns("wrapper"), data_module(id = ns("data"))), |
50 | ! |
ui_validate_reactive_teal_data(ns("validate")) |
51 |
) |
|
52 |
} |
|
53 | ||
54 |
#' @rdname module_teal_data |
|
55 |
#' @aliases srv_teal_data |
|
56 |
#' @note |
|
57 |
#' `srv_teal_data_module` was renamed from `srv_teal_data`. |
|
58 |
srv_teal_data_module <- function(id, |
|
59 |
data_module = function(id) NULL, |
|
60 |
modules = NULL, |
|
61 |
validate_shiny_silent_error = TRUE, |
|
62 |
is_transform_failed = reactiveValues()) { |
|
63 | ! |
checkmate::assert_string(id) |
64 | ! |
checkmate::assert_function(data_module, args = "id") |
65 | ! |
checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"), null.ok = TRUE) |
66 | ! |
checkmate::assert_class(is_transform_failed, "reactivevalues") |
67 | ||
68 | ! |
moduleServer(id, function(input, output, session) { |
69 | ! |
logger::log_debug("srv_teal_data_module initializing.") |
70 | ! |
is_transform_failed[[id]] <- FALSE |
71 | ! |
module_out <- data_module(id = "data") |
72 | ! |
try_module_out <- reactive(tryCatch(module_out(), error = function(e) e)) |
73 | ! |
observeEvent(try_module_out(), { |
74 | ! |
if (!inherits(try_module_out(), "teal_data")) { |
75 | ! |
is_transform_failed[[id]] <- TRUE |
76 |
} else { |
|
77 | ! |
is_transform_failed[[id]] <- FALSE |
78 |
} |
|
79 |
}) |
|
80 | ||
81 | ! |
is_previous_failed <- reactive({ |
82 | ! |
idx_this <- which(names(is_transform_failed) == id) |
83 | ! |
is_transform_failed_list <- reactiveValuesToList(is_transform_failed) |
84 | ! |
idx_failures <- which(unlist(is_transform_failed_list)) |
85 | ! |
any(idx_failures < idx_this) |
86 |
}) |
|
87 | ||
88 | ! |
observeEvent(is_previous_failed(), { |
89 | ! |
if (is_previous_failed()) { |
90 | ! |
shinyjs::disable("wrapper") |
91 |
} else { |
|
92 | ! |
shinyjs::enable("wrapper") |
93 |
} |
|
94 |
}) |
|
95 | ||
96 | ! |
srv_validate_reactive_teal_data( |
97 | ! |
"validate", |
98 | ! |
data = try_module_out, |
99 | ! |
modules = modules, |
100 | ! |
validate_shiny_silent_error = validate_shiny_silent_error, |
101 | ! |
hide_validation_error = is_previous_failed |
102 |
) |
|
103 |
}) |
|
104 |
} |
|
105 | ||
106 |
#' @rdname module_teal_data |
|
107 |
ui_validate_reactive_teal_data <- function(id) { |
|
108 | ! |
ns <- NS(id) |
109 | ! |
tagList( |
110 | ! |
div( |
111 | ! |
id = ns("validate_messages"), |
112 | ! |
class = "teal_validated", |
113 | ! |
ui_validate_error(ns("silent_error")), |
114 | ! |
ui_check_class_teal_data(ns("class_teal_data")), |
115 | ! |
ui_check_module_datanames(ns("shiny_warnings")) |
116 |
), |
|
117 | ! |
div( |
118 | ! |
class = "teal_validated", |
119 | ! |
uiOutput(ns("previous_failed")) |
120 |
) |
|
121 |
) |
|
122 |
} |
|
123 | ||
124 |
#' @rdname module_teal_data |
|
125 |
srv_validate_reactive_teal_data <- function(id, # nolint: object_length |
|
126 |
data, |
|
127 |
modules = NULL, |
|
128 |
validate_shiny_silent_error = FALSE, |
|
129 |
hide_validation_error = reactive(FALSE)) { |
|
130 | ! |
checkmate::assert_string(id) |
131 | ! |
checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"), null.ok = TRUE) |
132 | ! |
checkmate::assert_flag(validate_shiny_silent_error) |
133 | ||
134 | ! |
moduleServer(id, function(input, output, session) { |
135 |
# there is an empty reactive cycle on `init` and `data` has `shiny.silent.error` class |
|
136 | ! |
srv_validate_error("silent_error", data, validate_shiny_silent_error) |
137 | ! |
srv_check_class_teal_data("class_teal_data", data) |
138 | ! |
srv_check_module_datanames("shiny_warnings", data, modules) |
139 | ! |
output$previous_failed <- renderUI({ |
140 | ! |
if (hide_validation_error()) { |
141 | ! |
shinyjs::hide("validate_messages") |
142 | ! |
tags$div("One of previous transformators failed. Please check its inputs.", class = "teal-output-warning") |
143 |
} else { |
|
144 | ! |
shinyjs::show("validate_messages") |
145 | ! |
NULL |
146 |
} |
|
147 |
}) |
|
148 | ||
149 | ! |
.trigger_on_success(data) |
150 |
}) |
|
151 |
} |
|
152 | ||
153 |
#' @keywords internal |
|
154 |
ui_validate_error <- function(id) { |
|
155 | 116x |
ns <- NS(id) |
156 | 116x |
uiOutput(ns("message")) |
157 |
} |
|
158 | ||
159 |
#' @keywords internal |
|
160 |
srv_validate_error <- function(id, data, validate_shiny_silent_error) { |
|
161 | 113x |
checkmate::assert_string(id) |
162 | 113x |
checkmate::assert_flag(validate_shiny_silent_error) |
163 | 113x |
moduleServer(id, function(input, output, session) { |
164 | 113x |
output$message <- renderUI({ |
165 | 112x |
is_shiny_silent_error <- inherits(data(), "shiny.silent.error") && identical(data()$message, "") |
166 | 112x |
if (inherits(data(), "qenv.error")) { |
167 | 2x |
validate( |
168 | 2x |
need( |
169 | 2x |
FALSE, |
170 | 2x |
paste( |
171 | 2x |
"Error when executing the `data` module:", |
172 | 2x |
cli::ansi_strip(paste(data()$message, collapse = "\n")), |
173 | 2x |
"\nCheck your inputs or contact app developer if error persists.", |
174 | 2x |
collapse = "\n" |
175 |
) |
|
176 |
) |
|
177 |
) |
|
178 | 110x |
} else if (inherits(data(), "error")) { |
179 | 11x |
if (is_shiny_silent_error && !validate_shiny_silent_error) { |
180 | 4x |
return(NULL) |
181 |
} |
|
182 | 7x |
validate( |
183 | 7x |
need( |
184 | 7x |
FALSE, |
185 | 7x |
sprintf( |
186 | 7x |
"Shiny error when executing the `data` module.\n%s\n%s", |
187 | 7x |
data()$message, |
188 | 7x |
"Check your inputs or contact app developer if error persists." |
189 |
) |
|
190 |
) |
|
191 |
) |
|
192 |
} |
|
193 |
}) |
|
194 |
}) |
|
195 |
} |
|
196 | ||
197 | ||
198 |
#' @keywords internal |
|
199 |
ui_check_class_teal_data <- function(id) { |
|
200 | 116x |
ns <- NS(id) |
201 | 116x |
uiOutput(ns("message")) |
202 |
} |
|
203 | ||
204 |
#' @keywords internal |
|
205 |
srv_check_class_teal_data <- function(id, data) { |
|
206 | 113x |
checkmate::assert_string(id) |
207 | 113x |
moduleServer(id, function(input, output, session) { |
208 | 113x |
output$message <- renderUI({ |
209 | 112x |
validate( |
210 | 112x |
need( |
211 | 112x |
inherits(data(), c("teal_data", "error")), |
212 | 112x |
"Did not receive `teal_data` object. Cannot proceed further." |
213 |
) |
|
214 |
) |
|
215 |
}) |
|
216 |
}) |
|
217 |
} |
|
218 | ||
219 |
#' @keywords internal |
|
220 |
ui_check_module_datanames <- function(id) { |
|
221 | 116x |
ns <- NS(id) |
222 | 116x |
uiOutput(NS(id, "message")) |
223 |
} |
|
224 | ||
225 |
#' @keywords internal |
|
226 |
srv_check_module_datanames <- function(id, data, modules) { |
|
227 | 193x |
checkmate::assert_string(id) |
228 | 193x |
moduleServer(id, function(input, output, session) { |
229 | 193x |
output$message <- renderUI({ |
230 | 196x |
if (inherits(data(), "teal_data")) { |
231 | 179x |
is_modules_ok <- check_modules_datanames_html( |
232 | 179x |
modules = modules, datanames = names(data()) |
233 |
) |
|
234 | 179x |
if (!isTRUE(is_modules_ok)) { |
235 | 19x |
tags$div(is_modules_ok, class = "teal-output-warning") |
236 |
} |
|
237 |
} |
|
238 |
}) |
|
239 |
}) |
|
240 |
} |
|
241 | ||
242 |
.trigger_on_success <- function(data) { |
|
243 | 113x |
out <- reactiveVal(NULL) |
244 | 113x |
observeEvent(data(), { |
245 | 112x |
if (inherits(data(), "teal_data")) { |
246 | 97x |
if (!identical(data(), out())) { |
247 | 97x |
out(data()) |
248 |
} |
|
249 |
} |
|
250 |
}) |
|
251 | ||
252 | 113x |
out |
253 |
} |
1 |
# FilteredData ------ |
|
2 | ||
3 |
#' Drive a `teal` application |
|
4 |
#' |
|
5 |
#' Extension of the `shinytest2::AppDriver` class with methods for |
|
6 |
#' driving a teal application for performing interactions for `shinytest2` tests. |
|
7 |
#' |
|
8 |
#' @keywords internal |
|
9 |
#' |
|
10 |
TealAppDriver <- R6::R6Class( # nolint: object_name. |
|
11 |
"TealAppDriver", |
|
12 |
inherit = { |
|
13 |
lapply(c("testthat", "shinytest2", "rvest"), function(.x, use_testthat) { |
|
14 |
if (!requireNamespace(.x, quietly = TRUE)) { |
|
15 |
if (use_testthat) { |
|
16 |
testthat::skip(sprintf("%s is not installed", .x)) |
|
17 |
} else { |
|
18 |
stop("Please install '", .x, "' package to use this class.", call. = FALSE) |
|
19 |
} |
|
20 |
} |
|
21 |
}, use_testthat = requireNamespace("testthat", quietly = TRUE) && testthat::is_testing()) |
|
22 |
shinytest2::AppDriver |
|
23 |
}, |
|
24 |
# public methods ---- |
|
25 |
public = list( |
|
26 |
#' @description |
|
27 |
#' Initialize a `TealAppDriver` object for testing a `teal` application. |
|
28 |
#' |
|
29 |
#' @param data,modules,filter,title,header,footer,landing_popup arguments passed to `init` |
|
30 |
#' @param timeout (`numeric`) Default number of milliseconds for any timeout or |
|
31 |
#' timeout_ parameter in the `TealAppDriver` class. |
|
32 |
#' Defaults to 20s. |
|
33 |
#' |
|
34 |
#' See [`shinytest2::AppDriver`] `new` method for more details on how to change it |
|
35 |
#' via options or environment variables. |
|
36 |
#' @param load_timeout (`numeric`) How long to wait for the app to load, in ms. |
|
37 |
#' This includes the time to start R. Defaults to 100s. |
|
38 |
#' |
|
39 |
#' See [`shinytest2::AppDriver`] `new` method for more details on how to change it |
|
40 |
#' via options or environment variables |
|
41 |
#' @param ... Additional arguments to be passed to `shinytest2::AppDriver$new` |
|
42 |
#' |
|
43 |
#' |
|
44 |
#' @return Object of class `TealAppDriver` |
|
45 |
initialize = function(data, |
|
46 |
modules, |
|
47 |
filter = teal_slices(), |
|
48 |
title = build_app_title(), |
|
49 |
header = tags$p(), |
|
50 |
footer = tags$p(), |
|
51 |
landing_popup = NULL, |
|
52 |
timeout = rlang::missing_arg(), |
|
53 |
load_timeout = rlang::missing_arg(), |
|
54 |
...) { |
|
55 | ! |
private$data <- data |
56 | ! |
private$modules <- modules |
57 | ! |
private$filter <- filter |
58 | ! |
app <- init( |
59 | ! |
data = data, |
60 | ! |
modules = modules, |
61 | ! |
filter = filter, |
62 | ! |
title = title, |
63 | ! |
header = header, |
64 | ! |
footer = footer, |
65 | ! |
landing_popup = landing_popup, |
66 |
) |
|
67 | ||
68 |
# Default timeout is hardcoded to 4s in shinytest2:::resolve_timeout |
|
69 |
# It must be set as parameter to the AppDriver |
|
70 | ! |
suppressWarnings( |
71 | ! |
super$initialize( |
72 | ! |
app_dir = shinyApp(app$ui, app$server), |
73 | ! |
name = "teal", |
74 | ! |
variant = shinytest2::platform_variant(), |
75 | ! |
timeout = rlang::maybe_missing(timeout, 20 * 1000), |
76 | ! |
load_timeout = rlang::maybe_missing(load_timeout, 100 * 1000), |
77 |
... |
|
78 |
) |
|
79 |
) |
|
80 | ||
81 |
# Check for minimum version of Chrome that supports the tests |
|
82 |
# - Element.checkVisibility was added on 105 |
|
83 | ! |
chrome_version <- numeric_version( |
84 | ! |
gsub( |
85 | ! |
"[[:alnum:]_]+/", # Prefix that ends with forward slash |
86 |
"", |
|
87 | ! |
self$get_chromote_session()$Browser$getVersion()$product |
88 |
), |
|
89 | ! |
strict = FALSE |
90 |
) |
|
91 | ||
92 | ! |
required_version <- "121" |
93 | ||
94 | ! |
testthat::skip_if( |
95 | ! |
is.na(chrome_version), |
96 | ! |
"Problem getting Chrome version, please contact the developers." |
97 |
) |
|
98 | ! |
testthat::skip_if( |
99 | ! |
chrome_version < required_version, |
100 | ! |
sprintf( |
101 | ! |
"Chrome version '%s' is not supported, please upgrade to '%s' or higher", |
102 | ! |
chrome_version, |
103 | ! |
required_version |
104 |
) |
|
105 |
) |
|
106 |
# end od check |
|
107 | ||
108 | ! |
private$set_active_ns() |
109 | ! |
self$wait_for_idle() |
110 |
}, |
|
111 |
#' @description |
|
112 |
#' Append parent [`shinytest2::AppDriver`] `click` method with a call to `waif_for_idle()` method. |
|
113 |
#' @param ... arguments passed to parent [`shinytest2::AppDriver`] `click()` method. |
|
114 |
click = function(...) { |
|
115 | ! |
super$click(...) |
116 | ! |
private$wait_for_page_stability() |
117 |
}, |
|
118 |
#' @description |
|
119 |
#' Check if the app has shiny errors. This checks for global shiny errors. |
|
120 |
#' Note that any shiny errors dependent on shiny server render will only be captured after the teal module tab |
|
121 |
#' is visited because shiny will not trigger server computations when the tab is invisible. |
|
122 |
#' So, navigate to the module tab you want to test before calling this function. |
|
123 |
#' Although, this catches errors hidden in the other module tabs if they are already rendered. |
|
124 |
expect_no_shiny_error = function() { |
|
125 | ! |
testthat::expect_null( |
126 | ! |
self$get_html(".shiny-output-error:not(.shiny-output-error-validation)"), |
127 | ! |
info = "Shiny error is observed" |
128 |
) |
|
129 |
}, |
|
130 |
#' @description |
|
131 |
#' Check if the app has no validation errors. This checks for global shiny validation errors. |
|
132 |
expect_no_validation_error = function() { |
|
133 | ! |
testthat::expect_null( |
134 | ! |
self$get_html(".shiny-output-error-validation"), |
135 | ! |
info = "No validation error is observed" |
136 |
) |
|
137 |
}, |
|
138 |
#' @description |
|
139 |
#' Check if the app has validation errors. This checks for global shiny validation errors. |
|
140 |
expect_validation_error = function() { |
|
141 | ! |
testthat::expect_false( |
142 | ! |
is.null(self$get_html(".shiny-output-error-validation")), |
143 | ! |
info = "Validation error is not observed" |
144 |
) |
|
145 |
}, |
|
146 |
#' @description |
|
147 |
#' Set the input in the `teal` app. |
|
148 |
#' |
|
149 |
#' @param input_id (character) The shiny input id with it's complete name space. |
|
150 |
#' @param value The value to set the input to. |
|
151 |
#' @param ... Additional arguments to be passed to `shinytest2::AppDriver$set_inputs` |
|
152 |
#' |
|
153 |
#' @return The `TealAppDriver` object invisibly. |
|
154 |
set_input = function(input_id, value, ...) { |
|
155 | ! |
do.call( |
156 | ! |
self$set_inputs, |
157 | ! |
c(setNames(list(value), input_id), list(...)) |
158 |
) |
|
159 | ! |
invisible(self) |
160 |
}, |
|
161 |
#' @description |
|
162 |
#' Navigate the teal tabs in the `teal` app. |
|
163 |
#' |
|
164 |
#' @param tabs (character) Labels of tabs to navigate to. The order of the tabs is important, |
|
165 |
#' and it should start with the most parent level tab. |
|
166 |
#' Note: In case the teal tab group has duplicate names, the first tab will be selected, |
|
167 |
#' If you wish to select the second tab with the same name, use the suffix "_1". |
|
168 |
#' If you wish to select the third tab with the same name, use the suffix "_2" and so on. |
|
169 |
#' |
|
170 |
#' @return The `TealAppDriver` object invisibly. |
|
171 |
navigate_teal_tab = function(tabs) { |
|
172 | ! |
checkmate::check_character(tabs, min.len = 1) |
173 | ! |
for (tab in tabs) { |
174 | ! |
self$set_input( |
175 | ! |
"teal-teal_modules-active_tab", |
176 | ! |
get_unique_labels(tab), |
177 | ! |
wait_ = FALSE |
178 |
) |
|
179 |
} |
|
180 | ! |
self$wait_for_idle() |
181 | ! |
private$set_active_ns() |
182 | ! |
invisible(self) |
183 |
}, |
|
184 |
#' @description |
|
185 |
#' Get the active shiny name space for different components of the teal app. |
|
186 |
#' |
|
187 |
#' @return (`list`) The list of active shiny name space of the teal components. |
|
188 |
active_ns = function() { |
|
189 | ! |
if (identical(private$ns$module, character(0))) { |
190 | ! |
private$set_active_ns() |
191 |
} |
|
192 | ! |
private$ns |
193 |
}, |
|
194 |
#' @description |
|
195 |
#' Get the active shiny name space for interacting with the module content. |
|
196 |
#' |
|
197 |
#' @return (`string`) The active shiny name space of the component. |
|
198 |
active_module_ns = function() { |
|
199 | ! |
if (identical(private$ns$module, character(0))) { |
200 | ! |
private$set_active_ns() |
201 |
} |
|
202 | ! |
private$ns$module |
203 |
}, |
|
204 |
#' @description |
|
205 |
#' Get the active shiny name space bound with a custom `element` name. |
|
206 |
#' |
|
207 |
#' @param element `character(1)` custom element name. |
|
208 |
#' |
|
209 |
#' @return (`string`) The active shiny name space of the component bound with the input `element`. |
|
210 |
active_module_element = function(element) { |
|
211 | ! |
checkmate::assert_string(element) |
212 | ! |
sprintf("#%s-%s", self$active_module_ns(), element) |
213 |
}, |
|
214 |
#' @description |
|
215 |
#' Get the text of the active shiny name space bound with a custom `element` name. |
|
216 |
#' |
|
217 |
#' @param element `character(1)` the text of the custom element name. |
|
218 |
#' |
|
219 |
#' @return (`string`) The text of the active shiny name space of the component bound with the input `element`. |
|
220 |
active_module_element_text = function(element) { |
|
221 | ! |
checkmate::assert_string(element) |
222 | ! |
self$get_text(self$active_module_element(element)) |
223 |
}, |
|
224 |
#' @description |
|
225 |
#' Get the active shiny name space for interacting with the filter panel. |
|
226 |
#' |
|
227 |
#' @return (`string`) The active shiny name space of the component. |
|
228 |
active_filters_ns = function() { |
|
229 | ! |
if (identical(private$ns$filter_panel, character(0))) { |
230 | ! |
private$set_active_ns() |
231 |
} |
|
232 | ! |
private$ns$filter_panel |
233 |
}, |
|
234 |
#' @description |
|
235 |
#' Get the active shiny name space for interacting with the data-summary panel. |
|
236 |
#' |
|
237 |
#' @return (`string`) The active shiny name space of the data-summary component. |
|
238 |
active_data_summary_ns = function() { |
|
239 | ! |
if (identical(private$ns$data_summary, character(0))) { |
240 | ! |
private$set_active_ns() |
241 |
} |
|
242 | ! |
private$ns$data_summary |
243 |
}, |
|
244 |
#' @description |
|
245 |
#' Get the active shiny name space bound with a custom `element` name. |
|
246 |
#' |
|
247 |
#' @param element `character(1)` custom element name. |
|
248 |
#' |
|
249 |
#' @return (`string`) The active shiny name space of the component bound with the input `element`. |
|
250 |
active_data_summary_element = function(element) { |
|
251 | ! |
checkmate::assert_string(element) |
252 | ! |
sprintf("#%s-%s", self$active_data_summary_ns(), element) |
253 |
}, |
|
254 |
#' @description |
|
255 |
#' Get the input from the module in the `teal` app. |
|
256 |
#' This function will only access inputs from the name space of the current active teal module. |
|
257 |
#' |
|
258 |
#' @param input_id (character) The shiny input id to get the value from. |
|
259 |
#' |
|
260 |
#' @return The value of the shiny input. |
|
261 |
get_active_module_input = function(input_id) { |
|
262 | ! |
checkmate::check_string(input_id) |
263 | ! |
self$get_value(input = sprintf("%s-%s", self$active_module_ns(), input_id)) |
264 |
}, |
|
265 |
#' @description |
|
266 |
#' Get the output from the module in the `teal` app. |
|
267 |
#' This function will only access outputs from the name space of the current active teal module. |
|
268 |
#' |
|
269 |
#' @param output_id (character) The shiny output id to get the value from. |
|
270 |
#' |
|
271 |
#' @return The value of the shiny output. |
|
272 |
get_active_module_output = function(output_id) { |
|
273 | ! |
checkmate::check_string(output_id) |
274 | ! |
self$get_value(output = sprintf("%s-%s", self$active_module_ns(), output_id)) |
275 |
}, |
|
276 |
#' @description |
|
277 |
#' Get the output from the module's `teal.widgets::table_with_settings` or `DT::DTOutput` in the `teal` app. |
|
278 |
#' This function will only access outputs from the name space of the current active teal module. |
|
279 |
#' |
|
280 |
#' @param table_id (`character(1)`) The id of the table in the active teal module's name space. |
|
281 |
#' @param which (integer) If there is more than one table, which should be extracted. |
|
282 |
#' By default it will look for a table that is built using `teal.widgets::table_with_settings`. |
|
283 |
#' |
|
284 |
#' @return The data.frame with table contents. |
|
285 |
get_active_module_table_output = function(table_id, which = 1) { |
|
286 | ! |
checkmate::check_number(which, lower = 1) |
287 | ! |
checkmate::check_string(table_id) |
288 | ! |
table <- rvest::html_table( |
289 | ! |
self$get_html_rvest(self$active_module_element(table_id)), |
290 | ! |
fill = TRUE |
291 |
) |
|
292 | ! |
if (length(table) == 0) { |
293 | ! |
data.frame() |
294 |
} else { |
|
295 | ! |
table[[which]] |
296 |
} |
|
297 |
}, |
|
298 |
#' @description |
|
299 |
#' Get the output from the module's `teal.widgets::plot_with_settings` in the `teal` app. |
|
300 |
#' This function will only access plots from the name space of the current active teal module. |
|
301 |
#' |
|
302 |
#' @param plot_id (`character(1)`) The id of the plot in the active teal module's name space. |
|
303 |
#' |
|
304 |
#' @return The `src` attribute as `character(1)` vector. |
|
305 |
get_active_module_plot_output = function(plot_id) { |
|
306 | ! |
checkmate::check_string(plot_id) |
307 | ! |
self$get_attr( |
308 | ! |
self$active_module_element(sprintf("%s-plot_main > img", plot_id)), |
309 | ! |
"src" |
310 |
) |
|
311 |
}, |
|
312 |
#' @description |
|
313 |
#' Set the input in the module in the `teal` app. |
|
314 |
#' This function will only set inputs in the name space of the current active teal module. |
|
315 |
#' |
|
316 |
#' @param input_id (character) The shiny input id to get the value from. |
|
317 |
#' @param value The value to set the input to. |
|
318 |
#' @param ... Additional arguments to be passed to `shinytest2::AppDriver$set_inputs` |
|
319 |
#' |
|
320 |
#' @return The `TealAppDriver` object invisibly. |
|
321 |
set_active_module_input = function(input_id, value, ...) { |
|
322 | ! |
checkmate::check_string(input_id) |
323 | ! |
checkmate::check_string(value) |
324 | ! |
self$set_input( |
325 | ! |
sprintf("%s-%s", self$active_module_ns(), input_id), |
326 | ! |
value, |
327 |
... |
|
328 |
) |
|
329 | ! |
dots <- rlang::list2(...) |
330 | ! |
if (!isFALSE(dots[["wait"]])) self$wait_for_idle() # Default behavior is to wait |
331 | ! |
invisible(self) |
332 |
}, |
|
333 |
#' @description |
|
334 |
#' Get the active datasets that can be accessed via the filter panel of the current active teal module. |
|
335 |
get_active_filter_vars = function() { |
|
336 | ! |
displayed_datasets_index <- self$is_visible( |
337 | ! |
sprintf("#%s-filters-filter_active_vars_contents > span", self$active_filters_ns()) |
338 |
) |
|
339 | ||
340 | ! |
available_datasets <- self$get_text( |
341 | ! |
sprintf( |
342 | ! |
"#%s-filters-filter_active_vars_contents .filter_panel_dataname", |
343 | ! |
self$active_filters_ns() |
344 |
) |
|
345 |
) |
|
346 | ||
347 | ! |
available_datasets[displayed_datasets_index] |
348 |
}, |
|
349 |
#' @description |
|
350 |
#' Get the active data summary table |
|
351 |
#' @return `data.frame` |
|
352 |
get_active_data_summary_table = function() { |
|
353 | ! |
summary_table <- rvest::html_table( |
354 | ! |
self$get_html_rvest(self$active_data_summary_element("table")), |
355 | ! |
fill = TRUE |
356 | ! |
)[[1]] |
357 | ||
358 | ! |
col_names <- unlist(summary_table[1, ], use.names = FALSE) |
359 | ! |
summary_table <- summary_table[-1, ] |
360 | ! |
colnames(summary_table) <- col_names |
361 | ! |
if (nrow(summary_table) > 0) { |
362 | ! |
summary_table |
363 |
} else { |
|
364 | ! |
NULL |
365 |
} |
|
366 |
}, |
|
367 |
#' @description |
|
368 |
#' Test if `DOM` elements are visible on the page with a JavaScript call. |
|
369 |
#' @param selector (`character(1)`) `CSS` selector to check visibility. |
|
370 |
#' A `CSS` id will return only one element if the UI is well formed. |
|
371 |
#' @param content_visibility_auto,opacity_property,visibility_property (`logical(1)`) See more information |
|
372 |
#' on <https://developer.mozilla.org/en-US/docs/Web/API/Element/checkVisibility>. |
|
373 |
#' |
|
374 |
#' @return Logical vector with all occurrences of the selector. |
|
375 |
is_visible = function(selector, |
|
376 |
content_visibility_auto = FALSE, |
|
377 |
opacity_property = FALSE, |
|
378 |
visibility_property = FALSE) { |
|
379 | ! |
checkmate::assert_string(selector) |
380 | ! |
checkmate::assert_flag(content_visibility_auto) |
381 | ! |
checkmate::assert_flag(opacity_property) |
382 | ! |
checkmate::assert_flag(visibility_property) |
383 | ||
384 | ! |
private$wait_for_page_stability() |
385 | ||
386 | ! |
testthat::skip_if_not( |
387 | ! |
self$get_js("typeof Element.prototype.checkVisibility === 'function'"), |
388 | ! |
"Element.prototype.checkVisibility is not supported in the current browser." |
389 |
) |
|
390 | ||
391 | ! |
unlist( |
392 | ! |
self$get_js( |
393 | ! |
sprintf( |
394 | ! |
"Array.from(document.querySelectorAll('%s')).map(el => el.checkVisibility({%s, %s, %s}))", |
395 | ! |
selector, |
396 |
# Extra parameters |
|
397 | ! |
sprintf("contentVisibilityAuto: %s", tolower(content_visibility_auto)), |
398 | ! |
sprintf("opacityProperty: %s", tolower(opacity_property)), |
399 | ! |
sprintf("visibilityProperty: %s", tolower(visibility_property)) |
400 |
) |
|
401 |
) |
|
402 |
) |
|
403 |
}, |
|
404 |
#' @description |
|
405 |
#' Get the active filter variables from a dataset in the `teal` app. |
|
406 |
#' |
|
407 |
#' @param dataset_name (character) The name of the dataset to get the filter variables from. |
|
408 |
#' If `NULL`, the filter variables for all the datasets will be returned in a list. |
|
409 |
get_active_data_filters = function(dataset_name = NULL) { |
|
410 | ! |
checkmate::check_string(dataset_name, null.ok = TRUE) |
411 | ! |
datasets <- self$get_active_filter_vars() |
412 | ! |
checkmate::assert_subset(dataset_name, datasets) |
413 | ! |
active_filters <- lapply( |
414 | ! |
datasets, |
415 | ! |
function(x) { |
416 | ! |
var_names <- gsub( |
417 | ! |
pattern = "\\s", |
418 | ! |
replacement = "", |
419 | ! |
self$get_text( |
420 | ! |
sprintf( |
421 | ! |
"#%s-filters-%s .filter-card-varname", |
422 | ! |
self$active_filters_ns(), |
423 | ! |
x |
424 |
) |
|
425 |
) |
|
426 |
) |
|
427 | ! |
structure( |
428 | ! |
lapply(var_names, private$get_active_filter_selection, dataset_name = x), |
429 | ! |
names = var_names |
430 |
) |
|
431 |
} |
|
432 |
) |
|
433 | ! |
names(active_filters) <- datasets |
434 | ! |
if (is.null(dataset_name)) { |
435 | ! |
return(active_filters) |
436 |
} |
|
437 | ! |
active_filters[[dataset_name]] |
438 |
}, |
|
439 |
#' @description |
|
440 |
#' Add a new variable from the dataset to be filtered. |
|
441 |
#' |
|
442 |
#' @param dataset_name (character) The name of the dataset to add the filter variable to. |
|
443 |
#' @param var_name (character) The name of the variable to add to the filter panel. |
|
444 |
#' @param ... Additional arguments to be passed to `shinytest2::AppDriver$set_inputs` |
|
445 |
#' |
|
446 |
#' @return The `TealAppDriver` object invisibly. |
|
447 |
add_filter_var = function(dataset_name, var_name, ...) { |
|
448 | ! |
checkmate::check_string(dataset_name) |
449 | ! |
checkmate::check_string(var_name) |
450 | ! |
private$set_active_ns() |
451 | ! |
self$click( |
452 | ! |
selector = sprintf( |
453 | ! |
"#%s-filters-%s-add_filter_icon", |
454 | ! |
private$ns$filter_panel, |
455 | ! |
dataset_name |
456 |
) |
|
457 |
) |
|
458 | ! |
self$set_input( |
459 | ! |
sprintf( |
460 | ! |
"%s-filters-%s-%s-filter-var_to_add", |
461 | ! |
private$ns$filter_panel, |
462 | ! |
dataset_name, |
463 | ! |
dataset_name |
464 |
), |
|
465 | ! |
var_name, |
466 |
... |
|
467 |
) |
|
468 | ! |
invisible(self) |
469 |
}, |
|
470 |
#' @description |
|
471 |
#' Remove an active filter variable of a dataset from the active filter variables panel. |
|
472 |
#' |
|
473 |
#' @param dataset_name (character) The name of the dataset to remove the filter variable from. |
|
474 |
#' If `NULL`, all the filter variables will be removed. |
|
475 |
#' @param var_name (character) The name of the variable to remove from the filter panel. |
|
476 |
#' If `NULL`, all the filter variables of the dataset will be removed. |
|
477 |
#' |
|
478 |
#' @return The `TealAppDriver` object invisibly. |
|
479 |
remove_filter_var = function(dataset_name = NULL, var_name = NULL) { |
|
480 | ! |
checkmate::check_string(dataset_name, null.ok = TRUE) |
481 | ! |
checkmate::check_string(var_name, null.ok = TRUE) |
482 | ! |
if (is.null(dataset_name)) { |
483 | ! |
remove_selector <- sprintf( |
484 | ! |
"#%s-active-remove_all_filters", |
485 | ! |
self$active_filters_ns() |
486 |
) |
|
487 | ! |
} else if (is.null(var_name)) { |
488 | ! |
remove_selector <- sprintf( |
489 | ! |
"#%s-active-%s-remove_filters", |
490 | ! |
self$active_filters_ns(), |
491 | ! |
dataset_name |
492 |
) |
|
493 |
} else { |
|
494 | ! |
remove_selector <- sprintf( |
495 | ! |
"#%s-active-%s-filter-%s_%s-remove", |
496 | ! |
self$active_filters_ns(), |
497 | ! |
dataset_name, |
498 | ! |
dataset_name, |
499 | ! |
var_name |
500 |
) |
|
501 |
} |
|
502 | ! |
self$click( |
503 | ! |
selector = remove_selector |
504 |
) |
|
505 | ! |
invisible(self) |
506 |
}, |
|
507 |
#' @description |
|
508 |
#' Set the active filter values for a variable of a dataset in the active filter variable panel. |
|
509 |
#' |
|
510 |
#' @param dataset_name (character) The name of the dataset to set the filter value for. |
|
511 |
#' @param var_name (character) The name of the variable to set the filter value for. |
|
512 |
#' @param input The value to set the filter to. |
|
513 |
#' @param ... Additional arguments to be passed to `shinytest2::AppDriver$set_inputs` |
|
514 |
#' |
|
515 |
#' @return The `TealAppDriver` object invisibly. |
|
516 |
set_active_filter_selection = function(dataset_name, |
|
517 |
var_name, |
|
518 |
input, |
|
519 |
...) { |
|
520 | ! |
checkmate::check_string(dataset_name) |
521 | ! |
checkmate::check_string(var_name) |
522 | ! |
checkmate::check_string(input) |
523 | ||
524 | ! |
input_id_prefix <- sprintf( |
525 | ! |
"%s-filters-%s-filter-%s_%s-inputs", |
526 | ! |
self$active_filters_ns(), |
527 | ! |
dataset_name, |
528 | ! |
dataset_name, |
529 | ! |
var_name |
530 |
) |
|
531 | ||
532 |
# Find the type of filter (based on filter panel) |
|
533 | ! |
supported_suffix <- c("selection", "selection_manual") |
534 | ! |
slices_suffix <- supported_suffix[ |
535 | ! |
match( |
536 | ! |
TRUE, |
537 | ! |
vapply( |
538 | ! |
supported_suffix, |
539 | ! |
function(suffix) { |
540 | ! |
!is.null(self$get_html(sprintf("#%s-%s", input_id_prefix, suffix))) |
541 |
}, |
|
542 | ! |
logical(1) |
543 |
) |
|
544 |
) |
|
545 |
] |
|
546 | ||
547 |
# Generate correct namespace |
|
548 | ! |
slices_input_id <- sprintf( |
549 | ! |
"%s-filters-%s-filter-%s_%s-inputs-%s", |
550 | ! |
self$active_filters_ns(), |
551 | ! |
dataset_name, |
552 | ! |
dataset_name, |
553 | ! |
var_name, |
554 | ! |
slices_suffix |
555 |
) |
|
556 | ||
557 | ! |
if (identical(slices_suffix, "selection_manual")) { |
558 | ! |
checkmate::assert_numeric(input, len = 2) |
559 | ||
560 | ! |
dots <- rlang::list2(...) |
561 | ! |
checkmate::assert_choice(dots$priority_, formals(self$set_inputs)[["priority_"]], null.ok = TRUE) |
562 | ! |
checkmate::assert_flag(dots$wait_, null.ok = TRUE) |
563 | ||
564 | ! |
self$run_js( |
565 | ! |
sprintf( |
566 | ! |
"Shiny.setInputValue('%s:sw.numericRange', [%f, %f], {priority: '%s'})", |
567 | ! |
slices_input_id, |
568 | ! |
input[[1]], |
569 | ! |
input[[2]], |
570 | ! |
priority_ = ifelse(is.null(dots$priority_), "input", dots$priority_) |
571 |
) |
|
572 |
) |
|
573 | ||
574 | ! |
if (isTRUE(dots$wait_) || is.null(dots$wait_)) { |
575 | ! |
self$wait_for_idle( |
576 | ! |
timeout = if (is.null(dots$timeout_)) rlang::missing_arg() else dots$timeout_ |
577 |
) |
|
578 |
} |
|
579 | ! |
} else if (identical(slices_suffix, "selection")) { |
580 | ! |
self$set_input( |
581 | ! |
slices_input_id, |
582 | ! |
input, |
583 |
... |
|
584 |
) |
|
585 |
} else { |
|
586 | ! |
stop("Filter selection set not supported for this slice.") |
587 |
} |
|
588 | ||
589 | ! |
invisible(self) |
590 |
}, |
|
591 |
#' @description |
|
592 |
#' Extract `html` attribute (found by a `selector`). |
|
593 |
#' |
|
594 |
#' @param selector (`character(1)`) specifying the selector to be used to get the content of a specific node. |
|
595 |
#' @param attribute (`character(1)`) name of an attribute to retrieve from a node specified by `selector`. |
|
596 |
#' |
|
597 |
#' @return The `character` vector. |
|
598 |
get_attr = function(selector, attribute) { |
|
599 | ! |
rvest::html_attr( |
600 | ! |
rvest::html_nodes(self$get_html_rvest("html"), selector), |
601 | ! |
attribute |
602 |
) |
|
603 |
}, |
|
604 |
#' @description |
|
605 |
#' Wrapper around `get_html` that passes the output directly to `rvest::read_html`. |
|
606 |
#' |
|
607 |
#' @param selector `(character(1))` passed to `get_html`. |
|
608 |
#' |
|
609 |
#' @return An XML document. |
|
610 |
get_html_rvest = function(selector) { |
|
611 | ! |
rvest::read_html(self$get_html(selector)) |
612 |
}, |
|
613 |
#' Wrapper around `get_url()` method that opens the app in the browser. |
|
614 |
#' |
|
615 |
#' @return Nothing. Opens the underlying teal app in the browser. |
|
616 |
open_url = function() { |
|
617 | ! |
browseURL(self$get_url()) |
618 |
}, |
|
619 |
#' @description |
|
620 |
#' Waits until a specified input, output, or export value. |
|
621 |
#' This function serves as a wrapper around the `wait_for_value` method, |
|
622 |
#' providing a more flexible interface for waiting on different types of values within the active module namespace. |
|
623 |
#' @param input,output,export A name of an input, output, or export value. |
|
624 |
#' Only one of these parameters may be used. |
|
625 |
#' @param ... Must be empty. Allows for parameter expansion. |
|
626 |
#' Parameter with additional value to passed in `wait_for_value`. |
|
627 |
wait_for_active_module_value = function(input = rlang::missing_arg(), |
|
628 |
output = rlang::missing_arg(), |
|
629 |
export = rlang::missing_arg(), |
|
630 |
...) { |
|
631 | ! |
ns <- shiny::NS(self$active_module_ns()) |
632 | ||
633 | ! |
if (!rlang::is_missing(input) && checkmate::test_string(input, min.chars = 1)) input <- ns(input) |
634 | ! |
if (!rlang::is_missing(output) && checkmate::test_string(output, min.chars = 1)) output <- ns(output) |
635 | ! |
if (!rlang::is_missing(export) && checkmate::test_string(export, min.chars = 1)) export <- ns(export) |
636 | ||
637 | ! |
self$wait_for_value( |
638 | ! |
input = input, |
639 | ! |
output = output, |
640 | ! |
export = export, |
641 |
... |
|
642 |
) |
|
643 |
} |
|
644 |
), |
|
645 |
# private members ---- |
|
646 |
private = list( |
|
647 |
# private attributes ---- |
|
648 |
data = NULL, |
|
649 |
modules = NULL, |
|
650 |
filter = teal_slices(), |
|
651 |
ns = list( |
|
652 |
module = character(0), |
|
653 |
filter_panel = character(0) |
|
654 |
), |
|
655 |
# private methods ---- |
|
656 |
set_active_ns = function() { |
|
657 | ! |
all_inputs <- self$get_values()$input |
658 | ! |
active_tab_inputs <- all_inputs[grepl("-active_tab$", names(all_inputs))] |
659 | ||
660 | ! |
tab_ns <- unlist(lapply(names(active_tab_inputs), function(name) { |
661 | ! |
gsub( |
662 | ! |
pattern = "-active_tab$", |
663 | ! |
replacement = sprintf("-%s", active_tab_inputs[[name]]), |
664 | ! |
name |
665 |
) |
|
666 |
})) |
|
667 | ! |
active_ns <- tab_ns[1] |
668 | ! |
if (length(tab_ns) > 1) { |
669 | ! |
for (i in 2:length(tab_ns)) { |
670 | ! |
next_ns <- tab_ns[i] |
671 | ! |
if (grepl(pattern = active_ns, next_ns)) { |
672 | ! |
active_ns <- next_ns |
673 |
} |
|
674 |
} |
|
675 |
} |
|
676 | ! |
private$ns$module <- sprintf("%s-%s", active_ns, "module") |
677 | ||
678 | ! |
components <- c("filter_panel", "data_summary") |
679 | ! |
for (component in components) { |
680 |
if ( |
|
681 | ! |
!is.null(self$get_html(sprintf("#%s-%s-panel", active_ns, component))) || |
682 | ! |
!is.null(self$get_html(sprintf("#%s-%s-table", active_ns, component))) |
683 |
) { |
|
684 | ! |
private$ns[[component]] <- sprintf("%s-%s", active_ns, component) |
685 |
} else { |
|
686 | ! |
private$ns[[component]] <- sprintf("%s-module_%s", active_ns, component) |
687 |
} |
|
688 |
} |
|
689 |
}, |
|
690 |
# @description |
|
691 |
# Get the active filter values from the active filter selection of dataset from the filter panel. |
|
692 |
# |
|
693 |
# @param dataset_name (character) The name of the dataset to get the filter values from. |
|
694 |
# @param var_name (character) The name of the variable to get the filter values from. |
|
695 |
# |
|
696 |
# @return The value of the active filter selection. |
|
697 |
get_active_filter_selection = function(dataset_name, var_name) { |
|
698 | ! |
checkmate::check_string(dataset_name) |
699 | ! |
checkmate::check_string(var_name) |
700 | ! |
input_id_prefix <- sprintf( |
701 | ! |
"%s-filters-%s-filter-%s_%s-inputs", |
702 | ! |
self$active_filters_ns(), |
703 | ! |
dataset_name, |
704 | ! |
dataset_name, |
705 | ! |
var_name |
706 |
) |
|
707 | ||
708 |
# Find the type of filter (categorical or range) |
|
709 | ! |
supported_suffix <- c("selection", "selection_manual") |
710 | ! |
for (suffix in supported_suffix) { |
711 | ! |
if (!is.null(self$get_html(sprintf("#%s-%s", input_id_prefix, suffix)))) { |
712 | ! |
return(self$get_value(input = sprintf("%s-%s", input_id_prefix, suffix))) |
713 |
} |
|
714 |
} |
|
715 | ||
716 | ! |
NULL # If there are not any supported filters |
717 |
}, |
|
718 |
# @description |
|
719 |
# Check if the page is stable without any `DOM` updates in the body of the app. |
|
720 |
# This is achieved by blocing the R process by sleeping until the page is unchanged till the `stability_period`. |
|
721 |
# @param stability_period (`numeric(1)`) The time in milliseconds to wait till the page to be stable. |
|
722 |
# @param check_interval (`numeric(1)`) The time in milliseconds to check for changes in the page. |
|
723 |
# The stability check is reset when a change is detected in the page after sleeping for check_interval. |
|
724 |
wait_for_page_stability = function(stability_period = 2000, check_interval = 200) { |
|
725 | ! |
previous_content <- self$get_html("body") |
726 | ! |
end_time <- Sys.time() + (stability_period / 1000) |
727 | ||
728 | ! |
repeat { |
729 | ! |
Sys.sleep(check_interval / 1000) |
730 | ! |
current_content <- self$get_html("body") |
731 | ||
732 | ! |
if (!identical(previous_content, current_content)) { |
733 | ! |
previous_content <- current_content |
734 | ! |
end_time <- Sys.time() + (stability_period / 1000) |
735 | ! |
} else if (Sys.time() >= end_time) { |
736 | ! |
break |
737 |
} |
|
738 |
} |
|
739 |
} |
|
740 |
) |
|
741 |
) |
1 |
#' Validate that dataset has a minimum number of observations |
|
2 |
#' |
|
3 |
#' `r lifecycle::badge("stable")` |
|
4 |
#' |
|
5 |
#' This function is a wrapper for `shiny::validate`. |
|
6 |
#' |
|
7 |
#' @param x (`data.frame`) |
|
8 |
#' @param min_nrow (`numeric(1)`) Minimum allowed number of rows in `x`. |
|
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. |
|
12 |
#' |
|
13 |
#' @export |
|
14 |
#' |
|
15 |
#' @examples |
|
16 |
#' library(teal) |
|
17 |
#' ui <- fluidPage( |
|
18 |
#' sliderInput("len", "Max Length of Sepal", |
|
19 |
#' min = 4.3, max = 7.9, value = 5 |
|
20 |
#' ), |
|
21 |
#' plotOutput("plot") |
|
22 |
#' ) |
|
23 |
#' |
|
24 |
#' server <- function(input, output) { |
|
25 |
#' output$plot <- renderPlot({ |
|
26 |
#' iris_df <- iris[iris$Sepal.Length <= input$len, ] |
|
27 |
#' validate_has_data( |
|
28 |
#' iris_df, |
|
29 |
#' min_nrow = 10, |
|
30 |
#' complete = FALSE, |
|
31 |
#' msg = "Please adjust Max Length of Sepal" |
|
32 |
#' ) |
|
33 |
#' |
|
34 |
#' hist(iris_df$Sepal.Length, breaks = 5) |
|
35 |
#' }) |
|
36 |
#' } |
|
37 |
#' if (interactive()) { |
|
38 |
#' shinyApp(ui, server) |
|
39 |
#' } |
|
40 |
#' |
|
41 |
validate_has_data <- function(x, |
|
42 |
min_nrow = NULL, |
|
43 |
complete = FALSE, |
|
44 |
allow_inf = TRUE, |
|
45 |
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 |
)) |
|
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" |
61 |
) |
|
62 |
)) |
|
63 |
} |
|
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 |
#' |
|
76 |
#' `r lifecycle::badge("stable")` |
|
77 |
#' |
|
78 |
#' This function is a wrapper for `shiny::validate`. |
|
79 |
#' |
|
80 |
#' @param x (`data.frame`) |
|
81 |
#' @param key (`character`) Vector of ID variables from `x` that identify unique records. |
|
82 |
#' |
|
83 |
#' @export |
|
84 |
#' |
|
85 |
#' @examples |
|
86 |
#' iris$id <- rep(1:50, times = 3) |
|
87 |
#' ui <- fluidPage( |
|
88 |
#' selectInput( |
|
89 |
#' inputId = "species", |
|
90 |
#' label = "Select species", |
|
91 |
#' choices = c("setosa", "versicolor", "virginica"), |
|
92 |
#' selected = "setosa", |
|
93 |
#' multiple = TRUE |
|
94 |
#' ), |
|
95 |
#' plotOutput("plot") |
|
96 |
#' ) |
|
97 |
#' server <- function(input, output) { |
|
98 |
#' output$plot <- renderPlot({ |
|
99 |
#' iris_f <- iris[iris$Species %in% input$species, ] |
|
100 |
#' validate_one_row_per_id(iris_f, key = c("id")) |
|
101 |
#' |
|
102 |
#' hist(iris_f$Sepal.Length, breaks = 5) |
|
103 |
#' }) |
|
104 |
#' } |
|
105 |
#' if (interactive()) { |
|
106 |
#' shinyApp(ui, server) |
|
107 |
#' } |
|
108 |
#' |
|
109 |
validate_one_row_per_id <- function(x, key = c("USUBJID", "STUDYID")) { |
|
110 | ! |
validate(need(!any(duplicated(x[key])), paste("Found more than one row per id."))) |
111 |
} |
|
112 | ||
113 |
#' Validates that vector includes all expected values |
|
114 |
#' |
|
115 |
#' `r lifecycle::badge("stable")` |
|
116 |
#' |
|
117 |
#' This function is a wrapper for `shiny::validate`. |
|
118 |
#' |
|
119 |
#' @param x Vector of values to test. |
|
120 |
#' @param choices Vector to test against. |
|
121 |
#' @param msg (`character(1)`) Error message to display if some elements of `x` are not elements of `choices`. |
|
122 |
#' |
|
123 |
#' @export |
|
124 |
#' |
|
125 |
#' @examples |
|
126 |
#' ui <- fluidPage( |
|
127 |
#' selectInput( |
|
128 |
#' "species", |
|
129 |
#' "Select species", |
|
130 |
#' choices = c("setosa", "versicolor", "virginica", "unknown species"), |
|
131 |
#' selected = "setosa", |
|
132 |
#' multiple = FALSE |
|
133 |
#' ), |
|
134 |
#' verbatimTextOutput("summary") |
|
135 |
#' ) |
|
136 |
#' |
|
137 |
#' server <- function(input, output) { |
|
138 |
#' output$summary <- renderPrint({ |
|
139 |
#' validate_in(input$species, iris$Species, "Species does not exist.") |
|
140 |
#' nrow(iris[iris$Species == input$species, ]) |
|
141 |
#' }) |
|
142 |
#' } |
|
143 |
#' if (interactive()) { |
|
144 |
#' shinyApp(ui, server) |
|
145 |
#' } |
|
146 |
#' |
|
147 |
validate_in <- function(x, choices, msg) { |
|
148 | ! |
validate(need(length(x) > 0 && length(choices) > 0 && all(x %in% choices), msg)) |
149 |
} |
|
150 | ||
151 |
#' Validates that vector has length greater than 0 |
|
152 |
#' |
|
153 |
#' `r lifecycle::badge("stable")` |
|
154 |
#' |
|
155 |
#' This function is a wrapper for `shiny::validate`. |
|
156 |
#' |
|
157 |
#' @param x vector |
|
158 |
#' @param msg message to display |
|
159 |
#' |
|
160 |
#' @export |
|
161 |
#' |
|
162 |
#' @examples |
|
163 |
#' data <- data.frame( |
|
164 |
#' id = c(1:10, 11:20, 1:10), |
|
165 |
#' strata = rep(c("A", "B"), each = 15) |
|
166 |
#' ) |
|
167 |
#' ui <- fluidPage( |
|
168 |
#' selectInput("ref1", "Select strata1 to compare", |
|
169 |
#' choices = c("A", "B", "C"), selected = "A" |
|
170 |
#' ), |
|
171 |
#' selectInput("ref2", "Select strata2 to compare", |
|
172 |
#' choices = c("A", "B", "C"), selected = "B" |
|
173 |
#' ), |
|
174 |
#' verbatimTextOutput("arm_summary") |
|
175 |
#' ) |
|
176 |
#' |
|
177 |
#' server <- function(input, output) { |
|
178 |
#' output$arm_summary <- renderText({ |
|
179 |
#' sample_1 <- data$id[data$strata == input$ref1] |
|
180 |
#' sample_2 <- data$id[data$strata == input$ref2] |
|
181 |
#' |
|
182 |
#' validate_has_elements(sample_1, "No subjects in strata1.") |
|
183 |
#' validate_has_elements(sample_2, "No subjects in strata2.") |
|
184 |
#' |
|
185 |
#' paste0( |
|
186 |
#' "Number of samples in: strata1=", length(sample_1), |
|
187 |
#' " comparions strata2=", length(sample_2) |
|
188 |
#' ) |
|
189 |
#' }) |
|
190 |
#' } |
|
191 |
#' if (interactive()) { |
|
192 |
#' shinyApp(ui, server) |
|
193 |
#' } |
|
194 |
validate_has_elements <- function(x, msg) { |
|
195 | ! |
validate(need(length(x) > 0, msg)) |
196 |
} |
|
197 | ||
198 |
#' Validates no intersection between two vectors |
|
199 |
#' |
|
200 |
#' `r lifecycle::badge("stable")` |
|
201 |
#' |
|
202 |
#' This function is a wrapper for `shiny::validate`. |
|
203 |
#' |
|
204 |
#' @param x vector |
|
205 |
#' @param y vector |
|
206 |
#' @param msg (`character(1)`) message to display if `x` and `y` intersect |
|
207 |
#' |
|
208 |
#' @export |
|
209 |
#' |
|
210 |
#' @examples |
|
211 |
#' data <- data.frame( |
|
212 |
#' id = c(1:10, 11:20, 1:10), |
|
213 |
#' strata = rep(c("A", "B", "C"), each = 10) |
|
214 |
#' ) |
|
215 |
#' |
|
216 |
#' ui <- fluidPage( |
|
217 |
#' selectInput("ref1", "Select strata1 to compare", |
|
218 |
#' choices = c("A", "B", "C"), |
|
219 |
#' selected = "A" |
|
220 |
#' ), |
|
221 |
#' selectInput("ref2", "Select strata2 to compare", |
|
222 |
#' choices = c("A", "B", "C"), |
|
223 |
#' selected = "B" |
|
224 |
#' ), |
|
225 |
#' verbatimTextOutput("summary") |
|
226 |
#' ) |
|
227 |
#' |
|
228 |
#' server <- function(input, output) { |
|
229 |
#' output$summary <- renderText({ |
|
230 |
#' sample_1 <- data$id[data$strata == input$ref1] |
|
231 |
#' sample_2 <- data$id[data$strata == input$ref2] |
|
232 |
#' |
|
233 |
#' validate_no_intersection( |
|
234 |
#' sample_1, sample_2, |
|
235 |
#' "subjects within strata1 and strata2 cannot overlap" |
|
236 |
#' ) |
|
237 |
#' paste0( |
|
238 |
#' "Number of subject in: reference treatment=", length(sample_1), |
|
239 |
#' " comparions treatment=", length(sample_2) |
|
240 |
#' ) |
|
241 |
#' }) |
|
242 |
#' } |
|
243 |
#' if (interactive()) { |
|
244 |
#' shinyApp(ui, server) |
|
245 |
#' } |
|
246 |
#' |
|
247 |
validate_no_intersection <- function(x, y, msg) { |
|
248 | ! |
validate(need(length(intersect(x, y)) == 0, msg)) |
249 |
} |
|
250 | ||
251 | ||
252 |
#' Validates that dataset contains specific variable |
|
253 |
#' |
|
254 |
#' `r lifecycle::badge("stable")` |
|
255 |
#' |
|
256 |
#' This function is a wrapper for `shiny::validate`. |
|
257 |
#' |
|
258 |
#' @param data (`data.frame`) |
|
259 |
#' @param varname (`character(1)`) name of variable to check for in `data` |
|
260 |
#' @param msg (`character(1)`) message to display if `data` does not include `varname` |
|
261 |
#' |
|
262 |
#' @export |
|
263 |
#' |
|
264 |
#' @examples |
|
265 |
#' data <- data.frame( |
|
266 |
#' one = rep("a", length.out = 20), |
|
267 |
#' two = rep(c("a", "b"), length.out = 20) |
|
268 |
#' ) |
|
269 |
#' ui <- fluidPage( |
|
270 |
#' selectInput( |
|
271 |
#' "var", |
|
272 |
#' "Select variable", |
|
273 |
#' choices = c("one", "two", "three", "four"), |
|
274 |
#' selected = "one" |
|
275 |
#' ), |
|
276 |
#' verbatimTextOutput("summary") |
|
277 |
#' ) |
|
278 |
#' |
|
279 |
#' server <- function(input, output) { |
|
280 |
#' output$summary <- renderText({ |
|
281 |
#' validate_has_variable(data, input$var) |
|
282 |
#' paste0("Selected treatment variables: ", paste(input$var, collapse = ", ")) |
|
283 |
#' }) |
|
284 |
#' } |
|
285 |
#' if (interactive()) { |
|
286 |
#' shinyApp(ui, server) |
|
287 |
#' } |
|
288 |
validate_has_variable <- function(data, varname, msg) { |
|
289 | ! |
if (length(varname) != 0) { |
290 | ! |
has_vars <- varname %in% names(data) |
291 | ||
292 | ! |
if (!all(has_vars)) { |
293 | ! |
if (missing(msg)) { |
294 | ! |
msg <- sprintf( |
295 | ! |
"%s does not have the required variables: %s.", |
296 | ! |
deparse(substitute(data)), |
297 | ! |
toString(varname[!has_vars]) |
298 |
) |
|
299 |
} |
|
300 | ! |
validate(need(FALSE, msg)) |
301 |
} |
|
302 |
} |
|
303 |
} |
|
304 | ||
305 |
#' Validate that variables has expected number of levels |
|
306 |
#' |
|
307 |
#' `r lifecycle::badge("stable")` |
|
308 |
#' |
|
309 |
#' If the number of levels of `x` is less than `min_levels` |
|
310 |
#' or greater than `max_levels` the validation will fail. |
|
311 |
#' This function is a wrapper for `shiny::validate`. |
|
312 |
#' |
|
313 |
#' @param x variable name. If `x` is not a factor, the unique values |
|
314 |
#' are treated as levels. |
|
315 |
#' @param min_levels cutoff for minimum number of levels of `x` |
|
316 |
#' @param max_levels cutoff for maximum number of levels of `x` |
|
317 |
#' @param var_name name of variable being validated for use in |
|
318 |
#' validation message |
|
319 |
#' |
|
320 |
#' @export |
|
321 |
#' @examples |
|
322 |
#' data <- data.frame( |
|
323 |
#' one = rep("a", length.out = 20), |
|
324 |
#' two = rep(c("a", "b"), length.out = 20), |
|
325 |
#' three = rep(c("a", "b", "c"), length.out = 20), |
|
326 |
#' four = rep(c("a", "b", "c", "d"), length.out = 20), |
|
327 |
#' stringsAsFactors = TRUE |
|
328 |
#' ) |
|
329 |
#' ui <- fluidPage( |
|
330 |
#' selectInput( |
|
331 |
#' "var", |
|
332 |
#' "Select variable", |
|
333 |
#' choices = c("one", "two", "three", "four"), |
|
334 |
#' selected = "one" |
|
335 |
#' ), |
|
336 |
#' verbatimTextOutput("summary") |
|
337 |
#' ) |
|
338 |
#' |
|
339 |
#' server <- function(input, output) { |
|
340 |
#' output$summary <- renderText({ |
|
341 |
#' validate_n_levels(data[[input$var]], min_levels = 2, max_levels = 15, var_name = input$var) |
|
342 |
#' paste0( |
|
343 |
#' "Levels of selected treatment variable: ", |
|
344 |
#' paste(levels(data[[input$var]]), |
|
345 |
#' collapse = ", " |
|
346 |
#' ) |
|
347 |
#' ) |
|
348 |
#' }) |
|
349 |
#' } |
|
350 |
#' if (interactive()) { |
|
351 |
#' shinyApp(ui, server) |
|
352 |
#' } |
|
353 |
validate_n_levels <- function(x, min_levels = 1, max_levels = 12, var_name) { |
|
354 | ! |
x_levels <- if (is.factor(x)) { |
355 | ! |
levels(x) |
356 |
} else { |
|
357 | ! |
unique(x) |
358 |
} |
|
359 | ||
360 | ! |
if (!is.null(min_levels) && !(is.null(max_levels))) { |
361 | ! |
validate(need( |
362 | ! |
length(x_levels) >= min_levels && length(x_levels) <= max_levels, |
363 | ! |
sprintf( |
364 | ! |
"%s variable needs minimum %s level(s) and maximum %s level(s).", |
365 | ! |
var_name, min_levels, max_levels |
366 |
) |
|
367 |
)) |
|
368 | ! |
} else if (!is.null(min_levels)) { |
369 | ! |
validate(need( |
370 | ! |
length(x_levels) >= min_levels, |
371 | ! |
sprintf("%s variable needs minimum %s levels(s)", var_name, min_levels) |
372 |
)) |
|
373 | ! |
} else if (!is.null(max_levels)) { |
374 | ! |
validate(need( |
375 | ! |
length(x_levels) <= max_levels, |
376 | ! |
sprintf("%s variable needs maximum %s level(s)", var_name, max_levels) |
377 |
)) |
|
378 |
} |
|
379 |
} |
1 |
# 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 |
# and it is very end-user oriented. It may also perform more argument checking with more informative |
|
4 |
# error messages. |
|
5 | ||
6 |
#' Create the server and UI function for the `shiny` app |
|
7 |
#' |
|
8 |
#' @description `r lifecycle::badge("stable")` |
|
9 |
#' |
|
10 |
#' End-users: This is the most important function for you to start a |
|
11 |
#' `teal` app that is composed of `teal` modules. |
|
12 |
#' |
|
13 |
#' @param data (`teal_data` or `teal_data_module`) |
|
14 |
#' For constructing the data object, refer to [teal.data::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 |
#' @param modules (`list` or `teal_modules` or `teal_module`) |
|
17 |
#' Nested list of `teal_modules` or `teal_module` objects or a single |
|
18 |
#' `teal_modules` or `teal_module` object. These are the specific output modules which |
|
19 |
#' will be displayed in the `teal` application. See [modules()] and [module()] for |
|
20 |
#' more details. |
|
21 |
#' @param filter (`teal_slices`) Optionally, |
|
22 |
#' specifies the initial filter using [teal_slices()]. |
|
23 |
#' @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 |
#' 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", |
|
83 |
#' global_filters = "new_mtcars cyl" |
|
84 |
#' ) |
|
85 |
#' ), |
|
86 |
#' title = "App title", |
|
87 |
#' header = tags$h1("Sample App"), |
|
88 |
#' footer = tags$p("Sample footer") |
|
89 |
#' ) |
|
90 |
#' if (interactive()) { |
|
91 |
#' shinyApp(app$ui, app$server) |
|
92 |
#' } |
|
93 |
#' |
|
94 |
init <- function(data, |
|
95 |
modules, |
|
96 |
filter = teal_slices(), |
|
97 |
title = build_app_title(), |
|
98 |
header = tags$p(), |
|
99 |
footer = tags$p(), |
|
100 |
id = character(0), |
|
101 |
landing_popup = NULL) { |
|
102 | 14x |
logger::log_debug("init initializing teal app with: data ('{ class(data) }').") |
103 | ||
104 |
# argument checking (independent) |
|
105 |
## `data` |
|
106 | 14x |
checkmate::assert_multi_class(data, c("teal_data", "teal_data_module")) |
107 | 14x |
checkmate::assert_class(landing_popup, "teal_module_landing", null.ok = TRUE) |
108 | ||
109 |
## `modules` |
|
110 | 14x |
checkmate::assert( |
111 | 14x |
.var.name = "modules", |
112 | 14x |
checkmate::check_multi_class(modules, c("teal_modules", "teal_module")), |
113 | 14x |
checkmate::check_list(modules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules")) |
114 |
) |
|
115 | 14x |
if (inherits(modules, "teal_module")) { |
116 | 1x |
modules <- list(modules) |
117 |
} |
|
118 | 14x |
if (checkmate::test_list(modules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules"))) { |
119 | 8x |
modules <- do.call(teal::modules, modules) |
120 |
} |
|
121 | ||
122 |
## `filter` |
|
123 | 14x |
checkmate::assert_class(filter, "teal_slices") |
124 | ||
125 |
## all other arguments |
|
126 | 13x |
checkmate::assert( |
127 | 13x |
.var.name = "title", |
128 | 13x |
checkmate::check_string(title), |
129 | 13x |
checkmate::check_multi_class(title, c("shiny.tag", "shiny.tag.list", "html")) |
130 |
) |
|
131 | 13x |
checkmate::assert( |
132 | 13x |
.var.name = "header", |
133 | 13x |
checkmate::check_string(header), |
134 | 13x |
checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html")) |
135 |
) |
|
136 | 13x |
checkmate::assert( |
137 | 13x |
.var.name = "footer", |
138 | 13x |
checkmate::check_string(footer), |
139 | 13x |
checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html")) |
140 |
) |
|
141 | 13x |
checkmate::assert_character(id, max.len = 1, any.missing = FALSE) |
142 | ||
143 |
# log |
|
144 | 13x |
teal.logger::log_system_info() |
145 | ||
146 |
# argument transformations |
|
147 |
## `modules` - landing module |
|
148 | 13x |
landing <- extract_module(modules, "teal_module_landing") |
149 | 13x |
if (length(landing) == 1L) { |
150 | ! |
landing_popup <- landing[[1L]] |
151 | ! |
modules <- drop_module(modules, "teal_module_landing") |
152 | ! |
lifecycle::deprecate_soft( |
153 | ! |
when = "0.15.3", |
154 | ! |
what = "landing_popup_module()", |
155 | ! |
details = paste( |
156 | ! |
"Pass `landing_popup_module` to the `landing_popup` argument of the `init` ", |
157 | ! |
"instead of wrapping it into `modules()` and passing to the `modules` argument" |
158 |
) |
|
159 |
) |
|
160 | 13x |
} else if (length(landing) > 1L) { |
161 | ! |
stop("Only one `landing_popup_module` can be used.") |
162 |
} |
|
163 | ||
164 |
## `filter` - set app_id attribute unless present (when restoring bookmark) |
|
165 | 13x |
if (is.null(attr(filter, "app_id", exact = TRUE))) attr(filter, "app_id") <- create_app_id(data, modules) |
166 | ||
167 |
## `filter` - convert teal.slice::teal_slices to teal::teal_slices |
|
168 | 13x |
filter <- as.teal_slices(as.list(filter)) |
169 | ||
170 |
# argument checking (interdependent) |
|
171 |
## `filter` - `modules` |
|
172 | 13x |
if (isTRUE(attr(filter, "module_specific"))) { |
173 | ! |
module_names <- unlist(c(module_labels(modules), "global_filters")) |
174 | ! |
failed_mod_names <- setdiff(names(attr(filter, "mapping")), module_names) |
175 | ! |
if (length(failed_mod_names)) { |
176 | ! |
stop( |
177 | ! |
sprintf( |
178 | ! |
"Some module names in the mapping arguments don't match module labels.\n %s not in %s", |
179 | ! |
toString(failed_mod_names), |
180 | ! |
toString(unique(module_names)) |
181 |
) |
|
182 |
) |
|
183 |
} |
|
184 | ||
185 | ! |
if (anyDuplicated(module_names)) { |
186 |
# In teal we are able to set nested modules with duplicated label. |
|
187 |
# Because mapping argument bases on the relationship between module-label and filter-id, |
|
188 |
# it is possible that module-label in mapping might refer to multiple teal_module (identified by the same label) |
|
189 | ! |
stop( |
190 | ! |
sprintf( |
191 | ! |
"Module labels should be unique when teal_slices(mapping = TRUE). Duplicated labels:\n%s ", |
192 | ! |
toString(module_names[duplicated(module_names)]) |
193 |
) |
|
194 |
) |
|
195 |
} |
|
196 |
} |
|
197 | ||
198 |
## `data` - `modules` |
|
199 | 13x |
if (inherits(data, "teal_data")) { |
200 | 12x |
if (length(data) == 0) { |
201 | 1x |
stop("The environment of `data` is empty.") |
202 |
} |
|
203 | ||
204 | 11x |
is_modules_ok <- check_modules_datanames(modules, names(data)) |
205 | 11x |
if (!isTRUE(is_modules_ok) && length(unlist(extract_transformators(modules))) == 0) { |
206 | 4x |
warning(is_modules_ok, call. = FALSE) |
207 |
} |
|
208 | ||
209 | 11x |
is_filter_ok <- check_filter_datanames(filter, names(data)) |
210 | 11x |
if (!isTRUE(is_filter_ok)) { |
211 | 1x |
warning(is_filter_ok) |
212 |
# we allow app to continue if applied filters are outside |
|
213 |
# of possible data range |
|
214 |
} |
|
215 |
} |
|
216 | ||
217 | 12x |
reporter <- teal.reporter::Reporter$new()$set_id(attr(filter, "app_id")) |
218 | 12x |
if (is_arg_used(modules, "reporter") && length(extract_module(modules, "teal_module_previewer")) == 0) { |
219 | ! |
modules <- append_module( |
220 | ! |
modules, |
221 | ! |
reporter_previewer_module(server_args = list(previewer_buttons = c("download", "reset"))) |
222 |
) |
|
223 |
} |
|
224 | ||
225 | 12x |
ns <- NS(id) |
226 |
# Note: UI must be a function to support bookmarking. |
|
227 | 12x |
res <- list( |
228 | 12x |
ui = function(request) { |
229 | ! |
ui_teal( |
230 | ! |
id = ns("teal"), |
231 | ! |
modules = modules, |
232 | ! |
title = title, |
233 | ! |
header = header, |
234 | ! |
footer = footer |
235 |
) |
|
236 |
}, |
|
237 | 12x |
server = function(input, output, session) { |
238 | ! |
if (!is.null(landing_popup)) { |
239 | ! |
do.call(landing_popup$server, c(list(id = "landing_module_shiny_id"), landing_popup$server_args)) |
240 |
} |
|
241 | ! |
srv_teal(id = ns("teal"), data = data, modules = modules, filter = deep_copy_filter(filter)) |
242 |
} |
|
243 |
) |
|
244 | ||
245 | 12x |
logger::log_debug("init teal app has been initialized.") |
246 | ||
247 | 12x |
res |
248 |
} |
1 |
#' Manage multiple `FilteredData` objects |
|
2 |
#' |
|
3 |
#' @description |
|
4 |
#' Oversee filter states across the entire application. |
|
5 |
#' |
|
6 |
#' @section Slices global: |
|
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: |
|
9 |
#' - `all_slices` (`reactiveVal`) - reactive value containing all filters registered in an app. |
|
10 |
#' - `module_slices_api` (`reactiveValues`) - reactive field containing references to each modules' |
|
11 |
#' `FilteredData` object methods. At this moment it is used only in `srv_filter_manager` to display |
|
12 |
#' the filter states in a table combining informations from `all_slices` and from |
|
13 |
#' `FilteredData$get_available_teal_slices()`. |
|
14 |
#' |
|
15 |
#' During a session only new filters are added to `all_slices` unless [`module_snapshot_manager`] is |
|
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: |
|
18 |
#' - If module's filter is added or removed in its `FilteredData` object, this information is passed |
|
19 |
#' to `SlicesGlobal` which updates `attr(, "mapping")` accordingly. |
|
20 |
#' - When mapping changes in a `SlicesGlobal`, filters are set or removed from module's |
|
21 |
#' `FilteredData`. |
|
22 |
#' |
|
23 |
#' @section Filter manager: |
|
24 |
#' Filter-manager is split into two parts: |
|
25 |
#' 1. `ui/srv_filter_manager_panel` - Called once for the whole app. This module observes changes in |
|
26 |
#' the filters in `slices_global` and displays them in a table utilizing information from `mapping`: |
|
27 |
#' - (`TRUE`) - filter is active in the module |
|
28 |
#' - (`FALSE`) - filter is inactive in the module |
|
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 |
|
31 |
#' for of single module and keeping module `FilteredData` consistent with `slices_global`, so that |
|
32 |
#' local filters are always reflected in the `slices_global` and its mapping and vice versa. |
|
33 |
#' |
|
34 |
#' |
|
35 |
#' @param id (`character(1)`) |
|
36 |
#' `shiny` module instance id. |
|
37 |
#' |
|
38 |
#' @param slices_global (`reactiveVal`) |
|
39 |
#' containing `teal_slices`. |
|
40 |
#' |
|
41 |
#' @param module_fd (`FilteredData`) |
|
42 |
#' Object containing the data to be filtered in a single `teal` module. |
|
43 |
#' |
|
44 |
#' @return |
|
45 |
#' Module returns a `slices_global` (`reactiveVal`) containing a `teal_slices` object with mapping. |
|
46 |
#' |
|
47 |
#' @encoding UTF-8 |
|
48 |
#' |
|
49 |
#' @name module_filter_manager |
|
50 |
#' @rdname module_filter_manager |
|
51 |
#' |
|
52 |
NULL |
|
53 | ||
54 |
#' @rdname module_filter_manager |
|
55 |
ui_filter_manager_panel <- function(id) { |
|
56 | ! |
ns <- NS(id) |
57 | ! |
tags$button( |
58 | ! |
id = ns("show_filter_manager"), |
59 | ! |
class = "btn action-button wunder_bar_button", |
60 | ! |
title = "View filter mapping", |
61 | ! |
suppressMessages(icon("fas fa-grip")) |
62 |
) |
|
63 |
} |
|
64 | ||
65 |
#' @rdname module_filter_manager |
|
66 |
#' @keywords internal |
|
67 |
srv_filter_manager_panel <- function(id, slices_global) { |
|
68 | 87x |
checkmate::assert_string(id) |
69 | 87x |
checkmate::assert_class(slices_global, ".slicesGlobal") |
70 | 87x |
moduleServer(id, function(input, output, session) { |
71 | 87x |
setBookmarkExclude(c("show_filter_manager")) |
72 | 87x |
observeEvent(input$show_filter_manager, { |
73 | ! |
logger::log_debug("srv_filter_manager_panel@1 show_filter_manager button has been clicked.") |
74 | ! |
showModal( |
75 | ! |
modalDialog( |
76 | ! |
ui_filter_manager(session$ns("filter_manager")), |
77 | ! |
class = "filter_manager_modal", |
78 | ! |
size = "l", |
79 | ! |
footer = NULL, |
80 | ! |
easyClose = TRUE |
81 |
) |
|
82 |
) |
|
83 |
}) |
|
84 | 87x |
srv_filter_manager("filter_manager", slices_global = slices_global) |
85 |
}) |
|
86 |
} |
|
87 | ||
88 |
#' @rdname module_filter_manager |
|
89 |
ui_filter_manager <- function(id) { |
|
90 | ! |
ns <- NS(id) |
91 | ! |
actionButton(ns("filter_manager"), NULL, icon = icon("fas fa-filter")) |
92 | ! |
tags$div( |
93 | ! |
class = "filter_manager_content", |
94 | ! |
tableOutput(ns("slices_table")) |
95 |
) |
|
96 |
} |
|
97 | ||
98 |
#' @rdname module_filter_manager |
|
99 |
srv_filter_manager <- function(id, slices_global) { |
|
100 | 87x |
checkmate::assert_string(id) |
101 | 87x |
checkmate::assert_class(slices_global, ".slicesGlobal") |
102 | ||
103 | 87x |
moduleServer(id, function(input, output, session) { |
104 | 87x |
logger::log_debug("filter_manager_srv initializing.") |
105 | ||
106 |
# Bookmark slices global with mapping. |
|
107 | 87x |
session$onBookmark(function(state) { |
108 | ! |
logger::log_debug("filter_manager_srv@onBookmark: storing filter state") |
109 | ! |
state$values$filter_state_on_bookmark <- as.list( |
110 | ! |
slices_global$all_slices(), |
111 | ! |
recursive = TRUE |
112 |
) |
|
113 |
}) |
|
114 | ||
115 | 87x |
bookmarked_slices <- restoreValue(session$ns("filter_state_on_bookmark"), NULL) |
116 | 87x |
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 |
} |
|
120 | ||
121 | 87x |
mapping_table <- reactive({ |
122 |
# We want this to be reactive on slices_global$all_slices() only as get_available_teal_slices() |
|
123 |
# is dependent on slices_global$all_slices(). |
|
124 | 96x |
module_labels <- setdiff( |
125 | 96x |
names(attr(slices_global$all_slices(), "mapping")), |
126 | 96x |
"Report previewer" |
127 |
) |
|
128 | 96x |
isolate({ |
129 | 96x |
mm <- as.data.frame( |
130 | 96x |
sapply( |
131 | 96x |
module_labels, |
132 | 96x |
simplify = FALSE, |
133 | 96x |
function(module_label) { |
134 | 109x |
available_slices <- slices_global$module_slices_api[[module_label]]$get_available_teal_slices() |
135 | 101x |
global_ids <- sapply(slices_global$all_slices(), `[[`, "id", simplify = FALSE) |
136 | 101x |
module_ids <- sapply(slices_global$slices_get(module_label), `[[`, "id", simplify = FALSE) |
137 | 101x |
allowed_ids <- vapply(available_slices, `[[`, character(1L), "id") |
138 | 101x |
active_ids <- global_ids %in% module_ids |
139 | 101x |
setNames(nm = global_ids, ifelse(global_ids %in% allowed_ids, active_ids, NA)) |
140 |
} |
|
141 |
), |
|
142 | 96x |
check.names = FALSE |
143 |
) |
|
144 | 88x |
colnames(mm)[colnames(mm) == "global_filters"] <- "Global filters" |
145 | ||
146 | 88x |
mm |
147 |
}) |
|
148 |
}) |
|
149 | ||
150 | 87x |
output$slices_table <- renderTable( |
151 | 87x |
expr = { |
152 | 96x |
logger::log_debug("filter_manager_srv@1 rendering slices_table.") |
153 | 96x |
mm <- mapping_table() |
154 | ||
155 |
# Display logical values as UTF characters. |
|
156 | 88x |
mm[] <- lapply(mm, ifelse, yes = intToUtf8(9989), no = intToUtf8(10060)) |
157 | 88x |
mm[] <- lapply(mm, function(x) ifelse(is.na(x), intToUtf8(128306), x)) |
158 | ||
159 |
# Display placeholder if no filters defined. |
|
160 | 88x |
if (nrow(mm) == 0L) { |
161 | 64x |
mm <- data.frame(`Filter manager` = "No filters specified.", check.names = FALSE) |
162 | 64x |
rownames(mm) <- "" |
163 |
} |
|
164 | 88x |
mm |
165 |
}, |
|
166 | 87x |
rownames = TRUE |
167 |
) |
|
168 | ||
169 | 87x |
mapping_table # for testing purpose |
170 |
}) |
|
171 |
} |
|
172 | ||
173 |
#' @rdname module_filter_manager |
|
174 |
srv_module_filter_manager <- function(id, module_fd, slices_global) { |
|
175 | 112x |
checkmate::assert_string(id) |
176 | 112x |
assert_reactive(module_fd) |
177 | 112x |
checkmate::assert_class(slices_global, ".slicesGlobal") |
178 | ||
179 | 112x |
moduleServer(id, function(input, output, session) { |
180 | 112x |
logger::log_debug("srv_module_filter_manager initializing for module: { id }.") |
181 |
# Track filter global and local states. |
|
182 | 112x |
slices_global_module <- reactive({ |
183 | 201x |
slices_global$slices_get(module_label = id) |
184 |
}) |
|
185 | 112x |
slices_module <- reactive(req(module_fd())$get_filter_state()) |
186 | ||
187 | 112x |
module_fd_previous <- reactiveVal(NULL) |
188 | ||
189 |
# Set (reactively) available filters for the module. |
|
190 | 112x |
obs1 <- observeEvent(module_fd(), priority = 1, { |
191 | 93x |
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 | 93x |
slices <- slices_global_module() |
194 | ||
195 |
# Clean up previous filter states and refresh cache of previous module_fd with current |
|
196 | 3x |
if (!is.null(module_fd_previous())) module_fd_previous()$finalize() |
197 | 93x |
module_fd_previous(module_fd()) |
198 | ||
199 |
# Setting filter states from slices_global: |
|
200 |
# 1. when app initializes slices_global set to initial filters (specified by app developer) |
|
201 |
# 2. when data reinitializes slices_global reflects latest filter states |
|
202 | ||
203 | 93x |
module_fd()$set_filter_state(slices) |
204 | ||
205 |
# irrelevant filters are discarded in FilteredData$set_available_teal_slices |
|
206 |
# it means we don't need to subset slices_global$all_slices() from filters refering to irrelevant datasets |
|
207 | 93x |
module_fd()$set_available_teal_slices(slices_global$all_slices) |
208 | ||
209 |
# this needed in filter_manager_srv |
|
210 | 93x |
slices_global$module_slices_api_set( |
211 | 93x |
id, |
212 | 93x |
list( |
213 | 93x |
get_available_teal_slices = module_fd()$get_available_teal_slices(), |
214 | 93x |
set_filter_state = module_fd()$set_filter_state, # for testing purpose |
215 | 93x |
get_filter_state = module_fd()$get_filter_state # for testing purpose |
216 |
) |
|
217 |
) |
|
218 |
}) |
|
219 | ||
220 |
# Update global state and mapping matrix when module filters change. |
|
221 | 112x |
obs2 <- observeEvent(slices_module(), priority = 0, { |
222 | 113x |
this_slices <- slices_module() |
223 | 113x |
slices_global$slices_append(this_slices) # append new slices to the all_slices list |
224 | 113x |
mapping_elem <- setNames(nm = id, list(vapply(this_slices, `[[`, character(1L), "id"))) |
225 | 113x |
slices_global$slices_active(mapping_elem) |
226 |
}) |
|
227 | ||
228 | 112x |
obs3 <- observeEvent(slices_global_module(), { |
229 | 135x |
global_vs_module <- setdiff_teal_slices(slices_global_module(), slices_module()) |
230 | 135x |
module_vs_global <- setdiff_teal_slices(slices_module(), slices_global_module()) |
231 | 126x |
if (length(global_vs_module) || length(module_vs_global)) { |
232 |
# Comment: (Nota Bene) Normally new filters for a module are added through module-filter-panel, and slices |
|
233 |
# global are updated automatically so slices_module -> slices_global_module are equal. |
|
234 |
# this if is valid only when a change is made on the global level so the change needs to be propagated down |
|
235 |
# to the module (for example through snapshot manager). If it happens both slices are different |
|
236 | 13x |
logger::log_debug("srv_module_filter_manager@3 (N.B.) global state has changed for a module:{ id }.") |
237 | 13x |
module_fd()$clear_filter_states() |
238 | 13x |
module_fd()$set_filter_state(slices_global_module()) |
239 |
} |
|
240 |
}) |
|
241 | ||
242 | 112x |
slices_module # returned for testing purpose |
243 |
}) |
|
244 |
} |
|
245 | ||
246 |
#' @importFrom shiny reactiveVal reactiveValues |
|
247 |
methods::setOldClass("reactiveVal") |
|
248 |
methods::setOldClass("reactivevalues") |
|
249 | ||
250 |
#' @importFrom methods new |
|
251 |
#' @rdname module_filter_manager |
|
252 |
.slicesGlobal <- methods::setRefClass(".slicesGlobal", # nolint: object_name. |
|
253 |
fields = list( |
|
254 |
all_slices = "reactiveVal", |
|
255 |
module_slices_api = "reactivevalues" |
|
256 |
), |
|
257 |
methods = list( |
|
258 |
initialize = function(slices = teal_slices(), module_labels) { |
|
259 | 87x |
shiny::isolate({ |
260 | 87x |
checkmate::assert_class(slices, "teal_slices") |
261 |
# needed on init to not mix "global_filters" with module-specific-slots |
|
262 | 87x |
if (isTRUE(attr(slices, "module_specific"))) { |
263 | 11x |
old_mapping <- attr(slices, "mapping") |
264 | 11x |
new_mapping <- sapply(module_labels, simplify = FALSE, function(module_label) { |
265 | 20x |
unique(unlist(old_mapping[c(module_label, "global_filters")])) |
266 |
}) |
|
267 | 11x |
attr(slices, "mapping") <- new_mapping |
268 |
} |
|
269 | 87x |
.self$all_slices <<- shiny::reactiveVal(slices) |
270 | 87x |
.self$module_slices_api <<- shiny::reactiveValues() |
271 | 87x |
.self$slices_append(slices) |
272 | 87x |
.self$slices_active(attr(slices, "mapping")) |
273 | 87x |
invisible(.self) |
274 |
}) |
|
275 |
}, |
|
276 |
is_module_specific = function() { |
|
277 | 296x |
isTRUE(attr(.self$all_slices(), "module_specific")) |
278 |
}, |
|
279 |
module_slices_api_set = function(module_label, functions_list) { |
|
280 | 93x |
shiny::isolate({ |
281 | 93x |
if (!.self$is_module_specific()) { |
282 | 77x |
module_label <- "global_filters" |
283 |
} |
|
284 | 93x |
if (!identical(.self$module_slices_api[[module_label]], functions_list)) { |
285 | 93x |
.self$module_slices_api[[module_label]] <- functions_list |
286 |
} |
|
287 | 93x |
invisible(.self) |
288 |
}) |
|
289 |
}, |
|
290 |
slices_deactivate_all = function(module_label) { |
|
291 | ! |
shiny::isolate({ |
292 | ! |
new_slices <- .self$all_slices() |
293 | ! |
old_mapping <- attr(new_slices, "mapping") |
294 | ||
295 | ! |
new_mapping <- if (.self$is_module_specific()) { |
296 | ! |
new_module_mapping <- setNames(nm = module_label, list(character(0))) |
297 | ! |
modifyList(old_mapping, new_module_mapping) |
298 | ! |
} else if (missing(module_label)) { |
299 | ! |
lapply( |
300 | ! |
attr(.self$all_slices(), "mapping"), |
301 | ! |
function(x) character(0) |
302 |
) |
|
303 |
} else { |
|
304 | ! |
old_mapping[[module_label]] <- character(0) |
305 | ! |
old_mapping |
306 |
} |
|
307 | ||
308 | ! |
if (!identical(new_mapping, old_mapping)) { |
309 | ! |
logger::log_debug(".slicesGlobal@slices_deactivate_all: deactivating all slices.") |
310 | ! |
attr(new_slices, "mapping") <- new_mapping |
311 | ! |
.self$all_slices(new_slices) |
312 |
} |
|
313 | ! |
invisible(.self) |
314 |
}) |
|
315 |
}, |
|
316 |
slices_active = function(mapping_elem) { |
|
317 | 203x |
shiny::isolate({ |
318 | 203x |
if (.self$is_module_specific()) { |
319 | 36x |
new_mapping <- modifyList(attr(.self$all_slices(), "mapping"), mapping_elem) |
320 |
} else { |
|
321 | 167x |
new_mapping <- setNames(nm = "global_filters", list(unique(unlist(mapping_elem)))) |
322 |
} |
|
323 | ||
324 | 203x |
if (!identical(new_mapping, attr(.self$all_slices(), "mapping"))) { |
325 | 146x |
mapping_modules <- toString(names(new_mapping)) |
326 | 146x |
logger::log_debug(".slicesGlobal@slices_active: changing mapping for module(s): { mapping_modules }.") |
327 | 146x |
new_slices <- .self$all_slices() |
328 | 146x |
attr(new_slices, "mapping") <- new_mapping |
329 | 146x |
.self$all_slices(new_slices) |
330 |
} |
|
331 | ||
332 | 203x |
invisible(.self) |
333 |
}) |
|
334 |
}, |
|
335 |
# - only new filters are appended to the $all_slices |
|
336 |
# - mapping is not updated here |
|
337 |
slices_append = function(slices, activate = FALSE) { |
|
338 | 203x |
shiny::isolate({ |
339 | 203x |
if (!is.teal_slices(slices)) { |
340 | ! |
slices <- as.teal_slices(slices) |
341 |
} |
|
342 | ||
343 |
# to make sure that we don't unnecessary trigger $all_slices <reactiveVal> |
|
344 | 203x |
new_slices <- setdiff_teal_slices(slices, .self$all_slices()) |
345 | 203x |
old_mapping <- attr(.self$all_slices(), "mapping") |
346 | 203x |
if (length(new_slices)) { |
347 | 6x |
new_ids <- vapply(new_slices, `[[`, character(1L), "id") |
348 | 6x |
logger::log_debug(".slicesGlobal@slices_append: appending new slice(s): { new_ids }.") |
349 | 6x |
slices_ids <- vapply(.self$all_slices(), `[[`, character(1L), "id") |
350 | 6x |
lapply(new_slices, function(slice) { |
351 |
# In case the new state has the same id as an existing one, add a suffix |
|
352 | 6x |
if (slice$id %in% slices_ids) { |
353 | 1x |
slice$id <- utils::tail(make.unique(c(slices_ids, slice$id), sep = "_"), 1) |
354 |
} |
|
355 |
}) |
|
356 | ||
357 | 6x |
new_slices_all <- c(.self$all_slices(), new_slices) |
358 | 6x |
attr(new_slices_all, "mapping") <- old_mapping |
359 | 6x |
.self$all_slices(new_slices_all) |
360 |
} |
|
361 | ||
362 | 203x |
invisible(.self) |
363 |
}) |
|
364 |
}, |
|
365 |
slices_get = function(module_label) { |
|
366 | 302x |
if (missing(module_label)) { |
367 | ! |
.self$all_slices() |
368 |
} else { |
|
369 | 302x |
module_ids <- unlist(attr(.self$all_slices(), "mapping")[c(module_label, "global_filters")]) |
370 | 302x |
Filter( |
371 | 302x |
function(slice) slice$id %in% module_ids, |
372 | 302x |
.self$all_slices() |
373 |
) |
|
374 |
} |
|
375 |
}, |
|
376 |
slices_set = function(slices) { |
|
377 | 7x |
shiny::isolate({ |
378 | 7x |
if (!is.teal_slices(slices)) { |
379 | ! |
slices <- as.teal_slices(slices) |
380 |
} |
|
381 | 7x |
.self$all_slices(slices) |
382 | 7x |
invisible(.self) |
383 |
}) |
|
384 |
}, |
|
385 |
show = function() { |
|
386 | ! |
shiny::isolate(print(.self$all_slices())) |
387 | ! |
invisible(.self) |
388 |
} |
|
389 |
) |
|
390 |
) |
1 |
#' An example `teal` module |
|
2 |
#' |
|
3 |
#' `r lifecycle::badge("experimental")` |
|
4 |
#' |
|
5 |
#' This module creates an object called `object` that can be modified with decorators. |
|
6 |
#' The `object` is determined by what's selected in `Choose a dataset` input in UI. |
|
7 |
#' The object can be anything that can be handled by `renderPrint()`. |
|
8 |
#' See the `vignette("decorate-modules-output", package = "teal")` or [`teal_transform_module`] |
|
9 |
#' to read more about decorators. |
|
10 |
#' |
|
11 |
#' @inheritParams teal_modules |
|
12 |
#' @param decorators `r lifecycle::badge("experimental")` (`list` of `teal_transform_module` or `NULL`) optional, |
|
13 |
#' if not `NULL`, decorator for tables or plots included in the module. |
|
14 |
#' |
|
15 |
#' @return A `teal` module which can be included in the `modules` argument to [init()]. |
|
16 |
#' @examples |
|
17 |
#' app <- init( |
|
18 |
#' data = teal_data(IRIS = iris, MTCARS = mtcars), |
|
19 |
#' modules = example_module() |
|
20 |
#' ) |
|
21 |
#' if (interactive()) { |
|
22 |
#' shinyApp(app$ui, app$server) |
|
23 |
#' } |
|
24 |
#' @export |
|
25 |
example_module <- function(label = "example teal module", |
|
26 |
datanames = "all", |
|
27 |
transformators = list(), |
|
28 |
decorators = NULL) { |
|
29 | 43x |
checkmate::assert_string(label) |
30 | 43x |
checkmate::assert_list(decorators, "teal_transform_module", null.ok = TRUE) |
31 | ||
32 | 43x |
ans <- module( |
33 | 43x |
label, |
34 | 43x |
server = function(id, data, decorators) { |
35 | 5x |
checkmate::assert_class(isolate(data()), "teal_data") |
36 | 5x |
moduleServer(id, function(input, output, session) { |
37 | 5x |
datanames_rv <- reactive(names(req(data()))) |
38 | 5x |
observeEvent(datanames_rv(), { |
39 | 5x |
selected <- input$dataname |
40 | 5x |
if (identical(selected, "")) { |
41 | ! |
selected <- restoreInput(session$ns("dataname"), NULL) |
42 | 5x |
} else if (isFALSE(selected %in% datanames_rv())) { |
43 | ! |
selected <- datanames_rv()[1] |
44 |
} |
|
45 | 5x |
updateSelectInput( |
46 | 5x |
session = session, |
47 | 5x |
inputId = "dataname", |
48 | 5x |
choices = datanames_rv(), |
49 | 5x |
selected = selected |
50 |
) |
|
51 |
}) |
|
52 | ||
53 | 5x |
table_data <- reactive({ |
54 | 8x |
req(input$dataname) |
55 | 3x |
within(data(), |
56 |
{ |
|
57 | 3x |
object <- dataname |
58 |
}, |
|
59 | 3x |
dataname = as.name(input$dataname) |
60 |
) |
|
61 |
}) |
|
62 | ||
63 | 5x |
table_data_decorated_no_print <- srv_transform_teal_data( |
64 | 5x |
"decorate", |
65 | 5x |
data = table_data, |
66 | 5x |
transformators = decorators |
67 |
) |
|
68 | 5x |
table_data_decorated <- reactive(within(req(table_data_decorated_no_print()), expr = object)) |
69 | ||
70 | 5x |
output$text <- renderPrint({ |
71 | 9x |
req(table_data()) # Ensure original errors from module are displayed |
72 | 4x |
table_data_decorated()[["object"]] |
73 |
}) |
|
74 | ||
75 | 5x |
teal.widgets::verbatim_popup_srv( |
76 | 5x |
id = "rcode", |
77 | 5x |
verbatim_content = reactive(teal.code::get_code(req(table_data_decorated()))), |
78 | 5x |
title = "Example Code" |
79 |
) |
|
80 | ||
81 | 5x |
table_data_decorated |
82 |
}) |
|
83 |
}, |
|
84 | 43x |
ui = function(id, decorators) { |
85 | ! |
ns <- NS(id) |
86 | ! |
teal.widgets::standard_layout( |
87 | ! |
output = verbatimTextOutput(ns("text")), |
88 | ! |
encoding = tags$div( |
89 | ! |
selectInput(ns("dataname"), "Choose a dataset", choices = NULL), |
90 | ! |
ui_transform_teal_data(ns("decorate"), transformators = decorators), |
91 | ! |
teal.widgets::verbatim_popup_ui(ns("rcode"), "Show R code") |
92 |
) |
|
93 |
) |
|
94 |
}, |
|
95 | 43x |
ui_args = list(decorators = decorators), |
96 | 43x |
server_args = list(decorators = decorators), |
97 | 43x |
datanames = datanames, |
98 | 43x |
transformators = transformators |
99 |
) |
|
100 | 43x |
attr(ans, "teal_bookmarkable") <- TRUE |
101 | 43x |
ans |
102 |
} |
1 |
setOldClass("teal_module") |
|
2 |
setOldClass("teal_modules") |
|
3 | ||
4 |
#' Create `teal_module` and `teal_modules` objects |
|
5 |
#' |
|
6 |
#' @description |
|
7 |
#' `r lifecycle::badge("stable")` |
|
8 |
#' Create a nested tab structure to embed modules in a `teal` application. |
|
9 |
#' |
|
10 |
#' @details |
|
11 |
#' `module()` creates an instance of a `teal_module` that can be placed in a `teal` application. |
|
12 |
#' `modules()` shapes the structure of a the application by organizing `teal_module` within the navigation panel. |
|
13 |
#' It wraps `teal_module` and `teal_modules` objects in a `teal_modules` object, |
|
14 |
#' which results in a nested structure corresponding to the nested tabs in the final application. |
|
15 |
#' |
|
16 |
#' Note that for `modules()` `label` comes after `...`, so it must be passed as a named argument, |
|
17 |
#' otherwise it will be captured by `...`. |
|
18 |
#' |
|
19 |
#' The labels `"global_filters"` and `"Report previewer"` are reserved |
|
20 |
#' because they are used by the `mapping` argument of [teal_slices()] |
|
21 |
#' and the report previewer module [reporter_previewer_module()], respectively. |
|
22 |
#' |
|
23 |
#' # Restricting datasets used by `teal_module`: |
|
24 |
#' The `datanames` argument controls which datasets are used by the module’s server. These datasets, |
|
25 |
#' passed via server's `data` argument, are the only ones shown in the module's tab. |
|
26 |
#' |
|
27 |
#' When `datanames` is set to `"all"`, all datasets in the data object are treated as relevant. |
|
28 |
#' However, this may include unnecessary datasets, such as: |
|
29 |
#' - Proxy variables for column modifications |
|
30 |
#' - Temporary datasets used to create final versions |
|
31 |
#' - Connection objects |
|
32 |
#' |
|
33 |
#' To exclude irrelevant datasets, use the [set_datanames()] function to change `datanames` from |
|
34 |
#' `"all"` to specific names. Trying to modify non-`"all"` values with [set_datanames()] will result |
|
35 |
#' in a warning. Datasets with names starting with . are ignored globally unless explicitly listed |
|
36 |
#' in `datanames`. |
|
37 |
#' |
|
38 |
#' # `datanames` with `transformators` |
|
39 |
#' When transformators are specified, their `datanames` are added to the module’s `datanames`, which |
|
40 |
#' changes the behavior as follows: |
|
41 |
#' - If `module(datanames)` is `NULL` and the `transformators` have defined `datanames`, the sidebar |
|
42 |
#' will appear showing the `transformators`' datasets, instead of being hidden. |
|
43 |
#' - If `module(datanames)` is set to specific values and any `transformator` has `datanames = "all"`, |
|
44 |
#' the module may receive extra datasets that could be unnecessary |
|
45 |
#' |
|
46 |
#' @param label (`character(1)`) Label shown in the navigation item for the module or module group. |
|
47 |
#' For `modules()` defaults to `"root"`. See `Details`. |
|
48 |
#' @param server (`function`) `shiny` module with following arguments: |
|
49 |
#' - `id` - `teal` will set proper `shiny` namespace for this module (see [shiny::moduleServer()]). |
|
50 |
#' - `input`, `output`, `session` - (optional; not recommended) When provided, then [shiny::callModule()] |
|
51 |
#' will be used to call a module. From `shiny` 1.5.0, the recommended way is to use |
|
52 |
#' [shiny::moduleServer()] instead which doesn't require these arguments. |
|
53 |
#' - `data` (optional) When provided, the module will be called with `teal_data` object (i.e. a list of |
|
54 |
#' reactive (filtered) data specified in the `filters` argument) as the value of this argument. |
|
55 |
#' - `datasets` (optional) When provided, the module will be called with `FilteredData` object as the |
|
56 |
#' value of this argument. (See [`teal.slice::FilteredData`]). |
|
57 |
#' - `reporter` (optional) When provided, the module will be called with `Reporter` object as the value |
|
58 |
#' of this argument. (See [`teal.reporter::Reporter`]). |
|
59 |
#' - `filter_panel_api` (optional) When provided, the module will be called with `FilterPanelAPI` object |
|
60 |
#' as the value of this argument. (See [`teal.slice::FilterPanelAPI`]). |
|
61 |
#' - `...` (optional) When provided, `server_args` elements will be passed to the module named argument |
|
62 |
#' or to the `...`. |
|
63 |
#' @param ui (`function`) `shiny` UI module function with following arguments: |
|
64 |
#' - `id` - `teal` will set proper `shiny` namespace for this module. |
|
65 |
#' - `...` (optional) When provided, `ui_args` elements will be passed to the module named argument |
|
66 |
#' or to the `...`. |
|
67 |
#' @param filters (`character`) Deprecated. Use `datanames` instead. |
|
68 |
#' @param datanames (`character`) Names of the datasets relevant to the item. |
|
69 |
#' There are 2 reserved values that have specific behaviors: |
|
70 |
#' - The keyword `"all"` includes all datasets available in the data passed to the teal application. |
|
71 |
#' - `NULL` hides the sidebar panel completely. |
|
72 |
#' - If `transformators` are specified, their `datanames` are automatically added to this `datanames` |
|
73 |
#' argument. |
|
74 |
#' @param server_args (named `list`) with additional arguments passed on to the server function. |
|
75 |
#' @param ui_args (named `list`) with additional arguments passed on to the UI function. |
|
76 |
#' @param x (`teal_module` or `teal_modules`) Object to format/print. |
|
77 |
#' @param transformators (`list` of `teal_transform_module`) that will be applied to transformator module's data input. |
|
78 |
#' |
|
79 |
#' |
|
80 |
#' @param ... |
|
81 |
#' - For `modules()`: (`teal_module` or `teal_modules`) Objects to wrap into a tab. |
|
82 |
#' - For `format()` and `print()`: Arguments passed to other methods. |
|
83 |
#' |
|
84 |
#' @return |
|
85 |
#' `module()` returns an object of class `teal_module`. |
|
86 |
#' |
|
87 |
#' `modules()` returns a `teal_modules` object which contains following fields: |
|
88 |
#' - `label`: taken from the `label` argument. |
|
89 |
#' - `children`: a list containing objects passed in `...`. List elements are named after |
|
90 |
#' their `label` attribute converted to a valid `shiny` id. |
|
91 |
#' |
|
92 |
#' @name teal_modules |
|
93 |
#' @aliases teal_module |
|
94 |
#' |
|
95 |
#' @examples |
|
96 |
#' library(shiny) |
|
97 |
#' |
|
98 |
#' module_1 <- module( |
|
99 |
#' label = "a module", |
|
100 |
#' server = function(id, data) { |
|
101 |
#' moduleServer( |
|
102 |
#' id, |
|
103 |
#' module = function(input, output, session) { |
|
104 |
#' output$data <- renderDataTable(data()[["iris"]]) |
|
105 |
#' } |
|
106 |
#' ) |
|
107 |
#' }, |
|
108 |
#' ui = function(id) { |
|
109 |
#' ns <- NS(id) |
|
110 |
#' tagList(dataTableOutput(ns("data"))) |
|
111 |
#' }, |
|
112 |
#' datanames = "all" |
|
113 |
#' ) |
|
114 |
#' |
|
115 |
#' module_2 <- module( |
|
116 |
#' label = "another module", |
|
117 |
#' server = function(id) { |
|
118 |
#' moduleServer( |
|
119 |
#' id, |
|
120 |
#' module = function(input, output, session) { |
|
121 |
#' output$text <- renderText("Another Module") |
|
122 |
#' } |
|
123 |
#' ) |
|
124 |
#' }, |
|
125 |
#' ui = function(id) { |
|
126 |
#' ns <- NS(id) |
|
127 |
#' tagList(textOutput(ns("text"))) |
|
128 |
#' }, |
|
129 |
#' datanames = NULL |
|
130 |
#' ) |
|
131 |
#' |
|
132 |
#' modules <- modules( |
|
133 |
#' label = "modules", |
|
134 |
#' modules( |
|
135 |
#' label = "nested modules", |
|
136 |
#' module_1 |
|
137 |
#' ), |
|
138 |
#' module_2 |
|
139 |
#' ) |
|
140 |
#' |
|
141 |
#' app <- init( |
|
142 |
#' data = teal_data(iris = iris), |
|
143 |
#' modules = modules |
|
144 |
#' ) |
|
145 |
#' |
|
146 |
#' if (interactive()) { |
|
147 |
#' shinyApp(app$ui, app$server) |
|
148 |
#' } |
|
149 |
#' @rdname teal_modules |
|
150 |
#' @export |
|
151 |
#' |
|
152 |
module <- function(label = "module", |
|
153 |
server = function(id, data, ...) moduleServer(id, function(input, output, session) NULL), |
|
154 |
ui = function(id, ...) tags$p(paste0("This module has no UI (id: ", id, " )")), |
|
155 |
filters, |
|
156 |
datanames = "all", |
|
157 |
server_args = NULL, |
|
158 |
ui_args = NULL, |
|
159 |
transformators = list()) { |
|
160 |
# argument checking (independent) |
|
161 |
## `label` |
|
162 | 220x |
checkmate::assert_string(label) |
163 | 217x |
if (label == "global_filters") { |
164 | 1x |
stop( |
165 | 1x |
sprintf("module(label = \"%s\", ...\n ", label), |
166 | 1x |
"Label 'global_filters' is reserved in teal. Please change to something else.", |
167 | 1x |
call. = FALSE |
168 |
) |
|
169 |
} |
|
170 | 216x |
if (label == "Report previewer") { |
171 | ! |
stop( |
172 | ! |
sprintf("module(label = \"%s\", ...\n ", label), |
173 | ! |
"Label 'Report previewer' is reserved in teal. Please change to something else.", |
174 | ! |
call. = FALSE |
175 |
) |
|
176 |
} |
|
177 | ||
178 |
## server |
|
179 | 216x |
checkmate::assert_function(server) |
180 | 216x |
server_formals <- names(formals(server)) |
181 | 216x |
if (!( |
182 | 216x |
"id" %in% server_formals || |
183 | 216x |
all(c("input", "output", "session") %in% server_formals) |
184 |
)) { |
|
185 | 2x |
stop( |
186 | 2x |
"\nmodule() `server` argument requires a function with following arguments:", |
187 | 2x |
"\n - id - `teal` will set proper `shiny` namespace for this module.", |
188 | 2x |
"\n - input, output, session (not recommended) - then `shiny::callModule` will be used to call a module.", |
189 | 2x |
"\n\nFollowing arguments can be used optionaly:", |
190 | 2x |
"\n - `data` - module will receive list of reactive (filtered) data specified in the `filters` argument", |
191 | 2x |
"\n - `datasets` - module will receive `FilteredData`. See `help(teal.slice::FilteredData)`", |
192 | 2x |
"\n - `reporter` - module will receive `Reporter`. See `help(teal.reporter::Reporter)`", |
193 | 2x |
"\n - `filter_panel_api` - module will receive `FilterPanelAPI`. (See [teal.slice::FilterPanelAPI]).", |
194 | 2x |
"\n - `...` server_args elements will be passed to the module named argument or to the `...`" |
195 |
) |
|
196 |
} |
|
197 | ||
198 | 214x |
if ("datasets" %in% server_formals) { |
199 | 2x |
warning( |
200 | 2x |
sprintf("Called from module(label = \"%s\", ...)\n ", label), |
201 | 2x |
"`datasets` argument in the server is deprecated and will be removed in the next release. ", |
202 | 2x |
"Please use `data` instead.", |
203 | 2x |
call. = FALSE |
204 |
) |
|
205 |
} |
|
206 | ||
207 |
## UI |
|
208 | 214x |
checkmate::assert_function(ui) |
209 | 214x |
ui_formals <- names(formals(ui)) |
210 | 214x |
if (!"id" %in% ui_formals) { |
211 | 1x |
stop( |
212 | 1x |
"\nmodule() `ui` argument requires a function with following arguments:", |
213 | 1x |
"\n - id - `teal` will set proper `shiny` namespace for this module.", |
214 | 1x |
"\n\nFollowing arguments can be used optionally:", |
215 | 1x |
"\n - `...` ui_args elements will be passed to the module argument of the same name or to the `...`" |
216 |
) |
|
217 |
} |
|
218 | ||
219 | 213x |
if (any(c("data", "datasets") %in% ui_formals)) { |
220 | 2x |
stop( |
221 | 2x |
sprintf("Called from module(label = \"%s\", ...)\n ", label), |
222 | 2x |
"UI with `data` or `datasets` argument is no longer accepted.\n ", |
223 | 2x |
"If some UI inputs depend on data, please move the logic to your server instead.\n ", |
224 | 2x |
"Possible solutions are renderUI() or updateXyzInput() functions." |
225 |
) |
|
226 |
} |
|
227 | ||
228 |
## `filters` |
|
229 | 211x |
if (!missing(filters)) { |
230 | ! |
datanames <- filters |
231 | ! |
msg <- |
232 | ! |
"The `filters` argument is deprecated and will be removed in the next release. Please use `datanames` instead." |
233 | ! |
warning(msg) |
234 |
} |
|
235 | ||
236 |
## `datanames` (also including deprecated `filters`) |
|
237 |
# please note a race condition between datanames set when filters is not missing and data arg in server function |
|
238 | 211x |
if (!is.element("data", server_formals) && !is.null(datanames)) { |
239 | 12x |
message(sprintf("module \"%s\" server function takes no data so \"datanames\" will be ignored", label)) |
240 | 12x |
datanames <- NULL |
241 |
} |
|
242 | 211x |
checkmate::assert_character(datanames, min.len = 1, null.ok = TRUE, any.missing = FALSE) |
243 | ||
244 |
## `server_args` |
|
245 | 210x |
checkmate::assert_list(server_args, null.ok = TRUE, names = "named") |
246 | 208x |
srv_extra_args <- setdiff(names(server_args), server_formals) |
247 | 208x |
if (length(srv_extra_args) > 0 && !"..." %in% server_formals) { |
248 | 1x |
stop( |
249 | 1x |
"\nFollowing `server_args` elements have no equivalent in the formals of the server:\n", |
250 | 1x |
paste(paste(" -", srv_extra_args), collapse = "\n"), |
251 | 1x |
"\n\nUpdate the server arguments by including above or add `...`" |
252 |
) |
|
253 |
} |
|
254 | ||
255 |
## `ui_args` |
|
256 | 207x |
checkmate::assert_list(ui_args, null.ok = TRUE, names = "named") |
257 | 205x |
ui_extra_args <- setdiff(names(ui_args), ui_formals) |
258 | 205x |
if (length(ui_extra_args) > 0 && !"..." %in% ui_formals) { |
259 | 1x |
stop( |
260 | 1x |
"\nFollowing `ui_args` elements have no equivalent in the formals of UI:\n", |
261 | 1x |
paste(paste(" -", ui_extra_args), collapse = "\n"), |
262 | 1x |
"\n\nUpdate the UI arguments by including above or add `...`" |
263 |
) |
|
264 |
} |
|
265 | ||
266 |
## `transformators` |
|
267 | 204x |
if (inherits(transformators, "teal_transform_module")) { |
268 | 1x |
transformators <- list(transformators) |
269 |
} |
|
270 | 204x |
checkmate::assert_list(transformators, types = "teal_transform_module") |
271 | 204x |
transform_datanames <- unlist(lapply(transformators, attr, "datanames")) |
272 | 204x |
combined_datanames <- if (identical(datanames, "all")) { |
273 | 151x |
"all" |
274 |
} else { |
|
275 | 53x |
union(datanames, transform_datanames) |
276 |
} |
|
277 | ||
278 | 204x |
structure( |
279 | 204x |
list( |
280 | 204x |
label = label, |
281 | 204x |
server = server, |
282 | 204x |
ui = ui, |
283 | 204x |
datanames = combined_datanames, |
284 | 204x |
server_args = server_args, |
285 | 204x |
ui_args = ui_args, |
286 | 204x |
transformators = transformators |
287 |
), |
|
288 | 204x |
class = "teal_module" |
289 |
) |
|
290 |
} |
|
291 | ||
292 |
#' @rdname teal_modules |
|
293 |
#' @export |
|
294 |
#' |
|
295 |
modules <- function(..., label = "root") { |
|
296 | 144x |
checkmate::assert_string(label) |
297 | 142x |
submodules <- list(...) |
298 | 142x |
if (any(vapply(submodules, is.character, FUN.VALUE = logical(1)))) { |
299 | 2x |
stop( |
300 | 2x |
"The only character argument to modules() must be 'label' and it must be named, ", |
301 | 2x |
"change modules('lab', ...) to modules(label = 'lab', ...)" |
302 |
) |
|
303 |
} |
|
304 | ||
305 | 140x |
checkmate::assert_list(submodules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules")) |
306 |
# name them so we can more easily access the children |
|
307 |
# beware however that the label of the submodules should not be changed as it must be kept synced |
|
308 | 137x |
labels <- vapply(submodules, function(submodule) submodule$label, character(1)) |
309 | 137x |
names(submodules) <- get_unique_labels(labels) |
310 | 137x |
structure( |
311 | 137x |
list( |
312 | 137x |
label = label, |
313 | 137x |
children = submodules |
314 |
), |
|
315 | 137x |
class = "teal_modules" |
316 |
) |
|
317 |
} |
|
318 | ||
319 |
# printing methods ---- |
|
320 | ||
321 |
#' @rdname teal_modules |
|
322 |
#' @param is_last (`logical(1)`) Whether this is the last item in its parent's children list. |
|
323 |
#' Affects the tree branch character used (L- vs |-) |
|
324 |
#' @param parent_prefix (`character(1)`) The prefix inherited from parent nodes, |
|
325 |
#' used to maintain the tree structure in nested levels |
|
326 |
#' @param is_root (`logical(1)`) Whether this is the root node of the tree. Only used in |
|
327 |
#' format.teal_modules(). Determines whether to show "TEAL ROOT" header |
|
328 |
#' @param what (`character`) Specifies which metadata to display. |
|
329 |
#' Possible values: "datasets", "properties", "ui_args", "server_args", "transformators" |
|
330 |
#' @examples |
|
331 |
#' mod <- module( |
|
332 |
#' label = "My Custom Module", |
|
333 |
#' server = function(id, data, ...) {}, |
|
334 |
#' ui = function(id, ...) {}, |
|
335 |
#' datanames = c("ADSL", "ADTTE"), |
|
336 |
#' transformators = list(), |
|
337 |
#' ui_args = list(a = 1, b = "b"), |
|
338 |
#' server_args = list(x = 5, y = list(p = 1)) |
|
339 |
#' ) |
|
340 |
#' cat(format(mod)) |
|
341 |
#' @export |
|
342 |
format.teal_module <- function(x, |
|
343 |
is_last = FALSE, |
|
344 |
parent_prefix = "", |
|
345 |
what = c("datasets", "properties", "ui_args", "server_args", "transformators"), |
|
346 |
...) { |
|
347 | 3x |
empty_text <- "" |
348 | 3x |
branch <- if (is_last) "L-" else "|-" |
349 | 3x |
current_prefix <- paste0(parent_prefix, branch, " ") |
350 | 3x |
content_prefix <- paste0(parent_prefix, if (is_last) " " else "| ") |
351 | ||
352 | 3x |
format_list <- function(lst, empty = empty_text, label_width = 0) { |
353 | 6x |
if (is.null(lst) || length(lst) == 0) { |
354 | 6x |
empty |
355 |
} else { |
|
356 | ! |
colon_space <- paste(rep(" ", label_width), collapse = "") |
357 | ||
358 | ! |
first_item <- sprintf("%s (%s)", names(lst)[1], cli::col_silver(class(lst[[1]])[1])) |
359 | ! |
rest_items <- if (length(lst) > 1) { |
360 | ! |
paste( |
361 | ! |
vapply( |
362 | ! |
names(lst)[-1], |
363 | ! |
function(name) { |
364 | ! |
sprintf( |
365 | ! |
"%s%s (%s)", |
366 | ! |
paste0(content_prefix, "| ", colon_space), |
367 | ! |
name, |
368 | ! |
cli::col_silver(class(lst[[name]])[1]) |
369 |
) |
|
370 |
}, |
|
371 | ! |
character(1) |
372 |
), |
|
373 | ! |
collapse = "\n" |
374 |
) |
|
375 |
} |
|
376 | ! |
if (length(lst) > 1) paste0(first_item, "\n", rest_items) else first_item |
377 |
} |
|
378 |
} |
|
379 | ||
380 | 3x |
bookmarkable <- isTRUE(attr(x, "teal_bookmarkable")) |
381 | 3x |
reportable <- "reporter" %in% names(formals(x$server)) |
382 | ||
383 | 3x |
transformators <- if (length(x$transformators) > 0) { |
384 | ! |
paste(sapply(x$transformators, function(t) attr(t, "label")), collapse = ", ") |
385 |
} else { |
|
386 | 3x |
empty_text |
387 |
} |
|
388 | ||
389 | 3x |
output <- pasten(current_prefix, cli::bg_white(cli::col_black(x$label))) |
390 | ||
391 | 3x |
if ("datasets" %in% what) { |
392 | 3x |
output <- paste0( |
393 | 3x |
output, |
394 | 3x |
content_prefix, "|- ", cli::col_yellow("Datasets : "), paste(x$datanames, collapse = ", "), "\n" |
395 |
) |
|
396 |
} |
|
397 | 3x |
if ("properties" %in% what) { |
398 | 3x |
output <- paste0( |
399 | 3x |
output, |
400 | 3x |
content_prefix, "|- ", cli::col_blue("Properties:"), "\n", |
401 | 3x |
content_prefix, "| |- ", cli::col_cyan("Bookmarkable : "), bookmarkable, "\n", |
402 | 3x |
content_prefix, "| L- ", cli::col_cyan("Reportable : "), reportable, "\n" |
403 |
) |
|
404 |
} |
|
405 | 3x |
if ("ui_args" %in% what) { |
406 | 3x |
ui_args_formatted <- format_list(x$ui_args, label_width = 19) |
407 | 3x |
output <- paste0( |
408 | 3x |
output, |
409 | 3x |
content_prefix, "|- ", cli::col_green("UI Arguments : "), ui_args_formatted, "\n" |
410 |
) |
|
411 |
} |
|
412 | 3x |
if ("server_args" %in% what) { |
413 | 3x |
server_args_formatted <- format_list(x$server_args, label_width = 19) |
414 | 3x |
output <- paste0( |
415 | 3x |
output, |
416 | 3x |
content_prefix, "|- ", cli::col_green("Server Arguments : "), server_args_formatted, "\n" |
417 |
) |
|
418 |
} |
|
419 | 3x |
if ("transformators" %in% what) { |
420 | 3x |
output <- paste0( |
421 | 3x |
output, |
422 | 3x |
content_prefix, "L- ", cli::col_magenta("Transformators : "), transformators, "\n" |
423 |
) |
|
424 |
} |
|
425 | ||
426 | 3x |
output |
427 |
} |
|
428 | ||
429 |
#' @rdname teal_modules |
|
430 |
#' @examples |
|
431 |
#' custom_module <- function( |
|
432 |
#' label = "label", ui_args = NULL, server_args = NULL, |
|
433 |
#' datanames = "all", transformators = list(), bk = FALSE) { |
|
434 |
#' ans <- module( |
|
435 |
#' label, |
|
436 |
#' server = function(id, data, ...) {}, |
|
437 |
#' ui = function(id, ...) { |
|
438 |
#' }, |
|
439 |
#' datanames = datanames, |
|
440 |
#' transformators = transformators, |
|
441 |
#' ui_args = ui_args, |
|
442 |
#' server_args = server_args |
|
443 |
#' ) |
|
444 |
#' attr(ans, "teal_bookmarkable") <- bk |
|
445 |
#' ans |
|
446 |
#' } |
|
447 |
#' |
|
448 |
#' dummy_transformator <- teal_transform_module( |
|
449 |
#' label = "Dummy Transform", |
|
450 |
#' ui = function(id) div("(does nothing)"), |
|
451 |
#' server = function(id, data) { |
|
452 |
#' moduleServer(id, function(input, output, session) data) |
|
453 |
#' } |
|
454 |
#' ) |
|
455 |
#' |
|
456 |
#' plot_transformator <- teal_transform_module( |
|
457 |
#' label = "Plot Settings", |
|
458 |
#' ui = function(id) div("(does nothing)"), |
|
459 |
#' server = function(id, data) { |
|
460 |
#' moduleServer(id, function(input, output, session) data) |
|
461 |
#' } |
|
462 |
#' ) |
|
463 |
#' |
|
464 |
#' complete_modules <- modules( |
|
465 |
#' custom_module( |
|
466 |
#' label = "Data Overview", |
|
467 |
#' datanames = c("ADSL", "ADAE", "ADVS"), |
|
468 |
#' ui_args = list( |
|
469 |
#' view_type = "table", |
|
470 |
#' page_size = 10, |
|
471 |
#' filters = c("ARM", "SEX", "RACE") |
|
472 |
#' ), |
|
473 |
#' server_args = list( |
|
474 |
#' cache = TRUE, |
|
475 |
#' debounce = 1000 |
|
476 |
#' ), |
|
477 |
#' transformators = list(dummy_transformator), |
|
478 |
#' bk = TRUE |
|
479 |
#' ), |
|
480 |
#' modules( |
|
481 |
#' label = "Nested 1", |
|
482 |
#' custom_module( |
|
483 |
#' label = "Interactive Plots", |
|
484 |
#' datanames = c("ADSL", "ADVS"), |
|
485 |
#' ui_args = list( |
|
486 |
#' plot_type = c("scatter", "box", "line"), |
|
487 |
#' height = 600, |
|
488 |
#' width = 800, |
|
489 |
#' color_scheme = "viridis" |
|
490 |
#' ), |
|
491 |
#' server_args = list( |
|
492 |
#' render_type = "svg", |
|
493 |
#' cache_plots = TRUE |
|
494 |
#' ), |
|
495 |
#' transformators = list(dummy_transformator, plot_transformator), |
|
496 |
#' bk = TRUE |
|
497 |
#' ), |
|
498 |
#' modules( |
|
499 |
#' label = "Nested 2", |
|
500 |
#' custom_module( |
|
501 |
#' label = "Summary Statistics", |
|
502 |
#' datanames = "ADSL", |
|
503 |
#' ui_args = list( |
|
504 |
#' stats = c("mean", "median", "sd", "range"), |
|
505 |
#' grouping = c("ARM", "SEX") |
|
506 |
#' ) |
|
507 |
#' ), |
|
508 |
#' modules( |
|
509 |
#' label = "Labeled nested modules", |
|
510 |
#' custom_module( |
|
511 |
#' label = "Subgroup Analysis", |
|
512 |
#' datanames = c("ADSL", "ADAE"), |
|
513 |
#' ui_args = list( |
|
514 |
#' subgroups = c("AGE", "SEX", "RACE"), |
|
515 |
#' analysis_type = "stratified" |
|
516 |
#' ), |
|
517 |
#' bk = TRUE |
|
518 |
#' ) |
|
519 |
#' ), |
|
520 |
#' modules(custom_module(label = "Subgroup Analysis in non-labled modules")) |
|
521 |
#' ) |
|
522 |
#' ), |
|
523 |
#' custom_module("Non-nested module") |
|
524 |
#' ) |
|
525 |
#' |
|
526 |
#' cat(format(complete_modules)) |
|
527 |
#' cat(format(complete_modules, what = c("ui_args", "server_args", "transformators"))) |
|
528 |
#' @export |
|
529 |
format.teal_modules <- function(x, is_root = TRUE, is_last = FALSE, parent_prefix = "", ...) { |
|
530 | 1x |
if (is_root) { |
531 | 1x |
header <- pasten(cli::style_bold("TEAL ROOT")) |
532 | 1x |
new_parent_prefix <- " " #' Initial indent for root level |
533 |
} else { |
|
534 | ! |
if (!is.null(x$label)) { |
535 | ! |
branch <- if (is_last) "L-" else "|-" |
536 | ! |
header <- pasten(parent_prefix, branch, " ", cli::style_bold(x$label)) |
537 | ! |
new_parent_prefix <- paste0(parent_prefix, if (is_last) " " else "| ") |
538 |
} else { |
|
539 | ! |
header <- "" |
540 | ! |
new_parent_prefix <- parent_prefix |
541 |
} |
|
542 |
} |
|
543 | ||
544 | 1x |
if (length(x$children) > 0) { |
545 | 1x |
children_output <- character(0) |
546 | 1x |
n_children <- length(x$children) |
547 | ||
548 | 1x |
for (i in seq_along(x$children)) { |
549 | 3x |
child <- x$children[[i]] |
550 | 3x |
is_last_child <- (i == n_children) |
551 | ||
552 | 3x |
if (inherits(child, "teal_modules")) { |
553 | ! |
children_output <- c( |
554 | ! |
children_output, |
555 | ! |
format(child, |
556 | ! |
is_root = FALSE, |
557 | ! |
is_last = is_last_child, |
558 | ! |
parent_prefix = new_parent_prefix, |
559 |
... |
|
560 |
) |
|
561 |
) |
|
562 |
} else { |
|
563 | 3x |
children_output <- c( |
564 | 3x |
children_output, |
565 | 3x |
format(child, |
566 | 3x |
is_last = is_last_child, |
567 | 3x |
parent_prefix = new_parent_prefix, |
568 |
... |
|
569 |
) |
|
570 |
) |
|
571 |
} |
|
572 |
} |
|
573 | ||
574 | 1x |
paste0(header, paste(children_output, collapse = "")) |
575 |
} else { |
|
576 | ! |
header |
577 |
} |
|
578 |
} |
|
579 | ||
580 |
#' @rdname teal_modules |
|
581 |
#' @export |
|
582 |
print.teal_module <- function(x, ...) { |
|
583 | ! |
cat(format(x, ...)) |
584 | ! |
invisible(x) |
585 |
} |
|
586 | ||
587 |
#' @rdname teal_modules |
|
588 |
#' @export |
|
589 |
print.teal_modules <- function(x, ...) { |
|
590 | ! |
cat(format(x, ...)) |
591 | ! |
invisible(x) |
592 |
} |
|
593 | ||
594 |
#' @param modules (`teal_module` or `teal_modules`) |
|
595 |
#' @rdname teal_modules |
|
596 |
#' @examples |
|
597 |
#' # change the module's datanames |
|
598 |
#' set_datanames(module(datanames = "all"), "a") |
|
599 |
#' |
|
600 |
#' # change modules' datanames |
|
601 |
#' set_datanames( |
|
602 |
#' modules( |
|
603 |
#' module(datanames = "all"), |
|
604 |
#' module(datanames = "a") |
|
605 |
#' ), |
|
606 |
#' "b" |
|
607 |
#' ) |
|
608 |
#' @export |
|
609 |
set_datanames <- function(modules, datanames) { |
|
610 | ! |
checkmate::assert_multi_class(modules, c("teal_modules", "teal_module")) |
611 | ! |
if (inherits(modules, "teal_modules")) { |
612 | ! |
modules$children <- lapply(modules$children, set_datanames, datanames) |
613 |
} else { |
|
614 | ! |
if (identical(modules$datanames, "all")) { |
615 | ! |
modules$datanames <- datanames |
616 |
} else { |
|
617 | ! |
warning( |
618 | ! |
"Not possible to modify datanames of the module ", modules$label, |
619 | ! |
". set_datanames() can only change datanames if it was set to \"all\".", |
620 | ! |
call. = FALSE |
621 |
) |
|
622 |
} |
|
623 |
} |
|
624 | ! |
modules |
625 |
} |
|
626 | ||
627 |
# utilities ---- |
|
628 |
## subset or modify modules ---- |
|
629 | ||
630 |
#' Append a `teal_module` to `children` of a `teal_modules` object |
|
631 |
#' @keywords internal |
|
632 |
#' @param modules (`teal_modules`) |
|
633 |
#' @param module (`teal_module`) object to be appended onto the children of `modules` |
|
634 |
#' @return A `teal_modules` object with `module` appended. |
|
635 |
append_module <- function(modules, module) { |
|
636 | 8x |
checkmate::assert_class(modules, "teal_modules") |
637 | 6x |
checkmate::assert_class(module, "teal_module") |
638 | 4x |
modules$children <- c(modules$children, list(module)) |
639 | 4x |
labels <- vapply(modules$children, function(submodule) submodule$label, character(1)) |
640 | 4x |
names(modules$children) <- get_unique_labels(labels) |
641 | 4x |
modules |
642 |
} |
|
643 | ||
644 |
#' Extract/Remove module(s) of specific class |
|
645 |
#' |
|
646 |
#' Given a `teal_module` or a `teal_modules`, return the elements of the structure according to `class`. |
|
647 |
#' |
|
648 |
#' @param modules (`teal_modules`) |
|
649 |
#' @param class The class name of `teal_module` to be extracted or dropped. |
|
650 |
#' @keywords internal |
|
651 |
#' @return |
|
652 |
#' - For `extract_module`, a `teal_module` of class `class` or `teal_modules` containing modules of class `class`. |
|
653 |
#' - For `drop_module`, the opposite, which is all `teal_modules` of class other than `class`. |
|
654 |
#' @rdname module_management |
|
655 |
extract_module <- function(modules, class) { |
|
656 | 28x |
if (inherits(modules, class)) { |
657 | ! |
modules |
658 | 28x |
} else if (inherits(modules, "teal_module")) { |
659 | 15x |
NULL |
660 | 13x |
} else if (inherits(modules, "teal_modules")) { |
661 | 13x |
Filter(function(x) length(x) > 0L, lapply(modules$children, extract_module, class)) |
662 |
} |
|
663 |
} |
|
664 | ||
665 |
#' @keywords internal |
|
666 |
#' @return `teal_modules` |
|
667 |
#' @rdname module_management |
|
668 |
drop_module <- function(modules, class) { |
|
669 | ! |
if (inherits(modules, class)) { |
670 | ! |
NULL |
671 | ! |
} else if (inherits(modules, "teal_module")) { |
672 | ! |
modules |
673 | ! |
} else if (inherits(modules, "teal_modules")) { |
674 | ! |
do.call( |
675 | ! |
"modules", |
676 | ! |
c(Filter(function(x) length(x) > 0L, lapply(modules$children, drop_module, class)), label = modules$label) |
677 |
) |
|
678 |
} |
|
679 |
} |
|
680 | ||
681 |
## read modules ---- |
|
682 | ||
683 |
#' Does the object make use of the `arg` |
|
684 |
#' |
|
685 |
#' @param modules (`teal_module` or `teal_modules`) object |
|
686 |
#' @param arg (`character(1)`) names of the arguments to be checked against formals of `teal` modules. |
|
687 |
#' @return `logical` whether the object makes use of `arg`. |
|
688 |
#' @rdname is_arg_used |
|
689 |
#' @keywords internal |
|
690 |
is_arg_used <- function(modules, arg) { |
|
691 | 519x |
checkmate::assert_string(arg) |
692 | 516x |
if (inherits(modules, "teal_modules")) { |
693 | 20x |
any(unlist(lapply(modules$children, is_arg_used, arg))) |
694 | 496x |
} else if (inherits(modules, "teal_module")) { |
695 | 32x |
is_arg_used(modules$server, arg) || is_arg_used(modules$ui, arg) |
696 | 464x |
} else if (is.function(modules)) { |
697 | 462x |
isTRUE(arg %in% names(formals(modules))) |
698 |
} else { |
|
699 | 2x |
stop("is_arg_used function not implemented for this object") |
700 |
} |
|
701 |
} |
|
702 | ||
703 | ||
704 |
#' Get module depth |
|
705 |
#' |
|
706 |
#' Depth starts at 0, so a single `teal.module` has depth 0. |
|
707 |
#' Nesting it increases overall depth by 1. |
|
708 |
#' |
|
709 |
#' @inheritParams init |
|
710 |
#' @param depth optional integer determining current depth level |
|
711 |
#' |
|
712 |
#' @return Depth level for given module. |
|
713 |
#' @keywords internal |
|
714 |
modules_depth <- function(modules, depth = 0L) { |
|
715 | 12x |
checkmate::assert_multi_class(modules, c("teal_module", "teal_modules")) |
716 | 12x |
checkmate::assert_int(depth, lower = 0) |
717 | 11x |
if (inherits(modules, "teal_modules")) { |
718 | 4x |
max(vapply(modules$children, modules_depth, integer(1), depth = depth + 1L)) |
719 |
} else { |
|
720 | 7x |
depth |
721 |
} |
|
722 |
} |
|
723 | ||
724 |
#' Retrieve labels from `teal_modules` |
|
725 |
#' |
|
726 |
#' @param modules (`teal_modules`) |
|
727 |
#' @return A `list` containing the labels of the modules. If the modules are nested, |
|
728 |
#' the function returns a nested `list` of labels. |
|
729 |
#' @keywords internal |
|
730 |
module_labels <- function(modules) { |
|
731 | 199x |
if (inherits(modules, "teal_modules")) { |
732 | 87x |
lapply(modules$children, module_labels) |
733 |
} else { |
|
734 | 112x |
modules$label |
735 |
} |
|
736 |
} |
|
737 | ||
738 |
#' Retrieve `teal_bookmarkable` attribute from `teal_modules` |
|
739 |
#' |
|
740 |
#' @param modules (`teal_modules` or `teal_module`) object |
|
741 |
#' @return named list of the same structure as `modules` with `TRUE` or `FALSE` values indicating |
|
742 |
#' whether the module is bookmarkable. |
|
743 |
#' @keywords internal |
|
744 |
modules_bookmarkable <- function(modules) { |
|
745 | 199x |
checkmate::assert_multi_class(modules, c("teal_modules", "teal_module")) |
746 | 199x |
if (inherits(modules, "teal_modules")) { |
747 | 87x |
setNames( |
748 | 87x |
lapply(modules$children, modules_bookmarkable), |
749 | 87x |
vapply(modules$children, `[[`, "label", FUN.VALUE = character(1)) |
750 |
) |
|
751 |
} else { |
|
752 | 112x |
attr(modules, "teal_bookmarkable", exact = TRUE) |
753 |
} |
|
754 |
} |
1 |
#' App state management. |
|
2 |
#' |
|
3 |
#' @description |
|
4 |
#' `r lifecycle::badge("experimental")` |
|
5 |
#' |
|
6 |
#' Capture and restore the global (app) input state. |
|
7 |
#' |
|
8 |
#' @details |
|
9 |
#' This module introduces bookmarks into `teal` apps: the `shiny` bookmarking mechanism becomes enabled |
|
10 |
#' and server-side bookmarks can be created. |
|
11 |
#' |
|
12 |
#' The bookmark manager presents a button with the bookmark icon and is placed in the tab-bar. |
|
13 |
#' When clicked, the button creates a bookmark and opens a modal which displays the bookmark URL. |
|
14 |
#' |
|
15 |
#' `teal` does not guarantee that all modules (`teal_module` objects) are bookmarkable. |
|
16 |
#' Those that are, have a `teal_bookmarkable` attribute set to `TRUE`. If any modules are not bookmarkable, |
|
17 |
#' 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 |
#' should set the `teal_bookmarkable` attribute to `TRUE`. |
|
20 |
#' |
|
21 |
#' @section Server logic: |
|
22 |
#' A bookmark is a URL that contains the app address with a `/?_state_id_=<bookmark_dir>` suffix. |
|
23 |
#' `<bookmark_dir>` is a directory created on the server, where the state of the application is saved. |
|
24 |
#' Accessing the bookmark URL opens a new session of the app that starts in the previously saved state. |
|
25 |
#' |
|
26 |
#' @section Note: |
|
27 |
#' To enable bookmarking use either: |
|
28 |
#' - `shiny` app by using `shinyApp(..., enableBookmarking = "server")` (not supported in `shinytest2`) |
|
29 |
#' - set `options(shiny.bookmarkStore = "server")` before running the app |
|
30 |
#' |
|
31 |
#' |
|
32 |
#' @inheritParams init |
|
33 |
#' |
|
34 |
#' @return Invisible `NULL`. |
|
35 |
#' |
|
36 |
#' @aliases bookmark bookmark_manager bookmark_manager_module |
|
37 |
#' |
|
38 |
#' @name module_bookmark_manager |
|
39 |
#' @rdname module_bookmark_manager |
|
40 |
#' |
|
41 |
#' @keywords internal |
|
42 |
#' |
|
43 |
NULL |
|
44 | ||
45 |
#' @rdname module_bookmark_manager |
|
46 |
ui_bookmark_panel <- function(id, modules) { |
|
47 | ! |
ns <- NS(id) |
48 | ||
49 | ! |
bookmark_option <- get_bookmarking_option() |
50 | ! |
is_unbookmarkable <- need_bookmarking(modules) |
51 | ! |
shinyOptions(bookmarkStore = bookmark_option) |
52 | ||
53 |
# Render bookmark warnings count |
|
54 | ! |
if (!all(is_unbookmarkable) && identical(bookmark_option, "server")) { |
55 | ! |
tags$button( |
56 | ! |
id = ns("do_bookmark"), |
57 | ! |
class = "btn action-button wunder_bar_button bookmark_manager_button", |
58 | ! |
title = "Add bookmark", |
59 | ! |
tags$span( |
60 | ! |
suppressMessages(icon("fas fa-bookmark")), |
61 | ! |
if (any(is_unbookmarkable)) { |
62 | ! |
tags$span( |
63 | ! |
sum(is_unbookmarkable), |
64 | ! |
class = "badge-warning badge-count text-white bg-danger" |
65 |
) |
|
66 |
} |
|
67 |
) |
|
68 |
) |
|
69 |
} |
|
70 |
} |
|
71 | ||
72 |
#' @rdname module_bookmark_manager |
|
73 |
srv_bookmark_panel <- function(id, modules) { |
|
74 | 87x |
checkmate::assert_character(id) |
75 | 87x |
checkmate::assert_class(modules, "teal_modules") |
76 | 87x |
moduleServer(id, function(input, output, session) { |
77 | 87x |
logger::log_debug("bookmark_manager_srv initializing") |
78 | 87x |
ns <- session$ns |
79 | 87x |
bookmark_option <- get_bookmarking_option() |
80 | 87x |
is_unbookmarkable <- need_bookmarking(modules) |
81 | ||
82 |
# Set up bookmarking callbacks ---- |
|
83 |
# Register bookmark exclusions: do_bookmark button to avoid re-bookmarking |
|
84 | 87x |
setBookmarkExclude(c("do_bookmark")) |
85 |
# This bookmark can only be used on the app session. |
|
86 | 87x |
app_session <- .subset2(session, "parent") |
87 | 87x |
app_session$onBookmarked(function(url) { |
88 | ! |
logger::log_debug("bookmark_manager_srv@onBookmarked: bookmark button clicked, registering bookmark") |
89 | ! |
modal_content <- if (bookmark_option != "server") { |
90 | ! |
msg <- sprintf( |
91 | ! |
"Bookmarking has been set to \"%s\".\n%s\n%s", |
92 | ! |
bookmark_option, |
93 | ! |
"Only server-side bookmarking is supported.", |
94 | ! |
"Please contact your app developer." |
95 |
) |
|
96 | ! |
tags$div( |
97 | ! |
tags$p(msg, class = "text-warning") |
98 |
) |
|
99 |
} else { |
|
100 | ! |
tags$div( |
101 | ! |
tags$span( |
102 | ! |
tags$pre(url) |
103 |
), |
|
104 | ! |
if (any(is_unbookmarkable)) { |
105 | ! |
bkmb_summary <- rapply2( |
106 | ! |
modules_bookmarkable(modules), |
107 | ! |
function(x) { |
108 | ! |
if (isTRUE(x)) { |
109 | ! |
"\u2705" # check mark |
110 | ! |
} else if (isFALSE(x)) { |
111 | ! |
"\u274C" # cross mark |
112 |
} else { |
|
113 | ! |
"\u2753" # question mark |
114 |
} |
|
115 |
} |
|
116 |
) |
|
117 | ! |
tags$div( |
118 | ! |
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 |
), |
|
125 | ! |
tags$pre(yaml::as.yaml(bkmb_summary)) |
126 |
) |
|
127 |
} |
|
128 |
) |
|
129 |
} |
|
130 | ||
131 | ! |
showModal( |
132 | ! |
modalDialog( |
133 | ! |
id = ns("bookmark_modal"), |
134 | ! |
title = "Bookmarked teal app url", |
135 | ! |
modal_content, |
136 | ! |
easyClose = TRUE |
137 |
) |
|
138 |
) |
|
139 |
}) |
|
140 | ||
141 |
# manually trigger bookmarking because of the problems reported on windows with bookmarkButton in teal |
|
142 | 87x |
observeEvent(input$do_bookmark, { |
143 | ! |
logger::log_debug("bookmark_manager_srv@1 do_bookmark module clicked.") |
144 | ! |
session$doBookmark() |
145 |
}) |
|
146 | ||
147 | 87x |
invisible(NULL) |
148 |
}) |
|
149 |
} |
|
150 | ||
151 | ||
152 |
#' @rdname module_bookmark_manager |
|
153 |
get_bookmarking_option <- function() { |
|
154 | 87x |
bookmark_option <- getShinyOption("bookmarkStore") |
155 | 87x |
if (is.null(bookmark_option) && identical(getOption("shiny.bookmarkStore"), "server")) { |
156 | ! |
bookmark_option <- getOption("shiny.bookmarkStore") |
157 |
} |
|
158 | 87x |
bookmark_option |
159 |
} |
|
160 | ||
161 |
#' @rdname module_bookmark_manager |
|
162 |
need_bookmarking <- function(modules) { |
|
163 | 87x |
unlist(rapply2( |
164 | 87x |
modules_bookmarkable(modules), |
165 | 87x |
Negate(isTRUE) |
166 |
)) |
|
167 |
} |
|
168 | ||
169 | ||
170 |
# utilities ---- |
|
171 | ||
172 |
#' Restore value from bookmark. |
|
173 |
#' |
|
174 |
#' Get value from bookmark or return default. |
|
175 |
#' |
|
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 |
#' 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 |
#' 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 |
#' |
|
186 |
#' @param value (`character(1)`) name of value to restore |
|
187 |
#' @param default fallback value |
|
188 |
#' |
|
189 |
#' @return |
|
190 |
#' In an application restored from a server-side bookmark, |
|
191 |
#' the variable specified by `value` from the `values` environment. |
|
192 |
#' Otherwise `default`. |
|
193 |
#' |
|
194 |
#' @keywords internal |
|
195 |
#' |
|
196 |
restoreValue <- function(value, default) { # nolint: object_name. |
|
197 | 174x |
checkmate::assert_character("value") |
198 | 174x |
session_default <- shiny::getDefaultReactiveDomain() |
199 | 174x |
session_parent <- .subset2(session_default, "parent") |
200 | 174x |
session <- if (is.null(session_parent)) session_default else session_parent |
201 | ||
202 | 174x |
if (isTRUE(session$restoreContext$active) && exists(value, session$restoreContext$values, inherits = FALSE)) { |
203 | ! |
session$restoreContext$values[[value]] |
204 |
} else { |
|
205 | 174x |
default |
206 |
} |
|
207 |
} |
|
208 | ||
209 |
#' Compare bookmarks. |
|
210 |
#' |
|
211 |
#' Test if two bookmarks store identical state. |
|
212 |
#' |
|
213 |
#' `input` environments are compared one variable at a time and if not identical, |
|
214 |
#' values in both bookmarks are reported. States of `datatable`s are stripped |
|
215 |
#' of the `time` element before comparing because the time stamp is always different. |
|
216 |
#' The contents themselves are not printed as they are large and the contents are not informative. |
|
217 |
#' Elements present in one bookmark and absent in the other are also reported. |
|
218 |
#' Differences are printed as messages. |
|
219 |
#' |
|
220 |
#' `values` environments are compared with `all.equal`. |
|
221 |
#' |
|
222 |
#' @section How to use: |
|
223 |
#' Open an application, change relevant inputs (typically, all of them), and create a bookmark. |
|
224 |
#' Then open that bookmark and immediately create a bookmark of that. |
|
225 |
#' If restoring bookmarks occurred properly, the two bookmarks should store the same state. |
|
226 |
#' |
|
227 |
#' |
|
228 |
#' @param book1,book2 bookmark directories stored in `shiny_bookmarks/`; |
|
229 |
#' default to the two most recently modified directories |
|
230 |
#' |
|
231 |
#' @return |
|
232 |
#' Invisible `NULL` if bookmarks are identical or if there are no bookmarks to test. |
|
233 |
#' `FALSE` if inconsistencies are detected. |
|
234 |
#' |
|
235 |
#' @keywords internal |
|
236 |
#' |
|
237 |
bookmarks_identical <- function(book1, book2) { |
|
238 | ! |
if (!dir.exists("shiny_bookmarks")) { |
239 | ! |
message("no bookmark directory") |
240 | ! |
return(invisible(NULL)) |
241 |
} |
|
242 | ||
243 | ! |
ans <- TRUE |
244 | ||
245 | ! |
if (missing(book1) && missing(book2)) { |
246 | ! |
dirs <- list.dirs("shiny_bookmarks", recursive = FALSE) |
247 | ! |
bookmarks_sorted <- basename(rev(dirs[order(file.mtime(dirs))])) |
248 | ! |
if (length(bookmarks_sorted) < 2L) { |
249 | ! |
message("no bookmarks to compare") |
250 | ! |
return(invisible(NULL)) |
251 |
} |
|
252 | ! |
book1 <- bookmarks_sorted[2L] |
253 | ! |
book2 <- bookmarks_sorted[1L] |
254 |
} else { |
|
255 | ! |
if (!dir.exists(file.path("shiny_bookmarks", book1))) stop(book1, " not found") |
256 | ! |
if (!dir.exists(file.path("shiny_bookmarks", book2))) stop(book2, " not found") |
257 |
} |
|
258 | ||
259 | ! |
book1_input <- readRDS(file.path("shiny_bookmarks", book1, "input.rds")) |
260 | ! |
book2_input <- readRDS(file.path("shiny_bookmarks", book2, "input.rds")) |
261 | ||
262 | ! |
elements_common <- intersect(names(book1_input), names(book2_input)) |
263 | ! |
dt_states <- grepl("_state$", elements_common) |
264 | ! |
if (any(dt_states)) { |
265 | ! |
for (el in elements_common[dt_states]) { |
266 | ! |
book1_input[[el]][["time"]] <- NULL |
267 | ! |
book2_input[[el]][["time"]] <- NULL |
268 |
} |
|
269 |
} |
|
270 | ||
271 | ! |
identicals <- mapply(identical, book1_input[elements_common], book2_input[elements_common]) |
272 | ! |
non_identicals <- names(identicals[!identicals]) |
273 | ! |
compares <- sprintf("$ %s:\t%s --- %s", non_identicals, book1_input[non_identicals], book2_input[non_identicals]) |
274 | ! |
if (length(compares) != 0L) { |
275 | ! |
message("common elements not identical: \n", paste(compares, collapse = "\n")) |
276 | ! |
ans <- FALSE |
277 |
} |
|
278 | ||
279 | ! |
elements_boook1 <- setdiff(names(book1_input), names(book2_input)) |
280 | ! |
if (length(elements_boook1) != 0L) { |
281 | ! |
dt_states <- grepl("_state$", elements_boook1) |
282 | ! |
if (any(dt_states)) { |
283 | ! |
for (el in elements_boook1[dt_states]) { |
284 | ! |
if (is.list(book1_input[[el]])) book1_input[[el]] <- "--- data table state ---" |
285 |
} |
|
286 |
} |
|
287 | ! |
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 |
} |
|
291 | ||
292 | ! |
elements_boook2 <- setdiff(names(book2_input), names(book1_input)) |
293 | ! |
if (length(elements_boook2) != 0L) { |
294 | ! |
dt_states <- grepl("_state$", elements_boook1) |
295 | ! |
if (any(dt_states)) { |
296 | ! |
for (el in elements_boook1[dt_states]) { |
297 | ! |
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 | ! |
message("elements only in book2: \n", paste(excess2, collapse = "\n")) |
302 | ! |
ans <- FALSE |
303 |
} |
|
304 | ||
305 | ! |
book1_values <- readRDS(file.path("shiny_bookmarks", book1, "values.rds")) |
306 | ! |
book2_values <- readRDS(file.path("shiny_bookmarks", book2, "values.rds")) |
307 | ||
308 | ! |
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 |
} |
|
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 | 199x |
if (inherits(x, "list")) { |
323 | 87x |
lapply(x, rapply2, f = f) |
324 |
} else { |
|
325 | 112x |
f(x) |
326 |
} |
|
327 |
} |
1 |
#' Get client timezone |
|
2 |
#' |
|
3 |
#' User timezone in the browser may be different to the one on the server. |
|
4 |
#' This script can be run to register a `shiny` input which contains information about the timezone in the browser. |
|
5 |
#' |
|
6 |
#' @param ns (`function`) namespace function passed from the `session` object in the `shiny` server. |
|
7 |
#' For `shiny` modules this will allow for proper name spacing of the registered input. |
|
8 |
#' |
|
9 |
#' @return `NULL`, invisibly. |
|
10 |
#' |
|
11 |
#' @keywords internal |
|
12 |
#' |
|
13 |
get_client_timezone <- function(ns) { |
|
14 | 88x |
script <- sprintf( |
15 | 88x |
"Shiny.setInputValue(`%s`, Intl.DateTimeFormat().resolvedOptions().timeZone)", |
16 | 88x |
ns("timezone") |
17 |
) |
|
18 | 88x |
shinyjs::runjs(script) # function does not return anything |
19 | 88x |
invisible(NULL) |
20 |
} |
|
21 | ||
22 |
#' Resolve the expected bootstrap theme |
|
23 |
#' @noRd |
|
24 |
#' @keywords internal |
|
25 |
get_teal_bs_theme <- function() { |
|
26 | 4x |
bs_theme <- getOption("teal.bs_theme") |
27 | ||
28 | 4x |
if (is.null(bs_theme)) { |
29 | 1x |
return(NULL) |
30 |
} |
|
31 | ||
32 | 3x |
if (!checkmate::test_class(bs_theme, "bs_theme")) { |
33 | 2x |
warning( |
34 | 2x |
"Assertion on 'teal.bs_theme' option value failed: ", |
35 | 2x |
checkmate::check_class(bs_theme, "bs_theme"), |
36 | 2x |
". The default Shiny Bootstrap theme will be used." |
37 |
) |
|
38 | 2x |
return(NULL) |
39 |
} |
|
40 | ||
41 | 1x |
bs_theme |
42 |
} |
|
43 | ||
44 |
#' Return parentnames along with datanames. |
|
45 |
#' @noRd |
|
46 |
#' @keywords internal |
|
47 |
.include_parent_datanames <- function(datanames, join_keys) { |
|
48 | 32x |
ordered_datanames <- datanames |
49 | 32x |
for (current in datanames) { |
50 | 62x |
parents <- character(0L) |
51 | 62x |
while (length(current) > 0) { |
52 | 64x |
current <- teal.data::parent(join_keys, current) |
53 | 64x |
parents <- c(current, parents) |
54 |
} |
|
55 | 62x |
ordered_datanames <- c(parents, ordered_datanames) |
56 |
} |
|
57 | ||
58 | 32x |
unique(ordered_datanames) |
59 |
} |
|
60 | ||
61 |
#' Create a `FilteredData` |
|
62 |
#' |
|
63 |
#' Create a `FilteredData` object from a `teal_data` object. |
|
64 |
#' |
|
65 |
#' @param x (`teal_data`) object |
|
66 |
#' @param datanames (`character`) vector of data set names to include; must be subset of `names(x)` |
|
67 |
#' @return A `FilteredData` object. |
|
68 |
#' @keywords internal |
|
69 |
teal_data_to_filtered_data <- function(x, datanames = names(x)) { |
|
70 | 83x |
checkmate::assert_class(x, "teal_data") |
71 | 83x |
checkmate::assert_character(datanames, min.chars = 1L, any.missing = FALSE) |
72 |
# Otherwise, FilteredData will be created in the modules' scope later |
|
73 | 83x |
teal.slice::init_filtered_data( |
74 | 83x |
x = Filter(length, sapply(datanames, function(dn) x[[dn]], simplify = FALSE)), |
75 | 83x |
join_keys = teal.data::join_keys(x) |
76 |
) |
|
77 |
} |
|
78 | ||
79 | ||
80 |
#' Template function for `TealReportCard` creation and customization |
|
81 |
#' |
|
82 |
#' This function generates a report card with a title, |
|
83 |
#' an optional description, and the option to append the filter state list. |
|
84 |
#' |
|
85 |
#' @param title (`character(1)`) title of the card (unless overwritten by label) |
|
86 |
#' @param label (`character(1)`) label provided by the user when adding the card |
|
87 |
#' @param description (`character(1)`) optional, additional description |
|
88 |
#' @param with_filter (`logical(1)`) flag indicating to add filter state |
|
89 |
#' @param filter_panel_api (`FilterPanelAPI`) object with API that allows the generation |
|
90 |
#' of the filter state in the report |
|
91 |
#' |
|
92 |
#' @return (`TealReportCard`) populated with a title, description and filter state. |
|
93 |
#' |
|
94 |
#' @export |
|
95 |
report_card_template <- function(title, label, description = NULL, with_filter, filter_panel_api) { |
|
96 | 2x |
checkmate::assert_string(title) |
97 | 2x |
checkmate::assert_string(label) |
98 | 2x |
checkmate::assert_string(description, null.ok = TRUE) |
99 | 2x |
checkmate::assert_flag(with_filter) |
100 | 2x |
checkmate::assert_class(filter_panel_api, classes = "FilterPanelAPI") |
101 | ||
102 | 2x |
card <- teal::TealReportCard$new() |
103 | 2x |
title <- if (label == "") title else label |
104 | 2x |
card$set_name(title) |
105 | 2x |
card$append_text(title, "header2") |
106 | 1x |
if (!is.null(description)) card$append_text(description, "header3") |
107 | 1x |
if (with_filter) card$append_fs(filter_panel_api$get_filter_state()) |
108 | 2x |
card |
109 |
} |
|
110 | ||
111 | ||
112 |
#' Check `datanames` in modules |
|
113 |
#' |
|
114 |
#' These functions check if specified `datanames` in modules match those in the data object, |
|
115 |
#' returning error messages or `TRUE` for successful validation. Two functions return error message |
|
116 |
#' in different forms: |
|
117 |
#' - `check_modules_datanames` returns `character(1)` for basic assertion usage |
|
118 |
#' - `check_modules_datanames_html` returns `shiny.tag.list` to display it in the app. |
|
119 |
#' |
|
120 |
#' @param modules (`teal_modules`) object |
|
121 |
#' @param datanames (`character`) names of datasets available in the `data` object |
|
122 |
#' |
|
123 |
#' @return `TRUE` if validation passes, otherwise `character(1)` or `shiny.tag.list` |
|
124 |
#' @keywords internal |
|
125 |
check_modules_datanames <- function(modules, datanames) { |
|
126 | 11x |
out <- check_modules_datanames_html(modules, datanames) |
127 | 11x |
if (inherits(out, "shiny.tag.list")) { |
128 | 5x |
out_with_ticks <- gsub("<code>|</code>", "`", toString(out)) |
129 | 5x |
out_text <- gsub("<[^<>]+>", "", toString(out_with_ticks)) |
130 | 5x |
trimws(gsub("[[:space:]]+", " ", out_text)) |
131 |
} else { |
|
132 | 6x |
out |
133 |
} |
|
134 |
} |
|
135 | ||
136 |
#' @rdname check_modules_datanames |
|
137 |
check_reserved_datanames <- function(datanames) { |
|
138 | 190x |
reserved_datanames <- datanames[datanames %in% c("all", ".raw_data")] |
139 | 190x |
if (length(reserved_datanames) == 0L) { |
140 | 184x |
return(NULL) |
141 |
} |
|
142 | ||
143 | 6x |
tags$span( |
144 | 6x |
to_html_code_list(reserved_datanames), |
145 | 6x |
sprintf( |
146 | 6x |
"%s reserved for internal use. Please avoid using %s as %s.", |
147 | 6x |
pluralize(reserved_datanames, "is", "are"), |
148 | 6x |
pluralize(reserved_datanames, "it", "them"), |
149 | 6x |
pluralize(reserved_datanames, "a dataset name", "dataset names") |
150 |
) |
|
151 |
) |
|
152 |
} |
|
153 | ||
154 |
#' @rdname check_modules_datanames |
|
155 |
check_modules_datanames_html <- function(modules, datanames) { |
|
156 | 190x |
check_datanames <- check_modules_datanames_recursive(modules, datanames) |
157 | 190x |
show_module_info <- inherits(modules, "teal_modules") # used in two contexts - module and app |
158 | ||
159 | 190x |
reserved_datanames <- check_reserved_datanames(datanames) |
160 | ||
161 | 190x |
if (!length(check_datanames)) { |
162 | 172x |
out <- if (is.null(reserved_datanames)) { |
163 | 166x |
TRUE |
164 |
} else { |
|
165 | 6x |
shiny::tagList(reserved_datanames) |
166 |
} |
|
167 | 172x |
return(out) |
168 |
} |
|
169 | 18x |
shiny::tagList( |
170 | 18x |
reserved_datanames, |
171 | 18x |
lapply( |
172 | 18x |
check_datanames, |
173 | 18x |
function(mod) { |
174 | 18x |
tagList( |
175 | 18x |
tags$span( |
176 | 18x |
tags$span(pluralize(mod$missing_datanames, "Dataset")), |
177 | 18x |
to_html_code_list(mod$missing_datanames), |
178 | 18x |
tags$span( |
179 | 18x |
sprintf( |
180 | 18x |
"%s missing%s.", |
181 | 18x |
pluralize(mod$missing_datanames, "is", "are"), |
182 | 18x |
if (show_module_info) sprintf(" for module '%s'", mod$label) else "" |
183 |
) |
|
184 |
) |
|
185 |
), |
|
186 | 18x |
if (length(datanames) >= 1) { |
187 | 16x |
tagList( |
188 | 16x |
tags$span(pluralize(datanames, "Dataset")), |
189 | 16x |
tags$span("available in data:"), |
190 | 16x |
tagList( |
191 | 16x |
tags$span( |
192 | 16x |
to_html_code_list(datanames), |
193 | 16x |
tags$span(".", .noWS = "outside"), |
194 | 16x |
.noWS = c("outside") |
195 |
) |
|
196 |
) |
|
197 |
) |
|
198 |
} else { |
|
199 | 2x |
tags$span("No datasets are available in data.") |
200 |
}, |
|
201 | 18x |
tags$br(.noWS = "before") |
202 |
) |
|
203 |
} |
|
204 |
) |
|
205 |
) |
|
206 |
} |
|
207 | ||
208 |
#' Recursively checks modules and returns list for every datanames mismatch between module and data |
|
209 |
#' @noRd |
|
210 |
check_modules_datanames_recursive <- function(modules, datanames) { # nolint: object_name_length |
|
211 | 296x |
checkmate::assert_multi_class(modules, c("teal_module", "teal_modules")) |
212 | 296x |
checkmate::assert_character(datanames) |
213 | 296x |
if (inherits(modules, "teal_modules")) { |
214 | 86x |
unlist( |
215 | 86x |
lapply(modules$children, check_modules_datanames_recursive, datanames = datanames), |
216 | 86x |
recursive = FALSE |
217 |
) |
|
218 |
} else { |
|
219 | 210x |
missing_datanames <- setdiff(modules$datanames, c("all", datanames)) |
220 | 210x |
if (length(missing_datanames)) { |
221 | 18x |
list(list( |
222 | 18x |
label = modules$label, |
223 | 18x |
missing_datanames = missing_datanames |
224 |
)) |
|
225 |
} |
|
226 |
} |
|
227 |
} |
|
228 | ||
229 |
#' Convert character vector to html code separated with commas and "and" |
|
230 |
#' @noRd |
|
231 |
to_html_code_list <- function(x) { |
|
232 | 40x |
checkmate::assert_character(x) |
233 | 40x |
do.call( |
234 | 40x |
tagList, |
235 | 40x |
lapply(seq_along(x), function(.ix) { |
236 | 56x |
tagList( |
237 | 56x |
tags$code(x[.ix]), |
238 | 56x |
if (.ix != length(x)) { |
239 | 1x |
if (.ix == length(x) - 1) tags$span(" and ") else tags$span(", ", .noWS = "before") |
240 |
} |
|
241 |
) |
|
242 |
}) |
|
243 |
) |
|
244 |
} |
|
245 | ||
246 | ||
247 |
#' Check `datanames` in filters |
|
248 |
#' |
|
249 |
#' This function checks whether `datanames` in filters correspond to those in `data`, |
|
250 |
#' returning character vector with error messages or `TRUE` if all checks pass. |
|
251 |
#' |
|
252 |
#' @param filters (`teal_slices`) object |
|
253 |
#' @param datanames (`character`) names of datasets available in the `data` object |
|
254 |
#' |
|
255 |
#' @return A `character(1)` containing error message or TRUE if validation passes. |
|
256 |
#' @keywords internal |
|
257 |
check_filter_datanames <- function(filters, datanames) { |
|
258 | 86x |
checkmate::assert_class(filters, "teal_slices") |
259 | 86x |
checkmate::assert_character(datanames) |
260 | ||
261 |
# check teal_slices against datanames |
|
262 | 86x |
out <- unlist(sapply( |
263 | 86x |
filters, function(filter) { |
264 | 24x |
dataname <- shiny::isolate(filter$dataname) |
265 | 24x |
if (!dataname %in% datanames) { |
266 | 3x |
sprintf( |
267 | 3x |
"- Filter '%s' refers to dataname not available in 'data':\n %s not in (%s)", |
268 | 3x |
shiny::isolate(filter$id), |
269 | 3x |
dQuote(dataname, q = FALSE), |
270 | 3x |
toString(dQuote(datanames, q = FALSE)) |
271 |
) |
|
272 |
} |
|
273 |
} |
|
274 |
)) |
|
275 | ||
276 | ||
277 | 86x |
if (length(out)) { |
278 | 3x |
paste(out, collapse = "\n") |
279 |
} else { |
|
280 | 83x |
TRUE |
281 |
} |
|
282 |
} |
|
283 | ||
284 |
#' Function for validating the title parameter of `teal::init` |
|
285 |
#' |
|
286 |
#' Checks if the input of the title from `teal::init` will create a valid title and favicon tag. |
|
287 |
#' @param shiny_tag (`shiny.tag`) Object to validate for a valid title. |
|
288 |
#' @keywords internal |
|
289 |
validate_app_title_tag <- function(shiny_tag) { |
|
290 | 7x |
checkmate::assert_class(shiny_tag, "shiny.tag") |
291 | 7x |
checkmate::assert_true(shiny_tag$name == "head") |
292 | 6x |
child_names <- vapply(shiny_tag$children, `[[`, character(1L), "name") |
293 | 6x |
checkmate::assert_subset(c("title", "link"), child_names, .var.name = "child tags") |
294 | 4x |
rel_attr <- shiny_tag$children[[which(child_names == "link")]]$attribs$rel |
295 | 4x |
checkmate::assert_subset( |
296 | 4x |
rel_attr, |
297 | 4x |
c("icon", "shortcut icon"), |
298 | 4x |
.var.name = "Link tag's rel attribute", |
299 | 4x |
empty.ok = FALSE |
300 |
) |
|
301 |
} |
|
302 | ||
303 |
#' Build app title with favicon |
|
304 |
#' |
|
305 |
#' A helper function to create the browser title along with a logo. |
|
306 |
#' |
|
307 |
#' @param title (`character`) The browser title for the `teal` app. |
|
308 |
#' @param favicon (`character`) The path for the icon for the title. |
|
309 |
#' The image/icon path can be remote or the static path accessible by `shiny`, like the `www/` |
|
310 |
#' |
|
311 |
#' @return A `shiny.tag` containing the element that adds the title and logo to the `shiny` app. |
|
312 |
#' @export |
|
313 |
build_app_title <- function( |
|
314 |
title = "teal app", |
|
315 |
favicon = "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/nest.png") { |
|
316 | 15x |
checkmate::assert_string(title, null.ok = TRUE) |
317 | 15x |
checkmate::assert_string(favicon, null.ok = TRUE) |
318 | 15x |
tags$head( |
319 | 15x |
tags$title(title), |
320 | 15x |
tags$link( |
321 | 15x |
rel = "icon", |
322 | 15x |
href = favicon, |
323 | 15x |
sizes = "any" |
324 |
) |
|
325 |
) |
|
326 |
} |
|
327 | ||
328 |
#' Application ID |
|
329 |
#' |
|
330 |
#' Creates App ID used to match filter snapshots to application. |
|
331 |
#' |
|
332 |
#' Calculate app ID that will be used to stamp filter state snapshots. |
|
333 |
#' App ID is a hash of the app's data and modules. |
|
334 |
#' See "transferring snapshots" section in ?snapshot. |
|
335 |
#' |
|
336 |
#' @param data (`teal_data` or `teal_data_module`) as accepted by `init` |
|
337 |
#' @param modules (`teal_modules`) object as accepted by `init` |
|
338 |
#' |
|
339 |
#' @return A single character string. |
|
340 |
#' |
|
341 |
#' @keywords internal |
|
342 |
create_app_id <- function(data, modules) { |
|
343 | 23x |
checkmate::assert_multi_class(data, c("teal_data", "teal_data_module")) |
344 | 22x |
checkmate::assert_class(modules, "teal_modules") |
345 | ||
346 | 21x |
data <- if (inherits(data, "teal_data")) { |
347 | 19x |
as.list(data) |
348 | 21x |
} else if (inherits(data, "teal_data_module")) { |
349 | 2x |
deparse1(body(data$server)) |
350 |
} |
|
351 | 21x |
modules <- lapply(modules, defunction) |
352 | ||
353 | 21x |
rlang::hash(list(data = data, modules = modules)) |
354 |
} |
|
355 | ||
356 |
#' Go through list and extract bodies of encountered functions as string, recursively. |
|
357 |
#' @keywords internal |
|
358 |
#' @noRd |
|
359 |
defunction <- function(x) { |
|
360 | 297x |
if (is.list(x)) { |
361 | 121x |
lapply(x, defunction) |
362 | 176x |
} else if (is.function(x)) { |
363 | 54x |
deparse1(body(x)) |
364 |
} else { |
|
365 | 122x |
x |
366 |
} |
|
367 |
} |
|
368 | ||
369 |
#' Get unique labels |
|
370 |
#' |
|
371 |
#' Get unique labels for the modules to avoid namespace conflicts. |
|
372 |
#' |
|
373 |
#' @param labels (`character`) vector of labels |
|
374 |
#' |
|
375 |
#' @return (`character`) vector of unique labels |
|
376 |
#' |
|
377 |
#' @keywords internal |
|
378 |
get_unique_labels <- function(labels) { |
|
379 | 141x |
make.unique(gsub("[^[:alnum:]]", "_", tolower(labels)), sep = "_") |
380 |
} |
|
381 | ||
382 |
#' @keywords internal |
|
383 |
#' @noRd |
|
384 | 4x |
pasten <- function(...) paste0(..., "\n") |
385 | ||
386 |
#' Convert character list to human readable html with commas and "and" |
|
387 |
#' @noRd |
|
388 |
paste_datanames_character <- function(x, |
|
389 |
tags = list(span = shiny::tags$span, code = shiny::tags$code), |
|
390 |
tagList = shiny::tagList) { # nolint: object_name. |
|
391 | ! |
checkmate::assert_character(x) |
392 | ! |
do.call( |
393 | ! |
tagList, |
394 | ! |
lapply(seq_along(x), function(.ix) { |
395 | ! |
tagList( |
396 | ! |
tags$code(x[.ix]), |
397 | ! |
if (.ix != length(x)) { |
398 | ! |
tags$span(if (.ix == length(x) - 1) " and " else ", ") |
399 |
} |
|
400 |
) |
|
401 |
}) |
|
402 |
) |
|
403 |
} |
|
404 | ||
405 |
#' Build datanames error string for error message |
|
406 |
#' |
|
407 |
#' tags and tagList are overwritten in arguments allowing to create strings for |
|
408 |
#' logging purposes |
|
409 |
#' @noRd |
|
410 |
build_datanames_error_message <- function(label = NULL, |
|
411 |
datanames, |
|
412 |
extra_datanames, |
|
413 |
tags = list(span = shiny::tags$span, code = shiny::tags$code), |
|
414 |
tagList = shiny::tagList) { # nolint: object_name. |
|
415 | ! |
tags$span( |
416 | ! |
tags$span(pluralize(extra_datanames, "Dataset")), |
417 | ! |
paste_datanames_character(extra_datanames, tags, tagList), |
418 | ! |
tags$span( |
419 | ! |
sprintf( |
420 | ! |
"%s missing%s", |
421 | ! |
pluralize(extra_datanames, "is", "are"), |
422 | ! |
if (is.null(label)) "" else sprintf(" for tab '%s'", label) |
423 |
) |
|
424 |
), |
|
425 | ! |
if (length(datanames) >= 1) { |
426 | ! |
tagList( |
427 | ! |
tags$span(pluralize(datanames, "Dataset")), |
428 | ! |
tags$span("available in data:"), |
429 | ! |
tagList( |
430 | ! |
tags$span( |
431 | ! |
paste_datanames_character(datanames, tags, tagList), |
432 | ! |
tags$span(".", .noWS = "outside"), |
433 | ! |
.noWS = c("outside") |
434 |
) |
|
435 |
) |
|
436 |
) |
|
437 |
} else { |
|
438 | ! |
tags$span("No datasets are available in data.") |
439 |
} |
|
440 |
) |
|
441 |
} |
|
442 | ||
443 |
#' Smart `rbind` |
|
444 |
#' |
|
445 |
#' Combine `data.frame` objects which have different columns |
|
446 |
#' |
|
447 |
#' @param ... (`data.frame`) |
|
448 |
#' @keywords internal |
|
449 |
.smart_rbind <- function(...) { |
|
450 | 89x |
dots <- list(...) |
451 | 89x |
checkmate::assert_list(dots, "data.frame", .var.name = "...") |
452 | 89x |
Reduce( |
453 | 89x |
x = dots, |
454 | 89x |
function(x, y) { |
455 | 72x |
all_columns <- union(colnames(x), colnames(y)) |
456 | 72x |
x[setdiff(all_columns, colnames(x))] <- NA |
457 | 72x |
y[setdiff(all_columns, colnames(y))] <- NA |
458 | 72x |
rbind(x, y) |
459 |
} |
|
460 |
) |
|
461 |
} |
|
462 | ||
463 |
#' Pluralize a word depending on the size of the input |
|
464 |
#' |
|
465 |
#' @param x (`object`) to check length for plural. |
|
466 |
#' @param singular (`character`) singular form of the word. |
|
467 |
#' @param plural (optional `character`) plural form of the word. If not given an "s" |
|
468 |
#' is added to the singular form. |
|
469 |
#' |
|
470 |
#' @return A `character` that correctly represents the size of the `x` argument. |
|
471 |
#' @keywords internal |
|
472 |
pluralize <- function(x, singular, plural = NULL) { |
|
473 | 70x |
checkmate::assert_string(singular) |
474 | 70x |
checkmate::assert_string(plural, null.ok = TRUE) |
475 | 70x |
if (length(x) == 1L) { # Zero length object should use plural form. |
476 | 42x |
singular |
477 |
} else { |
|
478 | 28x |
if (is.null(plural)) { |
479 | 12x |
sprintf("%ss", singular) |
480 |
} else { |
|
481 | 16x |
plural |
482 |
} |
|
483 |
} |
|
484 |
} |
1 |
#' Module to transform `reactive` `teal_data` |
|
2 |
#' |
|
3 |
#' Module calls [teal_transform_module()] in sequence so that `reactive teal_data` output |
|
4 |
#' from one module is handed over to the following module's input. |
|
5 |
#' |
|
6 |
#' @inheritParams module_teal_data |
|
7 |
#' @inheritParams teal_modules |
|
8 |
#' @param class (character(1)) CSS class to be added in the `div` wrapper tag. |
|
9 | ||
10 |
#' @return `reactive` `teal_data` |
|
11 |
#' |
|
12 |
#' @name module_transform_data |
|
13 |
NULL |
|
14 | ||
15 |
#' @export |
|
16 |
#' @rdname module_transform_data |
|
17 |
ui_transform_teal_data <- function(id, transformators, class = "well") { |
|
18 | 1x |
checkmate::assert_string(id) |
19 | 1x |
if (length(transformators) == 0L) { |
20 | ! |
return(NULL) |
21 |
} |
|
22 | 1x |
if (inherits(transformators, "teal_transform_module")) { |
23 | 1x |
transformators <- list(transformators) |
24 |
} |
|
25 | 1x |
checkmate::assert_list(transformators, "teal_transform_module") |
26 | 1x |
names(transformators) <- sprintf("transform_%d", seq_len(length(transformators))) |
27 | ||
28 | 1x |
lapply( |
29 | 1x |
names(transformators), |
30 | 1x |
function(name) { |
31 | 1x |
child_id <- NS(id, name) |
32 | 1x |
ns <- NS(child_id) |
33 | 1x |
data_mod <- transformators[[name]] |
34 | 1x |
transform_wrapper_id <- ns(sprintf("wrapper_%s", name)) |
35 | ||
36 | 1x |
display_fun <- if (is.null(data_mod$ui)) shinyjs::hidden else function(x) x |
37 | ||
38 | 1x |
display_fun( |
39 | 1x |
div( |
40 |
# class .teal_validated changes the color of the boarder on error in ui_validate_reactive_teal_data |
|
41 |
# For details see tealValidate.js file. |
|
42 | 1x |
id = ns("wrapper"), |
43 | 1x |
class = c(class, "teal_validated"), |
44 | 1x |
title = attr(data_mod, "label"), |
45 | 1x |
tags$span( |
46 | 1x |
class = "text-primary mb-4", |
47 | 1x |
icon("fas fa-square-pen"), |
48 | 1x |
attr(data_mod, "label") |
49 |
), |
|
50 | 1x |
tags$i( |
51 | 1x |
class = "remove pull-right fa fa-angle-down", |
52 | 1x |
style = "cursor: pointer;", |
53 | 1x |
title = "fold/expand transformator panel", |
54 | 1x |
onclick = sprintf("togglePanelItems(this, '%s', 'fa-angle-right', 'fa-angle-down');", transform_wrapper_id) |
55 |
), |
|
56 | 1x |
tags$div( |
57 | 1x |
id = transform_wrapper_id, |
58 | 1x |
if (is.null(data_mod$ui)) { |
59 | ! |
return(NULL) |
60 |
} else { |
|
61 | 1x |
data_mod$ui(id = ns("transform")) |
62 |
}, |
|
63 | 1x |
div( |
64 | 1x |
id = ns("validate_messages"), |
65 | 1x |
class = "teal_validated", |
66 | 1x |
uiOutput(ns("error_wrapper")) |
67 |
) |
|
68 |
) |
|
69 |
) |
|
70 |
) |
|
71 |
} |
|
72 |
) |
|
73 |
} |
|
74 | ||
75 |
#' @export |
|
76 |
#' @rdname module_transform_data |
|
77 |
srv_transform_teal_data <- function(id, data, transformators, modules = NULL, is_transform_failed = reactiveValues()) { |
|
78 | 94x |
checkmate::assert_string(id) |
79 | 94x |
assert_reactive(data) |
80 | 94x |
checkmate::assert_class(modules, "teal_module", null.ok = TRUE) |
81 | 94x |
if (length(transformators) == 0L) { |
82 | 71x |
return(data) |
83 |
} |
|
84 | 23x |
if (inherits(transformators, "teal_transform_module")) { |
85 | 3x |
transformators <- list(transformators) |
86 |
} |
|
87 | 23x |
checkmate::assert_list(transformators, "teal_transform_module", null.ok = TRUE) |
88 | 23x |
names(transformators) <- sprintf("transform_%d", seq_len(length(transformators))) |
89 | ||
90 | 23x |
moduleServer(id, function(input, output, session) { |
91 | 23x |
module_output <- Reduce( |
92 | 23x |
function(data_previous, name) { |
93 | 26x |
moduleServer(name, function(input, output, session) { |
94 | 26x |
logger::log_debug("srv_transform_teal_data initializing for { name }.") |
95 | 26x |
is_transform_failed[[name]] <- FALSE |
96 | 26x |
data_out <- transformators[[name]]$server("transform", data = data_previous) |
97 | 26x |
data_handled <- reactive(tryCatch(data_out(), error = function(e) e)) |
98 | 26x |
observeEvent(data_handled(), { |
99 | 32x |
if (inherits(data_handled(), "teal_data")) { |
100 | 22x |
is_transform_failed[[name]] <- FALSE |
101 |
} else { |
|
102 | 10x |
is_transform_failed[[name]] <- TRUE |
103 |
} |
|
104 |
}) |
|
105 | ||
106 | 26x |
is_previous_failed <- reactive({ |
107 | 29x |
idx_this <- which(names(is_transform_failed) == name) |
108 | 29x |
is_transform_failed_list <- reactiveValuesToList(is_transform_failed) |
109 | 29x |
idx_failures <- which(unlist(is_transform_failed_list)) |
110 | 29x |
any(idx_failures < idx_this) |
111 |
}) |
|
112 | ||
113 | 26x |
srv_validate_error("silent_error", data_handled, validate_shiny_silent_error = FALSE) |
114 | 26x |
srv_check_class_teal_data("class_teal_data", data_handled) |
115 | 26x |
if (!is.null(modules)) { |
116 | 20x |
srv_check_module_datanames("datanames_warning", data_handled, modules) |
117 |
} |
|
118 | ||
119 |
# When there is no UI (`ui = NULL`) it should still show the errors |
|
120 | 26x |
observe({ |
121 | 32x |
if (!inherits(data_handled(), "teal_data") && !is_previous_failed()) { |
122 | 10x |
shinyjs::show("wrapper") |
123 |
} |
|
124 |
}) |
|
125 | ||
126 | 26x |
transform_wrapper_id <- sprintf("wrapper_%s", name) |
127 | 26x |
output$error_wrapper <- renderUI({ |
128 | 29x |
if (is_previous_failed()) { |
129 | ! |
shinyjs::disable(transform_wrapper_id) |
130 | ! |
tags$div("One of previous transformators failed. Please check its inputs.", class = "teal-output-warning") |
131 |
} else { |
|
132 | 29x |
shinyjs::enable(transform_wrapper_id) |
133 | 29x |
shiny::tagList( |
134 | 29x |
ui_validate_error(session$ns("silent_error")), |
135 | 29x |
ui_check_class_teal_data(session$ns("class_teal_data")), |
136 | 29x |
ui_check_module_datanames(session$ns("datanames_warning")) |
137 |
) |
|
138 |
} |
|
139 |
}) |
|
140 | ||
141 | 26x |
.trigger_on_success(data_handled) |
142 |
}) |
|
143 |
}, |
|
144 | 23x |
x = names(transformators), |
145 | 23x |
init = data |
146 |
) |
|
147 | 23x |
module_output |
148 |
}) |
|
149 |
} |
1 |
#' Create a `teal` module for previewing a report |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("experimental")` |
|
4 |
#' |
|
5 |
#' This function wraps [teal.reporter::reporter_previewer_ui()] and |
|
6 |
#' [teal.reporter::reporter_previewer_srv()] into a `teal_module` to be |
|
7 |
#' used in `teal` applications. |
|
8 |
#' |
|
9 |
#' If you are creating a `teal` application using [init()] then this |
|
10 |
#' module will be added to your application automatically if any of your `teal_modules` |
|
11 |
#' support report generation. |
|
12 |
#' |
|
13 |
#' @inheritParams teal_modules |
|
14 |
#' @param server_args (named `list`) |
|
15 |
#' Arguments passed to [teal.reporter::reporter_previewer_srv()]. |
|
16 |
#' |
|
17 |
#' @return |
|
18 |
#' `teal_module` (extended with `teal_module_previewer` class) containing the `teal.reporter` previewer functionality. |
|
19 |
#' |
|
20 |
#' @export |
|
21 |
#' |
|
22 |
reporter_previewer_module <- function(label = "Report previewer", server_args = list()) { |
|
23 | 7x |
checkmate::assert_string(label) |
24 | 5x |
checkmate::assert_list(server_args, names = "named") |
25 | 5x |
checkmate::assert_true(all(names(server_args) %in% names(formals(teal.reporter::reporter_previewer_srv)))) |
26 | ||
27 | 3x |
message("Initializing reporter_previewer_module") |
28 | ||
29 | 3x |
srv <- function(id, reporter, ...) { |
30 | ! |
teal.reporter::reporter_previewer_srv(id, reporter, ...) |
31 |
} |
|
32 | ||
33 | 3x |
ui <- function(id, ...) { |
34 | ! |
teal.reporter::reporter_previewer_ui(id, ...) |
35 |
} |
|
36 | ||
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 |
) |
|
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 |
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 |
#' Include `CSS` files from `/inst/css/` package directory to application header |
|
2 |
#' |
|
3 |
#' `system.file` should not be used to access files in other packages, it does |
|
4 |
#' not work with `devtools`. Therefore, we redefine this method in each package |
|
5 |
#' as needed. Thus, we do not export this method. |
|
6 |
#' |
|
7 |
#' @param pattern (`character`) pattern of files to be included |
|
8 |
#' |
|
9 |
#' @return HTML code that includes `CSS` files. |
|
10 |
#' @keywords internal |
|
11 |
include_css_files <- function(pattern = "*") { |
|
12 | ! |
css_files <- list.files( |
13 | ! |
system.file("css", package = "teal", mustWork = TRUE), |
14 | ! |
pattern = pattern, full.names = TRUE |
15 |
) |
|
16 | ||
17 | ! |
singleton( |
18 | ! |
tags$head(lapply(css_files, includeCSS)) |
19 |
) |
|
20 |
} |
|
21 | ||
22 |
#' Include `JS` files from `/inst/js/` package directory to application header |
|
23 |
#' |
|
24 |
#' `system.file` should not be used to access files in other packages, it does |
|
25 |
#' not work with `devtools`. Therefore, we redefine this method in each package |
|
26 |
#' as needed. Thus, we do not export this method |
|
27 |
#' |
|
28 |
#' @param pattern (`character`) pattern of files to be included, passed to `system.file` |
|
29 |
#' @param except (`character`) vector of basename filenames to be excluded |
|
30 |
#' |
|
31 |
#' @return HTML code that includes `JS` files. |
|
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) |
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 | ||
38 | ! |
singleton(lapply(js_files, includeScript)) |
39 |
} |
|
40 | ||
41 |
#' Run `JS` file from `/inst/js/` package directory |
|
42 |
#' |
|
43 |
#' This is triggered from the server to execute on the client |
|
44 |
#' rather than triggered directly on the client. |
|
45 |
#' Unlike `include_js_files` which includes `JavaScript` functions, |
|
46 |
#' the `run_js` actually executes `JavaScript` functions. |
|
47 |
#' |
|
48 |
#' `system.file` should not be used to access files in other packages, it does |
|
49 |
#' not work with `devtools`. Therefore, we redefine this method in each package |
|
50 |
#' as needed. Thus, we do not export this method. |
|
51 |
#' |
|
52 |
#' @param files (`character`) vector of filenames. |
|
53 |
#' |
|
54 |
#' @return `NULL`, invisibly. |
|
55 |
#' @keywords internal |
|
56 |
run_js_files <- function(files) { |
|
57 | 88x |
checkmate::assert_character(files, min.len = 1, any.missing = FALSE) |
58 | 88x |
lapply(files, function(file) { |
59 | 88x |
shinyjs::runjs(paste0(readLines(system.file("js", file, package = "teal", mustWork = TRUE)), collapse = "\n")) |
60 |
}) |
|
61 | 88x |
invisible(NULL) |
62 |
} |
|
63 | ||
64 |
#' Code to include `teal` `CSS` and `JavaScript` files |
|
65 |
#' |
|
66 |
#' This is useful when you want to use the same `JavaScript` and `CSS` files that are |
|
67 |
#' used with the `teal` application. |
|
68 |
#' This is also useful for running standalone modules in `teal` with the correct |
|
69 |
#' styles. |
|
70 |
#' Also initializes `shinyjs` so you can use it. |
|
71 |
#' |
|
72 |
#' Simply add `include_teal_css_js()` as one of the UI elements. |
|
73 |
#' @return A `shiny.tag.list`. |
|
74 |
#' @keywords internal |
|
75 |
include_teal_css_js <- function() { |
|
76 | ! |
tagList( |
77 | ! |
shinyjs::useShinyjs(), |
78 | ! |
include_css_files(), |
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 |
82 |
) |
|
83 |
} |
1 |
.onLoad <- function(libname, pkgname) { |
|
2 |
# adapted from https://github.com/r-lib/devtools/blob/master/R/zzz.R |
|
3 | ||
4 | ! |
teal_default_options <- list( |
5 | ! |
teal.show_js_log = FALSE, |
6 | ! |
teal.lockfile.mode = "auto", |
7 | ! |
shiny.sanitize.errors = FALSE |
8 |
) |
|
9 | ||
10 | ! |
op <- options() |
11 | ! |
toset <- !(names(teal_default_options) %in% names(op)) |
12 | ! |
if (any(toset)) options(teal_default_options[toset]) |
13 | ||
14 |
# Set up the teal logger instance |
|
15 | ! |
teal.logger::register_logger("teal") |
16 | ! |
teal.logger::register_handlers("teal") |
17 | ||
18 | ! |
invisible() |
19 |
} |
|
20 | ||
21 |
.onAttach <- function(libname, pkgname) { |
|
22 | 2x |
packageStartupMessage( |
23 | 2x |
"\nYou are using teal version ", |
24 |
# `system.file` uses the `shim` of `system.file` by `teal` |
|
25 |
# we avoid `desc` dependency here to get the version |
|
26 | 2x |
read.dcf(system.file("DESCRIPTION", package = "teal"))[, "Version"] |
27 |
) |
|
28 |
} |
|
29 | ||
30 |
# This one is here because setdiff_teal_slice should not be exported from teal.slice. |
|
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 |
coalesce_r <- getFromNamespace("coalesce_r", "teal.slice") |
|
34 |
# all *Block objects are private in teal.reporter |
|
35 |
RcodeBlock <- getFromNamespace("RcodeBlock", "teal.reporter") # nolint: object_name. |
|
36 | ||
37 |
# Use non-exported function(s) from teal.code |
|
38 |
# This one is here because lang2calls should not be exported from teal.code |
|
39 |
lang2calls <- getFromNamespace("lang2calls", "teal.code") |
|
40 |
code2list <- getFromNamespace("code2list", "teal.data") |
1 |
#' Data Module for teal |
|
2 |
#' |
|
3 |
#' This module manages the `data` argument for `srv_teal`. The `teal` framework uses [teal.data::teal_data()], |
|
4 |
#' which can be provided in various ways: |
|
5 |
#' 1. Directly as a [teal.data::teal_data()] object. This will automatically convert it into a `reactive` `teal_data`. |
|
6 |
#' 2. As a `reactive` object that returns a [teal.data::teal_data()] object. |
|
7 |
#' |
|
8 |
#' @details |
|
9 |
#' ## Reactive `teal_data`: |
|
10 |
#' |
|
11 |
#' The data in the application can be reactively updated, prompting [srv_teal()] to rebuild the |
|
12 |
#' content accordingly. There are two methods for creating interactive `teal_data`: |
|
13 |
#' 1. Using a `reactive` object provided from outside the `teal` application. In this scenario, |
|
14 |
#' reactivity is controlled by an external module, and `srv_teal` responds to changes. |
|
15 |
#' 2. Using [teal_data_module()], which is embedded within the `teal` application, allowing data to |
|
16 |
#' be resubmitted by the user as needed. |
|
17 |
#' |
|
18 |
#' Since the server of [teal_data_module()] must return a `reactive` `teal_data` object, both |
|
19 |
#' methods (1 and 2) produce the same reactive behavior within a `teal` application. The distinction |
|
20 |
#' lies in data control: the first method involves external control, while the second method |
|
21 |
#' involves control from a custom module within the app. |
|
22 |
#' |
|
23 |
#' For more details, see [`module_teal_data`]. |
|
24 |
#' |
|
25 |
#' @inheritParams init |
|
26 |
#' |
|
27 |
#' @param data (`teal_data`, `teal_data_module`, or `reactive` returning `teal_data`) |
|
28 |
#' The data which application will depend on. |
|
29 |
#' |
|
30 |
#' @return A `reactive` object that returns: |
|
31 |
#' Output of the `data`. If `data` fails then returned error is handled (after [tryCatch()]) so that |
|
32 |
#' rest of the application can respond to this respectively. |
|
33 |
#' |
|
34 |
#' @rdname module_init_data |
|
35 |
#' @name module_init_data |
|
36 |
#' @keywords internal |
|
37 |
NULL |
|
38 | ||
39 |
#' @rdname module_init_data |
|
40 |
ui_init_data <- function(id) { |
|
41 | 9x |
ns <- shiny::NS(id) |
42 | 9x |
shiny::div( |
43 | 9x |
id = ns("content"), |
44 | 9x |
style = "display: inline-block; width: 100%;", |
45 | 9x |
uiOutput(ns("data")) |
46 |
) |
|
47 |
} |
|
48 | ||
49 |
#' @rdname module_init_data |
|
50 |
srv_init_data <- function(id, data) { |
|
51 | 88x |
checkmate::assert_character(id, max.len = 1, any.missing = FALSE) |
52 | 88x |
checkmate::assert_multi_class(data, c("teal_data", "teal_data_module", "reactive")) |
53 | ||
54 | 88x |
moduleServer(id, function(input, output, session) { |
55 | 88x |
logger::log_debug("srv_data initializing.") |
56 | 88x |
data_out <- if (inherits(data, "teal_data_module")) { |
57 | 10x |
output$data <- renderUI(data$ui(id = session$ns("teal_data_module"))) |
58 | 10x |
data$server("teal_data_module") |
59 | 88x |
} else if (inherits(data, "teal_data")) { |
60 | 48x |
reactiveVal(data) |
61 | 88x |
} else if (test_reactive(data)) { |
62 | 30x |
data |
63 |
} |
|
64 | ||
65 | 87x |
data_handled <- reactive({ |
66 | 80x |
tryCatch(data_out(), error = function(e) e) |
67 |
}) |
|
68 | ||
69 |
# We want to exclude teal_data_module elements from bookmarking as they might have some secrets |
|
70 | 87x |
observeEvent(data_handled(), { |
71 | 80x |
if (inherits(data_handled(), "teal_data")) { |
72 | 75x |
app_session <- .subset2(shiny::getDefaultReactiveDomain(), "parent") |
73 | 75x |
setBookmarkExclude( |
74 | 75x |
session$ns( |
75 | 75x |
grep( |
76 | 75x |
pattern = "teal_data_module-", |
77 | 75x |
x = names(reactiveValuesToList(input)), |
78 | 75x |
value = TRUE |
79 |
) |
|
80 |
), |
|
81 | 75x |
session = app_session |
82 |
) |
|
83 |
} |
|
84 |
}) |
|
85 | ||
86 | 87x |
data_handled |
87 |
}) |
|
88 |
} |
|
89 | ||
90 |
#' Adds signature protection to the `datanames` in the data |
|
91 |
#' @param data (`teal_data`) |
|
92 |
#' @return `teal_data` with additional code that has signature of the `datanames` |
|
93 |
#' @keywords internal |
|
94 |
.add_signature_to_data <- function(data) { |
|
95 | 75x |
hashes <- .get_hashes_code(data) |
96 | 75x |
tdata <- do.call( |
97 | 75x |
teal.data::teal_data, |
98 | 75x |
c( |
99 | 75x |
list(code = trimws(c(teal.code::get_code(data), hashes), which = "right")), |
100 | 75x |
list(join_keys = teal.data::join_keys(data)), |
101 | 75x |
sapply( |
102 | 75x |
names(data), |
103 | 75x |
teal.code::get_var, |
104 | 75x |
object = data, |
105 | 75x |
simplify = FALSE |
106 |
) |
|
107 |
) |
|
108 |
) |
|
109 | ||
110 | 75x |
tdata@verified <- data@verified |
111 | 75x |
tdata |
112 |
} |
|
113 | ||
114 |
#' Get code that tests the integrity of the reproducible data |
|
115 |
#' |
|
116 |
#' @param data (`teal_data`) object holding the data |
|
117 |
#' @param datanames (`character`) names of `datasets` |
|
118 |
#' |
|
119 |
#' @return A character vector with the code lines. |
|
120 |
#' @keywords internal |
|
121 |
#' |
|
122 |
.get_hashes_code <- function(data, datanames = names(data)) { |
|
123 | 75x |
vapply( |
124 | 75x |
datanames, |
125 | 75x |
function(dataname, datasets) { |
126 | 133x |
x <- data[[dataname]] |
127 | ||
128 | 133x |
code <- if (is.function(x) && !is.primitive(x)) { |
129 | 6x |
x <- deparse1(x) |
130 | 6x |
bquote(rlang::hash(deparse1(.(as.name(dataname))))) |
131 |
} else { |
|
132 | 127x |
bquote(rlang::hash(.(as.name(dataname)))) |
133 |
} |
|
134 | 133x |
sprintf( |
135 | 133x |
"stopifnot(%s == %s) # @linksto %s", |
136 | 133x |
deparse1(code), |
137 | 133x |
deparse1(rlang::hash(x)), |
138 | 133x |
dataname |
139 |
) |
|
140 |
}, |
|
141 | 75x |
character(1L), |
142 | 75x |
USE.NAMES = TRUE |
143 |
) |
|
144 |
} |
1 |
#' Check that argument is reactive. |
|
2 |
#' |
|
3 |
#' @inherit checkmate::check_class params return |
|
4 |
#' |
|
5 |
#' @keywords internal |
|
6 |
check_reactive <- function(x, null.ok = FALSE) { # nolint: object_name_linter. |
|
7 | 1134x |
if (!isTRUE(checkmate::test_class(x, classes = "reactive", null.ok = null.ok))) { |
8 | 4x |
cl <- class(x) |
9 | 4x |
return(sprintf( |
10 | 4x |
"Must be a reactive (i.e. inherit from 'reactive' class) but has class%s '%s'", |
11 | 4x |
if (length(cl) > 1L) "es" else "", |
12 | 4x |
paste0(cl, collapse = "','") |
13 |
)) |
|
14 |
} |
|
15 | 1130x |
return(TRUE) |
16 |
} |
|
17 |
#' @rdname check_reactive |
|
18 |
test_reactive <- function(x, null.ok = FALSE) { # nolint: object_name_linter. |
|
19 | 30x |
isTRUE(check_reactive(x, null.ok = null.ok)) |
20 |
} |
|
21 |
#' @rdname check_reactive |
|
22 |
assert_reactive <- checkmate::makeAssertionFunction(check_reactive) |
|
23 | ||
24 |
#' Capture error and decorate error message. |
|
25 |
#' |
|
26 |
#' @param x object to evaluate |
|
27 |
#' @param pre (`character(1)`) A string to prepend to error message |
|
28 |
#' @param post (`character(1)`) A string to append to error message |
|
29 |
#' |
|
30 |
#' @return `x` if no error, otherwise throws error with decorated message |
|
31 |
#' |
|
32 |
#' @keywords internal |
|
33 |
decorate_err_msg <- function(x, pre = character(0), post = character(0)) { |
|
34 | 47x |
tryCatch( |
35 | 47x |
x, |
36 | 47x |
error = function(e) { |
37 | 2x |
stop( |
38 | 2x |
"\n", |
39 | 2x |
pre, |
40 | 2x |
"\n", |
41 | 2x |
e$message, |
42 | 2x |
"\n", |
43 | 2x |
post, |
44 | 2x |
call. = FALSE |
45 |
) |
|
46 |
} |
|
47 |
) |
|
48 | 45x |
x |
49 |
} |
1 |
#' Evaluate expression on `teal_data_module` |
|
2 |
#' |
|
3 |
#' @details |
|
4 |
#' `within` is a convenience function for evaluating inline code inside the environment of a `teal_data_module`. |
|
5 |
#' It accepts only inline expressions (both simple and compound) and allows for injecting values into `expr` through |
|
6 |
#' the `...` argument: as `name:value` pairs are passed to `...`, `name` in `expr` will be replaced with `value.` |
|
7 |
#' |
|
8 |
#' @param data (`teal_data_module`) object |
|
9 |
#' @param expr (`expression`) to evaluate. Must be inline code. See [within()] |
|
10 |
#' @param ... See `Details`. |
|
11 |
#' |
|
12 |
#' @return |
|
13 |
#' `within` returns a `teal_data_module` object with a delayed evaluation of `expr` when the module is run. |
|
14 |
#' |
|
15 |
#' @examples |
|
16 |
#' within(tdm, dataset1 <- subset(dataset1, Species == "virginica")) |
|
17 |
#' |
|
18 |
#' # use additional parameter for expression value substitution. |
|
19 |
#' valid_species <- "versicolor" |
|
20 |
#' within(tdm, dataset1 <- subset(dataset1, Species %in% species), species = valid_species) |
|
21 |
#' @include teal_data_module.R |
|
22 |
#' @name within |
|
23 |
#' @rdname teal_data_module |
|
24 |
#' |
|
25 |
#' @export |
|
26 |
#' |
|
27 |
within.teal_data_module <- function(data, expr, ...) { |
|
28 | 2x |
expr <- substitute(expr) |
29 | 2x |
extras <- list(...) |
30 | ||
31 |
# Add braces for consistency. |
|
32 | 2x |
if (!identical(as.list(expr)[[1L]], as.symbol("{"))) { |
33 | 2x |
expr <- call("{", expr) |
34 |
} |
|
35 | ||
36 | 2x |
calls <- as.list(expr)[-1] |
37 | ||
38 |
# Inject extra values into expressions. |
|
39 | 2x |
calls <- lapply(calls, function(x) do.call(substitute, list(x, env = extras))) |
40 | ||
41 | 2x |
eval_code(object = data, code = as.expression(calls)) |
42 |
} |
1 |
#' `teal_data` utils |
|
2 |
#' |
|
3 |
#' In `teal` we need to recreate the `teal_data` object due to two operations: |
|
4 |
#' - we need to append filter-data code and objects which have been evaluated in `FilteredData` and |
|
5 |
#' we want to avoid double-evaluation. |
|
6 |
#' - we need to subset `teal_data` to `datanames` used by the module, to shorten obtainable R-code |
|
7 |
#' |
|
8 |
#' Due to above recreation of `teal_data` object can't be done simply by using public |
|
9 |
#' `teal.code` and `teal.data` methods. |
|
10 |
#' |
|
11 |
#' @param data (`teal_data`) |
|
12 |
#' @param code (`character`) code to append to the object's code slot. |
|
13 |
#' @param objects (`list`) objects to append to object's environment. |
|
14 |
#' @return modified `teal_data` |
|
15 |
#' @keywords internal |
|
16 |
#' @name teal_data_utilities |
|
17 |
NULL |
|
18 | ||
19 |
#' @rdname teal_data_utilities |
|
20 |
.append_evaluated_code <- function(data, code) { |
|
21 | 89x |
checkmate::assert_class(data, "teal_data") |
22 | 89x |
data@code <- c(data@code, code2list(code)) |
23 | 89x |
methods::validObject(data) |
24 | 89x |
data |
25 |
} |
|
26 | ||
27 |
#' @rdname teal_data_utilities |
|
28 |
.append_modified_data <- function(data, objects) { |
|
29 | 89x |
checkmate::assert_class(data, "teal_data") |
30 | 89x |
checkmate::assert_class(objects, "list") |
31 | 89x |
new_env <- list2env(objects, parent = .GlobalEnv) |
32 | 89x |
rlang::env_coalesce(new_env, as.environment(data)) |
33 | 89x |
data@.xData <- new_env |
34 | 89x |
data |
35 |
} |
1 |
#' Landing popup module |
|
2 |
#' |
|
3 |
#' @description Creates a landing welcome popup for `teal` applications. |
|
4 |
#' |
|
5 |
#' This module is used to display a popup dialog when the application starts. |
|
6 |
#' The dialog blocks access to the application and must be closed with a button before the application can be viewed. |
|
7 |
#' |
|
8 |
#' @param label (`character(1)`) Label of the module. |
|
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. |
|
11 |
#' Passed to `...` of `shiny::modalDialog`. See examples. |
|
12 |
#' @param buttons (`shiny.tag` or `shiny.tag.list`) Typically a `modalButton` or `actionButton`. See examples. |
|
13 |
#' |
|
14 |
#' @return A `teal_module` (extended with `teal_landing_module` class) to be used in `teal` applications. |
|
15 |
#' |
|
16 |
#' @examples |
|
17 |
#' app1 <- init( |
|
18 |
#' data = teal_data(iris = iris), |
|
19 |
#' modules = modules( |
|
20 |
#' example_module() |
|
21 |
#' ), |
|
22 |
#' landing_popup = landing_popup_module( |
|
23 |
#' content = "A place for the welcome message or a disclaimer statement.", |
|
24 |
#' buttons = modalButton("Proceed") |
|
25 |
#' ) |
|
26 |
#' ) |
|
27 |
#' if (interactive()) { |
|
28 |
#' shinyApp(app1$ui, app1$server) |
|
29 |
#' } |
|
30 |
#' |
|
31 |
#' app2 <- init( |
|
32 |
#' data = teal_data(iris = iris), |
|
33 |
#' modules = modules( |
|
34 |
#' example_module() |
|
35 |
#' ), |
|
36 |
#' landing_popup = landing_popup_module( |
|
37 |
#' title = "Welcome", |
|
38 |
#' content = tags$b( |
|
39 |
#' "A place for the welcome message or a disclaimer statement.", |
|
40 |
#' style = "color: red;" |
|
41 |
#' ), |
|
42 |
#' buttons = tagList( |
|
43 |
#' modalButton("Proceed"), |
|
44 |
#' actionButton("read", "Read more", |
|
45 |
#' onclick = "window.open('http://google.com', '_blank')" |
|
46 |
#' ), |
|
47 |
#' actionButton("close", "Reject", onclick = "window.close()") |
|
48 |
#' ) |
|
49 |
#' ) |
|
50 |
#' ) |
|
51 |
#' |
|
52 |
#' if (interactive()) { |
|
53 |
#' shinyApp(app2$ui, app2$server) |
|
54 |
#' } |
|
55 |
#' |
|
56 |
#' @export |
|
57 |
landing_popup_module <- function(label = "Landing Popup", |
|
58 |
title = NULL, |
|
59 |
content = NULL, |
|
60 |
buttons = modalButton("Accept")) { |
|
61 | ! |
checkmate::assert_string(label) |
62 | ! |
checkmate::assert_string(title, null.ok = TRUE) |
63 | ! |
checkmate::assert_multi_class( |
64 | ! |
content, |
65 | ! |
classes = c("character", "shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE |
66 |
) |
|
67 | ! |
checkmate::assert_multi_class(buttons, classes = c("shiny.tag", "shiny.tag.list")) |
68 | ||
69 | ! |
message("Initializing landing_popup_module") |
70 | ||
71 | ! |
module <- module( |
72 | ! |
label = label, |
73 | ! |
server = function(id) { |
74 | ! |
moduleServer(id, function(input, output, session) { |
75 | ! |
showModal( |
76 | ! |
modalDialog( |
77 | ! |
id = "landingpopup", |
78 | ! |
title = title, |
79 | ! |
content, |
80 | ! |
footer = buttons |
81 |
) |
|
82 |
) |
|
83 |
}) |
|
84 |
} |
|
85 |
) |
|
86 | ! |
class(module) <- c("teal_module_landing", class(module)) |
87 | ! |
module |
88 |
} |
1 |
#' @title `TealReportCard` |
|
2 |
#' @description `r lifecycle::badge("experimental")` |
|
3 |
#' Child class of [`teal.reporter::ReportCard`] that is used for `teal` specific applications. |
|
4 |
#' 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 |
#' meta data. |
|
7 |
#' @export |
|
8 |
#' |
|
9 |
TealReportCard <- R6::R6Class( # nolint: object_name. |
|
10 |
classname = "TealReportCard", |
|
11 |
inherit = teal.reporter::ReportCard, |
|
12 |
public = list( |
|
13 |
#' @description Appends the source code to the `content` meta data of this `TealReportCard`. |
|
14 |
#' |
|
15 |
#' @param src (`character(1)`) code as text. |
|
16 |
#' @param ... any `rmarkdown` `R` chunk parameter and its value. |
|
17 |
#' But `eval` parameter is always set to `FALSE`. |
|
18 |
#' @return Object of class `TealReportCard`, invisibly. |
|
19 |
#' @examples |
|
20 |
#' card <- TealReportCard$new()$append_src( |
|
21 |
#' "plot(iris)" |
|
22 |
#' ) |
|
23 |
#' card$get_content()[[1]]$get_content() |
|
24 |
append_src = function(src, ...) { |
|
25 | 4x |
checkmate::assert_character(src, min.len = 0, max.len = 1) |
26 | 4x |
params <- list(...) |
27 | 4x |
params$eval <- FALSE |
28 | 4x |
rblock <- RcodeBlock$new(src) |
29 | 4x |
rblock$set_params(params) |
30 | 4x |
self$append_content(rblock) |
31 | 4x |
self$append_metadata("SRC", src) |
32 | 4x |
invisible(self) |
33 |
}, |
|
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 |
|
36 |
#' the default `yaml::as.yaml` to format the list. |
|
37 |
#' If the filter state list is empty, nothing is appended to the `content`. |
|
38 |
#' |
|
39 |
#' @param fs (`teal_slices`) object returned from [teal_slices()] function. |
|
40 |
#' @return `self`, invisibly. |
|
41 |
append_fs = function(fs) { |
|
42 | 5x |
checkmate::assert_class(fs, "teal_slices") |
43 | 4x |
self$append_text("Filter State", "header3") |
44 | 4x |
if (length(fs)) { |
45 | 3x |
self$append_content(TealSlicesBlock$new(fs)) |
46 |
} else { |
|
47 | 1x |
self$append_text("No filters specified.") |
48 |
} |
|
49 | 4x |
invisible(self) |
50 |
}, |
|
51 |
#' @description Appends the encodings list to the `content` and `metadata` of this `TealReportCard`. |
|
52 |
#' |
|
53 |
#' @param encodings (`list`) list of encodings selections of the `teal` app. |
|
54 |
#' @return `self`, invisibly. |
|
55 |
#' @examples |
|
56 |
#' card <- TealReportCard$new()$append_encodings(list(variable1 = "X")) |
|
57 |
#' card$get_content()[[1]]$get_content() |
|
58 |
#' |
|
59 |
append_encodings = function(encodings) { |
|
60 | 4x |
checkmate::assert_list(encodings) |
61 | 4x |
self$append_text("Selected Options", "header3") |
62 | 4x |
if (requireNamespace("yaml", quietly = TRUE)) { |
63 | 4x |
self$append_text(yaml::as.yaml(encodings, handlers = list( |
64 | 4x |
POSIXct = function(x) format(x, "%Y-%m-%d"), |
65 | 4x |
POSIXlt = function(x) format(x, "%Y-%m-%d"), |
66 | 4x |
Date = function(x) format(x, "%Y-%m-%d") |
67 | 4x |
)), "verbatim") |
68 |
} else { |
|
69 | ! |
stop("yaml package is required to format the encodings list") |
70 |
} |
|
71 | 4x |
self$append_metadata("Encodings", encodings) |
72 | 4x |
invisible(self) |
73 |
} |
|
74 |
), |
|
75 |
private = list( |
|
76 |
dispatch_block = function(block_class) { |
|
77 | ! |
eval(str2lang(block_class)) |
78 |
} |
|
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", |
|
89 |
inherit = teal.reporter:::TextBlock, |
|
90 |
public = list( |
|
91 |
#' @description Returns a `TealSlicesBlock` object. |
|
92 |
#' |
|
93 |
#' @details Returns a `TealSlicesBlock` object with no content and no parameters. |
|
94 |
#' |
|
95 |
#' @param content (`teal_slices`) object returned from [teal_slices()] function. |
|
96 |
#' @param style (`character(1)`) string specifying style to apply. |
|
97 |
#' |
|
98 |
#' @return Object of class `TealSlicesBlock`, invisibly. |
|
99 |
#' |
|
100 |
initialize = function(content = teal_slices(), style = "verbatim") { |
|
101 | 9x |
self$set_content(content) |
102 | 8x |
self$set_style(style) |
103 | 8x |
invisible(self) |
104 |
}, |
|
105 | ||
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. |
|
110 |
#' When `selected` field in `teal_slice` object is a range, then it is displayed as a "min" |
|
111 |
#' |
|
112 |
#' |
|
113 |
#' @param content (`teal_slices`) object returned from [teal_slices()] function. |
|
114 |
#' @return `self`, invisibly. |
|
115 |
set_content = function(content) { |
|
116 | 9x |
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 |
if ( |
121 | 6x |
inherits(x_list$choices, c("integer", "numeric", "Date", "POSIXct", "POSIXlt")) && |
122 | 6x |
length(x_list$choices) == 2 && |
123 | 6x |
length(x_list$selected) == 2 |
124 |
) { |
|
125 | ! |
x_list$range <- paste(x_list$selected, collapse = " - ") |
126 | ! |
x_list["selected"] <- NULL |
127 |
} |
|
128 | 6x |
if (!is.null(x_list$arg)) { |
129 | ! |
x_list$arg <- if (x_list$arg == "subset") "Genes" else "Samples" |
130 |
} |
|
131 | ||
132 | 6x |
x_list <- x_list[ |
133 | 6x |
c("dataname", "varname", "experiment", "arg", "expr", "selected", "range", "keep_na", "keep_inf") |
134 |
] |
|
135 | 6x |
names(x_list) <- c( |
136 | 6x |
"Dataset name", "Variable name", "Experiment", "Filtering by", "Applied expression", |
137 | 6x |
"Selected Values", "Selected range", "Include NA values", "Include Inf values" |
138 |
) |
|
139 | ||
140 | 6x |
Filter(Negate(is.null), x_list) |
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 |
} |
|
148 |
} |
|
149 | 8x |
private$teal_slices <- content |
150 | 8x |
invisible(self) |
151 |
}, |
|
152 |
#' @description Create the `TealSlicesBlock` from a list. |
|
153 |
#' |
|
154 |
#' @param x (`named list`) with two fields `text` and `style`. |
|
155 |
#' Use the `get_available_styles` method to get all possible styles. |
|
156 |
#' |
|
157 |
#' @return `self`, invisibly. |
|
158 |
#' @examples |
|
159 |
#' TealSlicesBlock <- getFromNamespace("TealSlicesBlock", "teal") |
|
160 |
#' block <- TealSlicesBlock$new() |
|
161 |
#' block$from_list(list(text = "sth", style = "default")) |
|
162 |
#' |
|
163 |
from_list = function(x) { |
|
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) |
167 | 1x |
super$set_style(x$style) |
168 | 1x |
invisible(self) |
169 |
}, |
|
170 |
#' @description Convert the `TealSlicesBlock` to a list. |
|
171 |
#' |
|
172 |
#' @return `named list` with a text and style. |
|
173 |
#' @examples |
|
174 |
#' TealSlicesBlock <- getFromNamespace("TealSlicesBlock", "teal") |
|
175 |
#' block <- TealSlicesBlock$new() |
|
176 |
#' block$to_list() |
|
177 |
#' |
|
178 |
to_list = function() { |
|
179 | 2x |
content <- self$get_content() |
180 | 2x |
list( |
181 | 2x |
text = if (length(content)) content else "", |
182 | 2x |
style = self$get_style() |
183 |
) |
|
184 |
} |
|
185 |
), |
|
186 |
private = list( |
|
187 |
style = "verbatim", |
|
188 |
teal_slices = NULL # teal_slices |
|
189 |
) |
|
190 |
) |
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 |
} |
1 |
#' Store and restore `teal_slices` object |
|
2 |
#' |
|
3 |
#' Functions that write a `teal_slices` object to a file in the `JSON` format, |
|
4 |
#' and also restore the object from disk. |
|
5 |
#' |
|
6 |
#' Date and date time objects are stored in the following formats: |
|
7 |
#' |
|
8 |
#' - `Date` class is converted to the `"ISO8601"` standard (`YYYY-MM-DD`). |
|
9 |
#' - `POSIX*t` classes are converted to character by using |
|
10 |
#' `format.POSIX*t(usetz = TRUE, tz = "UTC")` (`YYYY-MM-DD HH:MM:SS UTC`, where |
|
11 |
#' `UTC` is the `Coordinated Universal Time` timezone short-code). |
|
12 |
#' |
|
13 |
#' This format is assumed during `slices_restore`. All `POSIX*t` objects in |
|
14 |
#' `selected` or `choices` fields of `teal_slice` objects are always printed in |
|
15 |
#' `UTC` timezone as well. |
|
16 |
#' |
|
17 |
#' @param tss (`teal_slices`) object to be stored. |
|
18 |
#' @param file (`character(1)`) file path where `teal_slices` object will be |
|
19 |
#' saved and restored. The file extension should be `".json"`. |
|
20 |
#' |
|
21 |
#' @return `slices_store` returns `NULL`, invisibly. |
|
22 |
#' |
|
23 |
#' @seealso [teal_slices()] |
|
24 |
#' |
|
25 |
#' @keywords internal |
|
26 |
#' |
|
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") |
30 | ||
31 | 9x |
cat(format(tss, trim_lines = FALSE), "\n", file = file) |
32 |
} |
|
33 | ||
34 |
#' @rdname slices_store |
|
35 |
#' @return `slices_restore` returns a `teal_slices` object restored from the file. |
|
36 |
#' @keywords internal |
|
37 |
slices_restore <- function(file) { |
|
38 | 9x |
checkmate::assert_file_exists(file, access = "r", extension = "json") |
39 | ||
40 | 9x |
tss_json <- jsonlite::fromJSON(file, simplifyDataFrame = FALSE) |
41 | 9x |
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$") |
48 | ||
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") |
54 |
} else { |
|
55 | 3x |
slice[[field]] |
56 |
} |
|
57 |
} else { |
|
58 | 3x |
slice[[field]] <- character(0) |
59 |
} |
|
60 |
} |
|
61 |
} |
|
62 | 9x |
slice |
63 |
}) |
|
64 | ||
65 | 9x |
tss_elements <- lapply(tss_json$slices, as.teal_slice) |
66 | ||
67 | 9x |
do.call(teal_slices, c(tss_elements, tss_json$attributes)) |
68 |
} |
1 |
#' Data module for `teal` applications |
|
2 |
#' |
|
3 |
#' @description |
|
4 |
#' `r lifecycle::badge("experimental")` |
|
5 |
#' |
|
6 |
#' Create a `teal_data_module` object and evaluate code on it with history tracking. |
|
7 |
#' |
|
8 |
#' @details |
|
9 |
#' `teal_data_module` creates a `shiny` module to interactively supply or modify data in a `teal` application. |
|
10 |
#' The module allows for running any code (creation _and_ some modification) after the app starts or reloads. |
|
11 |
#' The body of the server function will be run in the app rather than in the global environment. |
|
12 |
#' This means it will be run every time the app starts, so use sparingly. |
|
13 |
#' |
|
14 |
#' 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 |
#' |
|
17 |
#' See vignette `vignette("data-as-shiny-module", package = "teal")` for more details. |
|
18 |
#' |
|
19 |
#' @param ui (`function(id)`) |
|
20 |
#' `shiny` module UI function; must only take `id` argument |
|
21 |
#' @param server (`function(id)`) |
|
22 |
#' `shiny` module server function; must only take `id` argument; |
|
23 |
#' must return reactive expression containing `teal_data` object |
|
24 |
#' @param label (`character(1)`) Label of the module. |
|
25 |
#' @param once (`logical(1)`) |
|
26 |
#' If `TRUE`, the data module will be shown only once and will disappear after successful data loading. |
|
27 |
#' App user will no longer be able to interact with this module anymore. |
|
28 |
#' If `FALSE`, the data module can be reused multiple times. |
|
29 |
#' App user will be able to interact and change the data output from the module multiple times. |
|
30 |
#' |
|
31 |
#' @return |
|
32 |
#' `teal_data_module` returns a list of class `teal_data_module` containing two elements, `ui` and |
|
33 |
#' `server` provided via arguments. |
|
34 |
#' |
|
35 |
#' @examples |
|
36 |
#' tdm <- teal_data_module( |
|
37 |
#' ui = function(id) { |
|
38 |
#' ns <- NS(id) |
|
39 |
#' actionButton(ns("submit"), label = "Load data") |
|
40 |
#' }, |
|
41 |
#' server = function(id) { |
|
42 |
#' moduleServer(id, function(input, output, session) { |
|
43 |
#' eventReactive(input$submit, { |
|
44 |
#' data <- within( |
|
45 |
#' teal_data(), |
|
46 |
#' { |
|
47 |
#' dataset1 <- iris |
|
48 |
#' dataset2 <- mtcars |
|
49 |
#' } |
|
50 |
#' ) |
|
51 |
#' |
|
52 |
#' data |
|
53 |
#' }) |
|
54 |
#' }) |
|
55 |
#' } |
|
56 |
#' ) |
|
57 |
#' |
|
58 |
#' @name teal_data_module |
|
59 |
#' @seealso [`teal.data::teal_data-class`], [teal.code::qenv()] |
|
60 |
#' |
|
61 |
#' @export |
|
62 |
teal_data_module <- function(ui, server, label = "data module", once = TRUE) { |
|
63 | 33x |
checkmate::assert_function(ui, args = "id", nargs = 1) |
64 | 32x |
checkmate::assert_function(server, args = "id", nargs = 1) |
65 | 30x |
checkmate::assert_string(label) |
66 | 30x |
checkmate::assert_flag(once) |
67 | 30x |
structure( |
68 | 30x |
list( |
69 | 30x |
ui = ui, |
70 | 30x |
server = function(id) { |
71 | 23x |
data_out <- server(id) |
72 | 22x |
decorate_err_msg( |
73 | 22x |
assert_reactive(data_out), |
74 | 22x |
pre = sprintf("From: 'teal_data_module()':\nA 'teal_data_module' with \"%s\" label:", label), |
75 | 22x |
post = "Please make sure that this module returns a 'reactive` object containing 'teal_data' class of object." # nolint: line_length_linter. |
76 |
) |
|
77 |
} |
|
78 |
), |
|
79 | 30x |
label = label, |
80 | 30x |
class = "teal_data_module", |
81 | 30x |
once = once |
82 |
) |
|
83 |
} |
1 |
setOldClass("teal_data_module") |
|
2 | ||
3 |
#' Evaluate code on `teal_data_module` |
|
4 |
#' |
|
5 |
#' @details |
|
6 |
#' `eval_code` evaluates given code in the environment of the `teal_data` object created by the `teal_data_module`. |
|
7 |
#' The code is added to the `@code` slot of the `teal_data`. |
|
8 |
#' |
|
9 |
#' @param object (`teal_data_module`) |
|
10 |
#' @inheritParams teal.code::eval_code |
|
11 |
#' |
|
12 |
#' @return |
|
13 |
#' `eval_code` returns a `teal_data_module` object with a delayed evaluation of `code` when the module is run. |
|
14 |
#' |
|
15 |
#' @examples |
|
16 |
#' eval_code(tdm, "dataset1 <- subset(dataset1, Species == 'virginica')") |
|
17 |
#' |
|
18 |
#' @include teal_data_module.R |
|
19 |
#' @name eval_code |
|
20 |
#' @rdname teal_data_module |
|
21 |
#' @aliases eval_code,teal_data_module,character-method |
|
22 |
#' @aliases eval_code,teal_data_module,language-method |
|
23 |
#' @aliases eval_code,teal_data_module,expression-method |
|
24 |
#' |
|
25 |
#' @importFrom methods setMethod |
|
26 |
#' @importMethodsFrom teal.code eval_code |
|
27 |
#' |
|
28 |
setMethod("eval_code", signature = c("teal_data_module", "character"), function(object, code) { |
|
29 | 9x |
teal_data_module( |
30 | 9x |
ui = function(id) { |
31 | 1x |
ns <- NS(id) |
32 | 1x |
object$ui(ns("mutate_inner")) |
33 |
}, |
|
34 | 9x |
server = function(id) { |
35 | 7x |
moduleServer(id, function(input, output, session) { |
36 | 7x |
data <- object$server("mutate_inner") |
37 | 6x |
td <- eventReactive(data(), |
38 |
{ |
|
39 | 6x |
if (inherits(data(), c("teal_data", "qenv.error"))) { |
40 | 4x |
eval_code(data(), code) |
41 |
} else { |
|
42 | 2x |
data() |
43 |
} |
|
44 |
}, |
|
45 | 6x |
ignoreNULL = FALSE |
46 |
) |
|
47 | 6x |
td |
48 |
}) |
|
49 |
} |
|
50 |
) |
|
51 |
}) |
|
52 | ||
53 |
setMethod("eval_code", signature = c("teal_data_module", "language"), function(object, code) { |
|
54 | 1x |
eval_code(object, code = paste(lang2calls(code), collapse = "\n")) |
55 |
}) |
|
56 | ||
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 |
#' Create a `tdata` object |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("superseded")` |
|
4 |
#' |
|
5 |
#' Recent changes in `teal` cause modules to fail because modules expect a `tdata` object |
|
6 |
#' to be passed to the `data` argument but instead they receive a `teal_data` object, |
|
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 |
new_tdata <- function(...) { |
|
19 | ! |
.deprecate_tdata_msg() |
20 |
} |
|
21 | ||
22 |
#' @rdname tdata |
|
23 |
#' @export |
|
24 |
tdata2env <- function(...) { |
|
25 | ! |
.deprecate_tdata_msg() |
26 |
} |
|
27 | ||
28 |
#' @rdname tdata |
|
29 |
#' @export |
|
30 |
get_code_tdata <- function(...) { |
|
31 | ! |
.deprecate_tdata_msg() |
32 |
} |
|
33 | ||
34 |
#' @rdname tdata |
|
35 |
#' @export |
|
36 |
join_keys.tdata <- function(...) { |
|
37 | ! |
.deprecate_tdata_msg() |
38 |
} |
|
39 | ||
40 |
#' @rdname tdata |
|
41 |
#' @export |
|
42 |
get_metadata <- function(...) { |
|
43 | ! |
.deprecate_tdata_msg() |
44 |
} |
|
45 | ||
46 |
#' @rdname tdata |
|
47 |
#' @export |
|
48 |
as_tdata <- function(...) { |
|
49 | ! |
.deprecate_tdata_msg() |
50 |
} |
|
51 | ||
52 | ||
53 |
.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 |
) |
|
61 |
) |
|
62 |
} |
1 |
#' Generates library calls from current session info |
|
2 |
#' |
|
3 |
#' Function to create multiple library calls out of current session info to ensure reproducible code works. |
|
4 |
#' |
|
5 |
#' @return Character vector of `library(<package>)` calls. |
|
6 |
#' @keywords internal |
|
7 |
get_rcode_libraries <- function() { |
|
8 | 1x |
libraries <- vapply( |
9 | 1x |
utils::sessionInfo()$otherPkgs, |
10 | 1x |
function(x) { |
11 | 15x |
paste0("library(", x$Package, ")") |
12 |
}, |
|
13 | 1x |
character(1) |
14 |
) |
|
15 | 1x |
paste0(paste0(rev(libraries), sep = "\n"), collapse = "") |
16 |
} |
|
17 | ||
18 | ||
19 |
#' @noRd |
|
20 |
#' @keywords internal |
|
21 |
get_rcode_str_install <- function() { |
|
22 | 5x |
code_string <- getOption("teal.load_nest_code") |
23 | 5x |
if (is.character(code_string)) { |
24 | 2x |
code_string |
25 |
} else { |
|
26 | 3x |
"# Add any code to install/load your NEST environment here\n" |
27 |
} |
|
28 |
} |
1 |
#' UI and server modules of `teal` |
|
2 |
#' |
|
3 |
#' @description `r lifecycle::badge("deprecated")` |
|
4 |
#' Please use [`module_teal`] instead. |
|
5 |
#' |
|
6 |
#' @inheritParams ui_teal |
|
7 |
#' @inheritParams srv_teal |
|
8 |
#' |
|
9 |
#' @return |
|
10 |
#' Returns a `reactive` expression containing a `teal_data` object when data is loaded or `NULL` when it is not. |
|
11 |
#' @name module_teal_with_splash |
|
12 |
#' |
|
13 |
NULL |
|
14 | ||
15 |
#' @export |
|
16 |
#' @rdname module_teal_with_splash |
|
17 |
ui_teal_with_splash <- function(id, |
|
18 |
data, |
|
19 |
title = build_app_title(), |
|
20 |
header = tags$p(), |
|
21 |
footer = tags$p()) { |
|
22 | ! |
lifecycle::deprecate_soft( |
23 | ! |
when = "0.16", |
24 | ! |
what = "ui_teal_with_splash()", |
25 | ! |
details = "Deprecated, please use `ui_teal` instead" |
26 |
) |
|
27 | ! |
ui_teal(id = id, title = title, header = header, footer = footer) |
28 |
} |
|
29 | ||
30 |
#' @export |
|
31 |
#' @rdname module_teal_with_splash |
|
32 |
srv_teal_with_splash <- function(id, data, modules, filter = teal_slices()) { |
|
33 | ! |
lifecycle::deprecate_soft( |
34 | ! |
when = "0.16", |
35 | ! |
what = "srv_teal_with_splash()", |
36 | ! |
details = "Deprecated, please use `srv_teal` instead" |
37 |
) |
|
38 | ! |
srv_teal(id = id, data = data, modules = modules, filter = filter) |
39 |
} |