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