diff --git a/main/coverage-report/index.html b/main/coverage-report/index.html index 80ec1b1039..48e310b0c1 100644 --- a/main/coverage-report/index.html +++ b/main/coverage-report/index.html @@ -94,7 +94,7 @@ font-size: 11px; }
1 |
- # This file adds a splash screen for delayed data loading on top of teal+ #' Add right filter panel into each of the top-level `teal_modules` UIs. |
|||
2 |
-
+ #' |
|||
3 |
- #' UI to show a splash screen in the beginning, then delegate to [srv_teal()]+ #' The [ui_nested_tabs] function returns a nested tabbed UI corresponding |
|||
4 |
- #'+ #' to the nested modules. |
|||
5 |
- #' @description `r lifecycle::badge("stable")`+ #' This function adds the right filter panel to each main tab. |
|||
6 |
- #' The splash screen could be used to query for a password to fetch the data.+ #' |
|||
7 |
- #' [init()] is a very thin wrapper around this module useful for end-users which+ #' The right filter panel's filter choices affect the `datasets` object. Therefore, |
|||
8 |
- #' assumes that it is a top-level module and cannot be embedded.+ #' all modules using the same `datasets` share the same filters. |
|||
9 |
- #' This function instead adheres to the Shiny module conventions.+ #' |
|||
10 |
- #'+ #' This works with nested modules of depth greater than 2, though the filter |
|||
11 |
- #' If data is obtained through delayed loading, its splash screen is used. Otherwise,+ #' panel is inserted at the right of the modules at depth 1 and not at the leaves. |
|||
12 |
- #' a default splash screen is shown.+ #' |
|||
13 |
- #'+ #' @name module_tabs_with_filters |
|||
14 |
- #' Please also refer to the doc of [init()].+ #' |
|||
15 |
- #'+ #' @inheritParams module_teal |
|||
16 |
- #' @param id (`character(1)`)\cr+ #' |
|||
17 |
- #' module id+ #' @param datasets (`named list` of `FilteredData`)\cr |
|||
18 |
- #' @inheritParams init+ #' object to store filter state and filtered datasets, shared across modules. For more |
|||
19 |
- #' @export+ #' details see [`teal.slice::FilteredData`]. Structure of the list must be the same as structure |
|||
20 |
- ui_teal_with_splash <- function(id,+ #' of the `modules` argument and list names must correspond to the labels in `modules`. |
|||
21 |
- data,+ #' When filter is not module-specific then list contains the same object in all elements. |
|||
22 |
- title,+ #' @param reporter (`Reporter`) object from `teal.reporter` |
|||
23 |
- header = tags$p("Add Title Here"),+ #' |
|||
24 |
- footer = tags$p("Add Footer Here")) {+ #' @return A `tagList` of The main menu, place holders for filters and |
|||
25 | -12x | +
- checkmate::assert_multi_class(data, c("teal_data", "teal_data_module"))+ #' place holders for the teal modules |
||
26 | -12x | +
- ns <- NS(id)+ #' |
||
27 |
-
+ #' |
|||
28 |
- # Startup splash screen for delayed loading+ #' @keywords internal |
|||
29 |
- # We use delayed loading in all cases, even when the data does not need to be fetched.+ #' |
|||
30 |
- # This has the benefit that when filtering the data takes a lot of time initially, the+ #' @examples |
|||
31 |
- # Shiny app does not time out.+ #' |
|||
32 | -12x | +
- splash_ui <- if (inherits(data, "teal_data_module")) {+ #' mods <- teal:::example_modules() |
||
33 | -1x | +
- data$ui(ns("teal_data_module"))+ #' datasets <- teal:::example_datasets() |
||
34 | -12x | +
- } else if (inherits(data, "teal_data")) {+ #' |
||
35 | -11x | +
- div()+ #' app <- shinyApp( |
||
36 |
- }+ #' ui = function() { |
|||
37 | -12x | +
- ui_teal(+ #' tagList( |
||
38 | -12x | +
- id = ns("teal"),+ #' teal:::include_teal_css_js(), |
||
39 | -12x | +
- splash_ui = div(splash_ui, uiOutput(ns("error"))),+ #' textOutput("info"), |
||
40 | -12x | +
- title = title,+ #' fluidPage( # needed for nice tabs |
||
41 | -12x | +
- header = header,+ #' ui_tabs_with_filters("dummy", modules = mods, datasets = datasets) |
||
42 | -12x | +
- footer = footer+ #' ) |
||
43 |
- )+ #' ) |
|||
44 |
- }+ #' }, |
|||
45 |
-
+ #' server = function(input, output, session) { |
|||
46 |
- #' Server function that loads the data through reactive loading and then delegates+ #' output$info <- renderText({ |
|||
47 |
- #' to [srv_teal()].+ #' paste0("The currently active tab name is ", active_module()$label) |
|||
48 |
- #'+ #' }) |
|||
49 |
- #' @description `r lifecycle::badge("stable")`+ #' active_module <- srv_tabs_with_filters(id = "dummy", datasets = datasets, modules = mods) |
|||
50 |
- #' Please also refer to the doc of [init()].+ #' } |
|||
51 |
- #'+ #' ) |
|||
52 |
- #' @inheritParams init+ #' if (interactive()) { |
|||
53 |
- #' @param modules `teal_modules` object containing the output modules which+ #' shinyApp(app$ui, app$server) |
|||
54 |
- #' will be displayed in the teal application. See [modules()] and [module()] for+ #' } |
|||
55 |
- #' more details.+ #' |
|||
56 |
- #' @inheritParams shiny::moduleServer+ NULL |
|||
57 |
- #' @return `reactive` containing `teal_data` object when data is loaded.+ |
|||
58 |
- #' If data is not loaded yet, `reactive` returns `NULL`.+ #' @rdname module_tabs_with_filters |
|||
59 |
- #' @export+ ui_tabs_with_filters <- function(id, modules, datasets, filter = teal_slices()) { |
|||
60 | -+ | ! |
- srv_teal_with_splash <- function(id, data, modules, filter = teal_slices()) {+ checkmate::assert_class(modules, "teal_modules") |
|
61 | -15x | +! |
- checkmate::check_multi_class(data, c("teal_data", "teal_data_module"))+ checkmate::assert_list(datasets, types = c("list", "FilteredData")) |
|
62 | -+ | ! |
-
+ checkmate::assert_class(filter, "teal_slices") |
|
63 | -15x | +
- moduleServer(id, function(input, output, session) {+ |
||
64 | -15x | +! |
- logger::log_trace("srv_teal_with_splash initializing module with data.")+ ns <- NS(id) |
|
65 | -+ | ! |
-
+ is_module_specific <- isTRUE(attr(filter, "module_specific")) |
|
66 | -15x | +
- if (getOption("teal.show_js_log", default = FALSE)) {+ |
||
67 | ! |
- shinyjs::showLog()+ teal_ui <- ui_nested_tabs(ns("root"), modules = modules, datasets, is_module_specific = is_module_specific) |
||
68 | -+ | ! |
- }+ filter_panel_btns <- tags$li( |
|
69 | -+ | ! |
-
+ class = "flex-grow", |
|
70 | -+ | ! |
- # teal_data_rv contains teal_data object+ tags$button( |
|
71 | -+ | ! |
- # either passed to teal::init or returned from teal_data_module+ class = "btn action-button filter_hamburger", # see sidebar.css for style filter_hamburger |
|
72 | -15x | +! |
- teal_data_rv <- if (inherits(data, "teal_data_module")) {+ href = "javascript:void(0)", |
|
73 | -10x | +! |
- data <- data$server(id = "teal_data_module")+ onclick = "toggleFilterPanel();", # see sidebar.js |
|
74 | -10x | +! |
- if (!is.reactive(data)) {+ title = "Toggle filter panels", |
|
75 | -1x | +! |
- stop("The `teal_data_module` must return a reactive expression.", call. = FALSE)+ icon("fas fa-bars") |
|
76 |
- }+ ), |
|||
77 | -9x | +! |
- data+ filter_manager_modal_ui(ns("filter_manager")) |
|
78 | -15x | +
- } else if (inherits(data, "teal_data")) {+ ) |
||
79 | -5x | +! |
- reactiveVal(data)+ teal_ui$children[[1]] <- tagAppendChild(teal_ui$children[[1]], filter_panel_btns) |
|
80 |
- }+ |
|||
81 | -+ | ! |
-
+ if (!is_module_specific) { |
|
82 | -14x | +
- teal_data_rv_validate <- reactive({+ # need to rearrange html so that filter panel is within tabset |
||
83 | -+ | ! |
- # custom module can return error+ tabset_bar <- teal_ui$children[[1]] |
|
84 | -11x | +! |
- data <- tryCatch(teal_data_rv(), error = function(e) e)+ teal_modules <- teal_ui$children[[2]] |
|
85 | -+ | ! |
-
+ filter_ui <- unlist(datasets)[[1]]$ui_filter_panel(ns("filter_panel")) |
|
86 | -+ | ! |
- # there is an empty reactive cycle on init!+ list( |
|
87 | -11x | +! |
- if (inherits(data, "shiny.silent.error") && identical(data$message, "")) {+ tabset_bar, |
|
88 | ! |
- return(NULL)+ tags$hr(class = "my-2"), |
||
89 | -+ | ! |
- }+ fluidRow( |
|
90 | -+ | ! |
-
+ column(width = 9, teal_modules, class = "teal_primary_col"), |
|
91 | -+ | ! |
- # to handle qenv.error+ column(width = 3, filter_ui, class = "teal_secondary_col") |
|
92 | -11x | +
- if (inherits(data, "qenv.error")) {+ ) |
||
93 | -2x | +
- validate(+ ) |
||
94 | -2x | +
- need(+ } else { |
||
95 | -2x | +! |
- FALSE,+ teal_ui |
|
96 | -2x | +
- paste(+ } |
||
97 | -2x | +
- "Error when executing `teal_data_module`:\n ",+ } |
||
98 | -2x | +
- paste(data$message, collapse = "\n"),+ |
||
99 | -2x | +
- "\n Check your inputs or contact app developer if error persists."+ #' @rdname module_tabs_with_filters |
||
100 |
- )+ srv_tabs_with_filters <- function(id, |
|||
101 |
- )+ datasets, |
|||
102 |
- )+ modules, |
|||
103 |
- }+ reporter = teal.reporter::Reporter$new(), |
|||
104 |
-
+ filter = teal_slices()) { |
|||
105 | -+ | 5x |
- # to handle module non-qenv errors+ checkmate::assert_class(modules, "teal_modules") |
|
106 | -9x | +5x |
- if (inherits(data, "error")) {+ checkmate::assert_list(datasets, types = c("list", "FilteredData")) |
|
107 | -1x | +5x |
- validate(+ checkmate::assert_class(reporter, "Reporter") |
|
108 | -1x | +3x |
- need(+ checkmate::assert_class(filter, "teal_slices") |
|
109 | -1x | +
- FALSE,+ |
||
110 | -1x | +3x |
- paste(+ moduleServer(id, function(input, output, session) { |
|
111 | -1x | +3x |
- "Error when executing `teal_data_module`:\n ",+ logger::log_trace("srv_tabs_with_filters initializing the module.") |
|
112 | -1x | +
- paste(data$message, collpase = "\n"),+ |
||
113 | -1x | +3x |
- "\n Check your inputs or contact app developer if error persists."+ is_module_specific <- isTRUE(attr(filter, "module_specific")) |
|
114 | -+ | 3x |
- )+ manager_out <- filter_manager_modal_srv("filter_manager", filtered_data_list = datasets, filter = filter) |
|
115 |
- )+ |
|||
116 | -+ | 3x |
- )+ active_module <- srv_nested_tabs( |
|
117 | -+ | 3x |
- }+ id = "root", |
|
118 | -+ | 3x |
-
+ datasets = datasets, |
|
119 | -8x | +3x |
- validate(+ modules = modules, |
|
120 | -8x | +3x |
- need(+ reporter = reporter, |
|
121 | -8x | +3x |
- inherits(data, "teal_data"),+ is_module_specific = is_module_specific |
|
122 | -8x | +
- paste(+ ) |
||
123 | -8x | +
- "Error: `teal_data_module` did not return `teal_data` object",+ |
||
124 | -8x | +3x |
- "\n Check your inputs or contact app developer if error persists"+ if (!is_module_specific) { |
|
125 | -+ | 3x |
- )+ active_datanames <- reactive({ |
|
126 | -+ | 6x |
- )+ if (identical(active_module()$datanames, "all")) { |
|
127 | -+ | ! |
- )+ singleton$datanames() |
|
128 |
-
+ } else { |
|||
129 | 5x |
- if (!length(teal.data::datanames(data))) {+ active_module()$datanames |
||
130 | -1x | +
- warning("`data` object has no datanames. Default datanames are set using `teal_data`'s environment.")+ } |
||
131 |
- }+ }) |
|||
132 | -+ | 3x |
-
+ singleton <- unlist(datasets)[[1]] |
|
133 | -5x | +3x |
- is_modules_ok <- check_modules_datanames(modules, teal_data_datanames(data))+ singleton$srv_filter_panel("filter_panel", active_datanames = active_datanames) |
|
134 | -5x | +
- if (!isTRUE(is_modules_ok)) {+ |
||
135 | -1x | +3x |
- logger::log_warn(is_modules_ok)+ observeEvent( |
|
136 | -1x | +3x |
- validate(need(isTRUE(is_modules_ok), sprintf("%s. Contact app developer.", is_modules_ok)))+ eventExpr = active_datanames(), |
|
137 | -+ | 3x |
- }+ handlerExpr = { |
|
138 | -+ | 4x |
-
+ script <- if (length(active_datanames()) == 0 || is.null(active_datanames())) { |
|
139 | -4x | +
- is_filter_ok <- check_filter_datanames(filter, teal_data_datanames(data))+ # hide the filter panel and disable the burger button |
||
140 | -4x | +! |
- if (!isTRUE(is_filter_ok)) {+ "handleNoActiveDatasets();" |
|
141 | -1x | -
- showNotification(- |
- ||
142 | -1x | -
- "Some filters were not applied because of incompatibility with data. Contact app developer.",- |
- ||
143 | -1x | -
- type = "warning",- |
- ||
144 | -1x | +
- duration = 10+ } else { |
||
145 | +142 |
- )+ # show the filter panel and enable the burger button |
||
146 | -1x | -
- logger::log_warn(is_filter_ok)- |
- ||
147 | -+ | 143 | +4x |
- }+ "handleActiveDatasetsPresent();" |
148 | +144 |
-
+ } |
||
149 | +145 | 4x |
- teal_data_rv()- |
- |
150 | -- |
- })+ shinyjs::runjs(script) |
||
151 | +146 |
-
+ }, |
||
152 | -14x | -
- output$error <- renderUI({- |
- ||
153 | -! | -
- teal_data_rv_validate()- |
- ||
154 | -! | +147 | +3x |
- NULL+ ignoreNULL = FALSE |
155 | +148 |
- })+ ) |
||
156 | +149 |
-
+ } |
||
157 | +150 | |||
158 | -14x | +151 | +3x |
- res <- srv_teal(id = "teal", modules = modules, teal_data_rv = teal_data_rv_validate, filter = filter)+ showNotification("Data loaded - App fully started up") |
159 | -14x | +152 | +3x |
- logger::log_trace("srv_teal_with_splash initialized module with data.")+ logger::log_trace("srv_tabs_with_filters initialized the module") |
160 | -14x | +153 | +3x |
- return(res)+ return(active_module) |
161 | +154 |
}) |
||
162 | +155 |
}@@ -1251,14 +1202,14 @@ teal coverage - 63.82% |
1 |
- #' Create a `tdata` Object+ #' Create a UI of nested tabs of `teal_modules` |
||
3 |
- #' @description `r lifecycle::badge("deprecated")`+ #' @section `ui_nested_tabs`: |
||
4 |
- #' Create a new object called `tdata` which contains `data`, a `reactive` list of data.frames+ #' Each `teal_modules` is translated to a `tabsetPanel` and each |
||
5 |
- #' (or `MultiAssayExperiment`), with attributes:+ #' of its children is another tab-module called recursively. The UI of a |
||
6 |
- #' \itemize{+ #' `teal_module` is obtained by calling the `ui` function on it. |
||
7 |
- #' \item{`code` (`reactive`) containing code used to generate the data}+ #' |
||
8 |
- #' \item{join_keys (`join_keys`) containing the relationships between the data}+ #' The `datasets` argument is required to resolve the teal arguments in an |
||
9 |
- #' \item{metadata (`named list`) containing any metadata associated with the data frames}+ #' isolated context (with respect to reactivity) |
||
10 |
- #' }+ #' |
||
11 |
- #' @name tdata+ #' @section `srv_nested_tabs`: |
||
12 |
- #' @param data A `named list` of `data.frames` (or `MultiAssayExperiment`)+ #' This module calls recursively all elements of the `modules` returns one which |
||
13 |
- #' which optionally can be `reactive`.+ #' is currently active. |
||
14 |
- #' Inside this object all of these items will be made `reactive`.+ #' - `teal_module` returns self as a active module. |
||
15 |
- #' @param code A `character` (or `reactive` which evaluates to a `character`) containing+ #' - `teal_modules` also returns module active within self which is determined by the `input$active_tab`. |
||
16 |
- #' the code used to generate the data. This should be `reactive` if the code is changing+ #' |
||
17 |
- #' during a reactive context (e.g. if filtering changes the code). Inside this+ #' @name module_nested_tabs |
||
18 |
- #' object `code` will be made reactive+ #' |
||
19 |
- #' @param join_keys A `teal.data::join_keys` object containing relationships between the+ #' @inheritParams module_tabs_with_filters |
||
20 |
- #' datasets.+ #' |
||
21 |
- #' @param metadata A `named list` each element contains a list of metadata about the named data.frame+ #' @param depth (`integer(1)`)\cr |
||
22 |
- #' Each element of these list should be atomic and length one.+ #' number which helps to determine depth of the modules nesting. |
||
23 |
- #' @return A `tdata` object+ #' @param is_module_specific (`logical(1)`)\cr |
||
24 |
- #'+ #' flag determining if the filter panel is global or module-specific. |
||
25 |
- #' @seealso `as_tdata`+ #' When set to `TRUE`, a filter panel is called inside of each module tab. |
||
26 |
- #'+ #' @return depending on class of `modules`, `ui_nested_tabs` returns: |
||
27 |
- #' @examples+ #' - `teal_module`: instantiated UI of the module |
||
28 |
- #'+ #' - `teal_modules`: `tabsetPanel` with each tab corresponding to recursively |
||
29 |
- #' data <- new_tdata(+ #' calling this function on it.\cr |
||
30 |
- #' data = list(iris = iris, mtcars = reactive(mtcars), dd = data.frame(x = 1:10)),+ #' `srv_nested_tabs` returns a reactive which returns the active module that corresponds to the selected tab. |
||
31 |
- #' code = "iris <- iris+ #' |
||
32 |
- #' mtcars <- mtcars+ #' @examples |
||
33 |
- #' dd <- data.frame(x = 1:10)",+ #' mods <- teal:::example_modules() |
||
34 |
- #' metadata = list(dd = list(author = "NEST"), iris = list(version = 1))+ #' datasets <- teal:::example_datasets() |
||
35 |
- #' )+ #' app <- shinyApp( |
||
36 |
- #'+ #' ui = function() { |
||
37 |
- #' # Extract a data.frame+ #' tagList( |
||
38 |
- #' isolate(data[["iris"]]())+ #' teal:::include_teal_css_js(), |
||
39 |
- #'+ #' textOutput("info"), |
||
40 |
- #' # Get code+ #' fluidPage( # needed for nice tabs |
||
41 |
- #' isolate(get_code_tdata(data))+ #' teal:::ui_nested_tabs("dummy", modules = mods, datasets = datasets) |
||
42 |
- #'+ #' ) |
||
43 |
- #' # Get metadata+ #' ) |
||
44 |
- #' get_metadata(data, "iris")+ #' }, |
||
45 |
- #'+ #' server = function(input, output, session) { |
||
46 |
- #' @export+ #' active_module <- teal:::srv_nested_tabs( |
||
47 |
- new_tdata <- function(data, code = "", join_keys = NULL, metadata = NULL) {+ #' "dummy", |
||
48 | -34x | +
- lifecycle::deprecate_soft(+ #' datasets = datasets, |
|
49 | -34x | +
- when = "0.99.0",+ #' modules = mods |
|
50 | -34x | +
- what = "tdata()",+ #' ) |
|
51 | -34x | +
- details = paste(+ #' output$info <- renderText({ |
|
52 | -34x | +
- "tdata is deprecated and will be removed in the next release. Use `teal_data` instead.\n",+ #' paste0("The currently active tab name is ", active_module()$label) |
|
53 | -34x | +
- "Please follow migration instructions https://github.com/insightsengineering/teal/discussions/987."+ #' }) |
|
54 |
- )+ #' } |
||
55 |
- )+ #' ) |
||
56 | -34x | +
- checkmate::assert_list(+ #' if (interactive()) { |
|
57 | -34x | +
- data,+ #' shinyApp(app$ui, app$server) |
|
58 | -34x | +
- any.missing = FALSE, names = "unique",+ #' } |
|
59 | -34x | +
- types = c("data.frame", "reactive", "MultiAssayExperiment")+ #' @keywords internal |
|
60 |
- )+ NULL |
||
61 | -30x | +
- checkmate::assert_class(join_keys, "join_keys", null.ok = TRUE)+ |
|
62 | -29x | +
- checkmate::assert_multi_class(code, c("character", "reactive"))+ #' @rdname module_nested_tabs |
|
63 |
-
+ ui_nested_tabs <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) { |
||
64 | -28x | +! |
- checkmate::assert_list(metadata, names = "unique", null.ok = TRUE)+ checkmate::assert_multi_class(modules, c("teal_modules", "teal_module")) |
65 | -26x | +! |
- checkmate::assert_subset(names(metadata), names(data))+ checkmate::assert_count(depth) |
66 | -+ | ! |
-
+ UseMethod("ui_nested_tabs", modules) |
67 | -25x | +
- if (is.reactive(code)) {+ } |
|
68 | -9x | +
- isolate(checkmate::assert_class(code(), "character", .var.name = "code"))+ |
|
69 |
- }+ #' @rdname module_nested_tabs |
||
70 |
-
+ #' @export |
||
71 |
- # create reactive data.frames+ ui_nested_tabs.default <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) { |
||
72 | -24x | +! |
- for (x in names(data)) {+ stop("Modules class not supported: ", paste(class(modules), collapse = " ")) |
73 | -47x | +
- if (!is.reactive(data[[x]])) {+ } |
|
74 | -31x | +
- data[[x]] <- do.call(reactive, list(as.name(x)), envir = list2env(data[x]))+ |
|
75 |
- }+ #' @rdname module_nested_tabs |
||
76 |
- }+ #' @export |
||
77 |
-
+ ui_nested_tabs.teal_modules <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) { |
||
78 | -+ | ! |
- # set attributes+ checkmate::assert_list(datasets, types = c("list", "FilteredData")) |
79 | -24x | +! |
- attr(data, "code") <- if (is.reactive(code)) code else reactive(code)+ ns <- NS(id) |
80 | -24x | +! |
- attr(data, "join_keys") <- join_keys+ do.call( |
81 | -24x | +! |
- attr(data, "metadata") <- metadata+ tabsetPanel, |
82 | -+ | ! |
-
+ c( |
83 |
- # set class+ # by giving an id, we can reactively respond to tab changes |
||
84 | -24x | +! |
- class(data) <- c("tdata", class(data))+ list( |
85 | -24x | +! |
- data+ id = ns("active_tab"), |
86 | -+ | ! |
- }+ type = if (modules$label == "root") "pills" else "tabs" |
87 |
-
+ ), |
||
88 | -+ | ! |
- #' Function to convert a `tdata` object to an `environment`+ lapply( |
89 | -+ | ! |
- #' Any `reactives` inside `tdata` are first evaluated+ names(modules$children), |
90 | -+ | ! |
- #' @param data a `tdata` object+ function(module_id) { |
91 | -+ | ! |
- #' @return an `environment`+ module_label <- modules$children[[module_id]]$label |
92 | -+ | ! |
- #' @examples+ tabPanel( |
93 | -+ | ! |
- #'+ title = module_label, |
94 | -+ | ! |
- #' data <- new_tdata(+ value = module_id, # when clicked this tab value changes input$<tabset panel id> |
95 | -+ | ! |
- #' data = list(iris = iris, mtcars = reactive(mtcars)),+ ui_nested_tabs( |
96 | -+ | ! |
- #' code = "iris <- iris+ id = ns(module_id), |
97 | -+ | ! |
- #' mtcars = mtcars"+ modules = modules$children[[module_id]], |
98 | -+ | ! |
- #' )+ datasets = datasets[[module_label]], |
99 | -+ | ! |
- #'+ depth = depth + 1L, |
100 | -+ | ! |
- #' my_env <- isolate(tdata2env(data))+ is_module_specific = is_module_specific |
101 |
- #'+ ) |
||
102 |
- #' @export+ ) |
||
103 |
- tdata2env <- function(data) { # nolint+ } |
||
104 | -2x | +
- checkmate::assert_class(data, "tdata")+ ) |
|
105 | -1x | +
- list2env(lapply(data, function(x) if (is.reactive(x)) x() else x))+ ) |
|
106 |
- }+ ) |
||
107 |
-
+ } |
||
109 |
- #' Wrapper for `get_code.tdata`+ #' @rdname module_nested_tabs |
||
110 |
- #' This wrapper is to be used by downstream packages to extract the code of a `tdata` object+ #' @export |
||
111 |
- #'+ ui_nested_tabs.teal_module <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) { |
||
112 | -+ | ! |
- #' @param data (`tdata`) object+ checkmate::assert_class(datasets, classes = "FilteredData") |
113 | -+ | ! |
- #'+ ns <- NS(id) |
114 |
- #' @return (`character`) code used in the `tdata` object.+ |
||
115 | -+ | ! |
- #' @export+ args <- c(list(id = ns("module")), modules$ui_args) |
116 |
- get_code_tdata <- function(data) {+ |
||
117 | -7x | +! |
- checkmate::assert_class(data, "tdata")+ teal_ui <- tags$div( |
118 | -5x | +! |
- attr(data, "code")()+ id = id, |
119 | -+ | ! |
- }+ class = "teal_module", |
120 | -+ | ! |
-
+ uiOutput(ns("data_reactive"), inline = TRUE), |
121 | -+ | ! |
- #' Extract `join_keys` from `tdata`+ tagList( |
122 | -+ | ! |
- #' @param data A `tdata` object+ if (depth >= 2L) div(style = "mt-6"), |
123 | -+ | ! |
- #' @param ... Additional arguments (not used)+ do.call(modules$ui, args) |
124 |
- #' @export+ ) |
||
125 |
- join_keys.tdata <- function(data, ...) {+ ) |
||
126 | -2x | +
- attr(data, "join_keys")+ |
|
127 | -+ | ! |
- }+ if (!is.null(modules$datanames) && is_module_specific) { |
128 | -+ | ! |
-
+ fluidRow( |
129 | -+ | ! |
-
+ column(width = 9, teal_ui, class = "teal_primary_col"), |
130 | -+ | ! |
- #' Function to get metadata from a `tdata` object+ column( |
131 | -+ | ! |
- #' @param data `tdata` - object to extract the data from+ width = 3, |
132 | -+ | ! |
- #' @param dataname `character(1)` the dataset name whose metadata is requested+ datasets$ui_filter_panel(ns("module_filter_panel")), |
133 | -+ | ! |
- #' @return Either list of metadata or NULL if no metadata+ class = "teal_secondary_col" |
134 |
- #' @export+ ) |
||
135 |
- get_metadata <- function(data, dataname) {+ ) |
||
136 | -4x | +
- checkmate::assert_string(dataname)+ } else { |
|
137 | -4x | +! |
- UseMethod("get_metadata", data)+ teal_ui |
138 |
- }+ } |
||
139 |
-
+ } |
||
140 |
- #' @rdname get_metadata+ |
||
141 |
- #' @export+ #' @rdname module_nested_tabs |
||
142 |
- get_metadata.tdata <- function(data, dataname) {+ srv_nested_tabs <- function(id, datasets, modules, is_module_specific = FALSE, |
||
143 | -4x | +
- metadata <- attr(data, "metadata")+ reporter = teal.reporter::Reporter$new()) { |
|
144 | -4x | +50x |
- if (is.null(metadata)) {+ checkmate::assert_multi_class(modules, c("teal_modules", "teal_module")) |
145 | -1x | +50x |
- return(NULL)+ checkmate::assert_class(reporter, "Reporter") |
146 | -+ | 49x |
- }+ UseMethod("srv_nested_tabs", modules) |
147 | -3x | +
- metadata[[dataname]]+ } |
|
148 |
- }+ |
||
149 |
-
+ #' @rdname module_nested_tabs |
||
150 |
- #' @rdname get_metadata+ #' @export |
||
151 |
- #' @export+ srv_nested_tabs.default <- function(id, datasets, modules, is_module_specific = FALSE, |
||
152 |
- get_metadata.default <- function(data, dataname) {+ reporter = teal.reporter::Reporter$new()) { |
||
153 | ! |
- stop("get_metadata function not implemented for this object")+ stop("Modules class not supported: ", paste(class(modules), collapse = " ")) |
|
156 |
-
+ #' @rdname module_nested_tabs |
||
157 |
- #' Downgrade `teal_data` objects in modules for compatibility.+ #' @export |
||
158 |
- #'+ srv_nested_tabs.teal_modules <- function(id, datasets, modules, is_module_specific = FALSE, |
||
159 |
- #' Convert `teal_data` to `tdata` in `teal` modules.+ reporter = teal.reporter::Reporter$new()) { |
||
160 | -+ | 22x |
- #'+ checkmate::assert_list(datasets, types = c("list", "FilteredData")) |
161 |
- #' Recent changes in `teal` cause modules to fail because modules expect a `tdata` object+ |
||
162 | -+ | 22x |
- #' to be passed to the `data` argument but instead they receive a `teal_data` object,+ moduleServer(id = id, module = function(input, output, session) { |
163 | -+ | 22x |
- #' which is additionally wrapped in a reactive expression in the server functions.+ logger::log_trace("srv_nested_tabs.teal_modules initializing the module { deparse1(modules$label) }.") |
164 |
- #' In order to easily adapt such modules without a proper refactor,+ |
||
165 | -+ | 22x |
- #' use this function to downgrade the `data` argument.+ labels <- vapply(modules$children, `[[`, character(1), "label") |
166 | -+ | 22x |
- #'+ modules_reactive <- sapply( |
167 | -+ | 22x |
- #' @param x data object, either `tdata` or `teal_data`, the latter possibly in a reactive expression+ names(modules$children), |
168 | -+ | 22x |
- #'+ function(module_id) { |
169 | -+ | 33x |
- #' @return Object of class `tdata`.+ srv_nested_tabs( |
170 | -+ | 33x |
- #'+ id = module_id, |
171 | -+ | 33x |
- #' @examples+ datasets = datasets[[labels[module_id]]], |
172 | -+ | 33x |
- #' td <- teal_data()+ modules = modules$children[[module_id]], |
173 | -+ | 33x |
- #' td <- within(td, iris <- iris) %>% within(mtcars <- mtcars)+ is_module_specific = is_module_specific, |
174 | -+ | 33x |
- #' td+ reporter = reporter |
175 |
- #' as_tdata(td)+ ) |
||
176 |
- #' as_tdata(reactive(td))+ }, |
||
177 | -+ | 22x |
- #'+ simplify = FALSE |
178 |
- #' @export+ ) |
||
179 |
- #' @rdname tdata_deprecation+ |
||
180 |
- #'+ # when not ready input$active_tab would return NULL - this would fail next reactive |
||
181 | -+ | 22x |
- as_tdata <- function(x) {+ input_validated <- eventReactive(input$active_tab, input$active_tab, ignoreNULL = TRUE) |
182 | -8x | +22x |
- if (inherits(x, "tdata")) {+ get_active_module <- reactive({ |
183 | -2x | +12x |
- return(x)+ if (length(modules$children) == 1L) { |
184 |
- }+ # single tab is active by default |
||
185 | -6x | +1x |
- if (is.reactive(x)) {+ modules_reactive[[1]]() |
186 | -1x | +
- checkmate::assert_class(isolate(x()), "teal_data")+ } else { |
|
187 | -1x | +
- datanames <- isolate(teal_data_datanames(x()))+ # switch to active tab |
|
188 | -1x | +11x |
- datasets <- sapply(datanames, function(dataname) reactive(x()[[dataname]]), simplify = FALSE)+ modules_reactive[[input_validated()]]() |
189 | -1x | +
- code <- reactive(teal.code::get_code(x()))+ } |
|
190 | -1x | +
- join_keys <- isolate(teal.data::join_keys(x()))+ }) |
|
191 | -5x | +
- } else if (inherits(x, "teal_data")) {+ |
|
192 | -5x | +22x |
- datanames <- teal_data_datanames(x)+ get_active_module |
193 | -5x | +
- datasets <- sapply(datanames, function(dataname) reactive(x[[dataname]]), simplify = FALSE)+ }) |
|
194 | -5x | +
- code <- reactive(teal.code::get_code(x))+ } |
|
195 | -5x | +
- join_keys <- isolate(teal.data::join_keys(x))+ |
|
196 |
- }+ #' @rdname module_nested_tabs |
||
197 | - - | -||
198 | -6x | -
- new_tdata(data = datasets, code = code, join_keys = join_keys)+ #' @export |
|
199 | +198 |
- }+ srv_nested_tabs.teal_module <- function(id, datasets, modules, is_module_specific = TRUE, |
1 | +199 |
- #' Get Client Timezone+ reporter = teal.reporter::Reporter$new()) { |
||
2 | -+ | |||
200 | +27x |
- #'+ checkmate::assert_class(datasets, "FilteredData") |
||
3 | -+ | |||
201 | +27x |
- #' Local timezone in the browser may differ from the system timezone from the server.+ logger::log_trace("srv_nested_tabs.teal_module initializing the module: { deparse1(modules$label) }.") |
||
4 | +202 |
- #' This script can be run to register a shiny input which contains information about+ |
||
5 | -+ | |||
203 | +27x |
- #' the timezone in the browser.+ moduleServer(id = id, module = function(input, output, session) { |
||
6 | -+ | |||
204 | +27x |
- #'+ if (!is.null(modules$datanames) && is_module_specific) { |
||
7 | -+ | |||
205 | +! |
- #' @param ns (`function`) namespace function passed from the `session` object in the+ datasets$srv_filter_panel("module_filter_panel") |
||
8 | +206 |
- #' Shiny server. For Shiny modules this will allow for proper name spacing of the+ } |
||
9 | +207 |
- #' registered input.+ |
||
10 | +208 |
- #'+ # Create two triggers to limit reactivity between filter-panel and modules. |
||
11 | +209 |
- #' @return (`Shiny`) input variable accessible with `input$tz` which is a (`character`)+ # We want to recalculate only visible modules |
||
12 | +210 |
- #' string containing the timezone of the browser/client.+ # - trigger the data when the tab is selected |
||
13 | +211 |
- #' @keywords internal+ # - trigger module to be called when the tab is selected for the first time |
||
14 | -+ | |||
212 | +27x |
- get_client_timezone <- function(ns) {+ trigger_data <- reactiveVal(1L) |
||
15 | -18x | +213 | +27x |
- script <- sprintf(+ trigger_module <- reactiveVal(NULL) |
16 | -18x | +214 | +27x |
- "Shiny.setInputValue(`%s`, Intl.DateTimeFormat().resolvedOptions().timeZone)",+ output$data_reactive <- renderUI({ |
17 | -18x | +215 | +17x |
- ns("timezone")+ lapply(datasets$datanames(), function(x) {+ |
+
216 | +21x | +
+ datasets$get_data(x, filtered = TRUE) |
||
18 | +217 |
- )+ }) |
||
19 | -18x | +218 | +17x |
- shinyjs::runjs(script) # function does not return anything+ isolate(trigger_data(trigger_data() + 1)) |
20 | -18x | +219 | +17x |
- return(invisible(NULL))+ isolate(trigger_module(TRUE)) |
21 | +220 |
- }+ |
||
22 | -+ | |||
221 | +17x |
-
+ NULL |
||
23 | +222 |
- #' Resolve the expected bootstrap theme+ }) |
||
24 | +223 |
- #' @keywords internal+ |
||
25 | +224 |
- get_teal_bs_theme <- function() {+ # collect arguments to run teal_module |
||
26 | -16x | +225 | +27x |
- bs_theme <- getOption("teal.bs_theme")+ args <- c(list(id = "module"), modules$server_args) |
27 | -16x | +226 | +27x |
- if (is.null(bs_theme)) {+ if (is_arg_used(modules$server, "reporter")) { |
28 | -13x | +|||
227 | +! |
- NULL+ args <- c(args, list(reporter = reporter)) |
||
29 | -3x | +|||
228 | +
- } else if (!inherits(bs_theme, "bs_theme")) {+ } |
|||
30 | -2x | +|||
229 | +
- warning("teal.bs_theme has to be of a bslib::bs_theme class, the default shiny bootstrap is used.")+ |
|||
31 | -2x | -
- NULL- |
- ||
32 | -+ | 230 | +27x |
- } else {+ if (is_arg_used(modules$server, "datasets")) { |
33 | +231 | 1x |
- bs_theme+ args <- c(args, datasets = datasets) |
|
34 | +232 |
- }+ } |
||
35 | +233 |
- }+ |
||
36 | -+ | |||
234 | +27x |
-
+ if (is_arg_used(modules$server, "data")) { |
||
37 | -+ | |||
235 | +7x |
- include_parent_datanames <- function(dataname, join_keys) {+ data <- eventReactive(trigger_data(), .datasets_to_data(modules, datasets)) |
||
38 | -3x | +236 | +7x |
- parents <- character(0)+ args <- c(args, data = list(data)) |
39 | -3x | +|||
237 | +
- for (i in dataname) {+ } |
|||
40 | -6x | +|||
238 | +
- while (length(i) > 0) {+ |
|||
41 | -6x | +239 | +27x |
- parent_i <- teal.data::parent(join_keys, i)+ if (is_arg_used(modules$server, "filter_panel_api")) { |
42 | -6x | +240 | +2x |
- parents <- c(parent_i, parents)+ filter_panel_api <- teal.slice::FilterPanelAPI$new(datasets) |
43 | -6x | +241 | +2x |
- i <- parent_i+ args <- c(args, filter_panel_api = filter_panel_api) |
44 | +242 |
} |
||
45 | +243 |
- }+ |
||
46 | +244 |
-
+ # observe the trigger_module above to induce the module once the renderUI is triggered |
||
47 | -3x | +245 | +27x |
- return(unique(c(parents, dataname)))+ observeEvent( |
48 | -+ | |||
246 | +27x |
- }+ ignoreNULL = TRUE, |
||
49 | -+ | |||
247 | +27x |
-
+ once = TRUE, |
||
50 | -+ | |||
248 | +27x |
-
+ eventExpr = trigger_module(), |
||
51 | -+ | |||
249 | +27x |
-
+ handlerExpr = { |
||
52 | -+ | |||
250 | +17x |
- #' Create a `FilteredData`+ module_output <- if (is_arg_used(modules$server, "id")) { |
||
53 | -+ | |||
251 | +17x |
- #'+ do.call(modules$server, args) |
||
54 | +252 |
- #' Create a `FilteredData` object from a `teal_data` object+ } else { |
||
55 | -+ | |||
253 | +! |
- #' @param x (`teal_data`) object+ do.call(callModule, c(args, list(module = modules$server))) |
||
56 | +254 |
- #' @param datanames (`character`) vector of data set names to include; must be subset of `datanames(x)`+ } |
||
57 | +255 |
- #' @return (`FilteredData`) object+ } |
||
58 | +256 |
- #' @keywords internal+ ) |
||
59 | +257 |
- teal_data_to_filtered_data <- function(x, datanames = teal_data_datanames(x)) {+ |
||
60 | -13x | +258 | +27x |
- checkmate::assert_class(x, "teal_data")+ reactive(modules) |
61 | -13x | +|||
259 | +
- checkmate::assert_character(datanames, min.chars = 1L, any.missing = FALSE)+ }) |
|||
62 | +260 |
-
+ } |
||
63 | -13x | +|||
261 | +
- ans <- teal.slice::init_filtered_data(+ |
|||
64 | -13x | +|||
262 | +
- x = sapply(datanames, function(dn) x[[dn]], simplify = FALSE),+ #' Convert `FilteredData` to reactive list of datasets of the `teal_data` type. |
|||
65 | -13x | +|||
263 | +
- join_keys = teal.data::join_keys(x)+ #' |
|||
66 | +264 |
- )+ #' Converts `FilteredData` object to `teal_data` object containing datasets needed for a specific module. |
||
67 | +265 |
- # Piggy-back entire pre-processing code so that filtering code can be appended later.+ #' Please note that if module needs dataset which has a parent, then parent will be also returned. |
||
68 | -13x | +|||
266 | +
- attr(ans, "preprocessing_code") <- teal.code::get_code(x)+ #' A hash per `dataset` is calculated internally and returned in the code. |
|||
69 | -13x | +|||
267 | +
- ans+ #' |
|||
70 | +268 |
- }+ #' @param module (`teal_module`) module where needed filters are taken from |
||
71 | +269 |
-
+ #' @param datasets (`FilteredData`) object where needed data are taken from |
||
72 | +270 |
- #' Template Function for `TealReportCard` Creation and Customization+ #' @return A `teal_data` object. |
||
73 | +271 |
#' |
||
74 | +272 |
- #' This function generates a report card with a title,+ #' @keywords internal |
||
75 | +273 |
- #' an optional description, and the option to append the filter state list.+ .datasets_to_data <- function(module, datasets) { |
||
76 | -+ | |||
274 | +4x |
- #'+ checkmate::assert_class(module, "teal_module") |
||
77 | -+ | |||
275 | +4x |
- #' @param title (`character(1)`) title of the card (unless overwritten by label)+ checkmate::assert_class(datasets, "FilteredData") |
||
78 | +276 |
- #' @param label (`character(1)`) label provided by the user when adding the card+ |
||
79 | -+ | |||
277 | +4x |
- #' @param description (`character(1)`) optional additional description+ datanames <- if (is.null(module$datanames) || identical(module$datanames, "all")) { |
||
80 | -+ | |||
278 | +1x |
- #' @param with_filter (`logical(1)`) flag indicating to add filter state+ datasets$datanames() |
||
81 | +279 |
- #' @param filter_panel_api (`FilterPanelAPI`) object with API that allows the generation+ } else { |
||
82 | -+ | |||
280 | +3x |
- #' of the filter state in the report+ unique(module$datanames) # todo: include parents! unique shouldn't be needed here! |
||
83 | +281 |
- #'+ } |
||
84 | +282 |
- #' @return (`TealReportCard`) populated with a title, description and filter state+ |
||
85 | +283 |
- #'+ # list of reactive filtered data+ |
+ ||
284 | +4x | +
+ data <- sapply(datanames, function(x) datasets$get_data(x, filtered = TRUE), simplify = FALSE) |
||
86 | +285 |
- #' @export+ + |
+ ||
286 | +4x | +
+ hashes <- calculate_hashes(datanames, datasets) |
||
87 | +287 |
- report_card_template <- function(title, label, description = NULL, with_filter, filter_panel_api) {+ |
||
88 | -2x | +288 | +4x |
- checkmate::assert_string(title)+ code <- c( |
89 | -2x | +289 | +4x |
- checkmate::assert_string(label)+ get_rcode_str_install(), |
90 | -2x | +290 | +4x |
- checkmate::assert_string(description, null.ok = TRUE)+ get_rcode_libraries(), |
91 | -2x | +291 | +4x |
- checkmate::assert_flag(with_filter)+ get_datasets_code(datanames, datasets, hashes) |
92 | -2x | +|||
292 | +
- checkmate::assert_class(filter_panel_api, classes = "FilterPanelAPI")+ ) |
|||
93 | +293 | |||
94 | -2x | +294 | +4x |
- card <- teal::TealReportCard$new()+ do.call( |
95 | -2x | +295 | +4x |
- title <- if (label == "") title else label+ teal.data::teal_data, |
96 | -2x | +296 | +4x |
- card$set_name(title)+ args = c(data, code = list(code), join_keys = list(datasets$get_join_keys()[datanames])) |
97 | -2x | +|||
297 | +
- card$append_text(title, "header2")+ ) |
|||
98 | -1x | +|||
298 | +
- if (!is.null(description)) card$append_text(description, "header3")+ } |
|||
99 | -1x | +|||
299 | +
- if (with_filter) card$append_fs(filter_panel_api$get_filter_state())+ |
|||
100 | -2x | +|||
300 | +
- card+ #' Get the hash of a dataset |
|||
101 | +301 |
- }+ #' |
||
102 | +302 |
- #' Resolve `datanames` for the modules+ #' @param datanames (`character`) names of datasets |
||
103 | +303 |
- #'+ #' @param datasets (`FilteredData`) object holding the data |
||
104 | +304 |
- #' Modifies `module$datanames` to include names of the parent dataset (taken from `join_keys`).+ #' |
||
105 | +305 |
- #' When `datanames` is set to `"all"` it is replaced with all available datasets names.+ #' @return A list of hashes per dataset |
||
106 | +306 |
- #' @param modules (`teal_modules`) object+ #' @keywords internal |
||
107 | +307 |
- #' @param datanames (`character`) names of datasets available in the `data` object+ #' |
||
108 | +308 |
- #' @param join_keys (`join_keys`) object+ calculate_hashes <- function(datanames, datasets) {+ |
+ ||
309 | +7x | +
+ sapply(datanames, function(x) rlang::hash(datasets$get_data(x, filtered = FALSE)), simplify = FALSE) |
||
109 | +310 |
- #' @return `teal_modules` with resolved `datanames`+ } |
110 | +1 |
- #' @keywords internal+ #' Filter state snapshot management. |
||
111 | +2 |
- resolve_modules_datanames <- function(modules, datanames, join_keys) {+ #' |
||
112 | -! | +|||
3 | +
- if (inherits(modules, "teal_modules")) {+ #' Capture and restore snapshots of the global (app) filter state. |
|||
113 | -! | +|||
4 | +
- modules$children <- sapply(+ #' |
|||
114 | -! | +|||
5 | +
- modules$children,+ #' This module introduces snapshots: stored descriptions of the filter state of the entire application. |
|||
115 | -! | +|||
6 | +
- resolve_modules_datanames,+ #' Snapshots allow the user to save the current filter state of the application for later use in the session, |
|||
116 | -! | +|||
7 | +
- simplify = FALSE,+ #' as well as to save it to file in order to share it with an app developer or other users, |
|||
117 | -! | +|||
8 | +
- datanames = datanames,+ #' who in turn can upload it to their own session. |
|||
118 | -! | +|||
9 | +
- join_keys = join_keys+ #' |
|||
119 | +10 |
- )+ #' The snapshot manager is accessed through the filter manager, with the cog icon in the top right corner. |
||
120 | -! | +|||
11 | +
- modules+ #' At the beginning of a session it presents three icons: a camera, an upload, and an circular arrow. |
|||
121 | +12 |
- } else {+ #' Clicking the camera captures a snapshot, clicking the upload adds a snapshot from a file |
||
122 | -! | +|||
13 | +
- modules$datanames <- if (identical(modules$datanames, "all")) {+ #' and applies the filter states therein, and clicking the arrow resets initial application state. |
|||
123 | -! | +|||
14 | +
- datanames+ #' As snapshots are added, they will show up as rows in a table and each will have a select button and a save button. |
|||
124 | -! | +|||
15 | +
- } else if (is.character(modules$datanames)) {+ #' |
|||
125 | -! | +|||
16 | +
- extra_datanames <- setdiff(modules$datanames, datanames)+ #' @section Server logic: |
|||
126 | -! | +|||
17 | +
- if (length(extra_datanames)) {+ #' Snapshots are basically `teal_slices` objects, however, since each module is served by a separate instance |
|||
127 | -! | +|||
18 | +
- stop(+ #' of `FilteredData` and these objects require shared state, `teal_slice` is a `reactiveVal` so `teal_slices` |
|||
128 | -! | +|||
19 | +
- sprintf(+ #' cannot be stored as is. Therefore, `teal_slices` are reversibly converted to a list of lists representation |
|||
129 | -! | +|||
20 | +
- "Module %s has datanames that are not available in a 'data':\n %s not in %s",+ #' (attributes are maintained). |
|||
130 | -! | +|||
21 | +
- modules$label,+ #' |
|||
131 | -! | +|||
22 | +
- toString(extra_datanames),+ #' Snapshots are stored in a `reactiveVal` as a named list. |
|||
132 | -! | +|||
23 | +
- toString(datanames)+ #' The first snapshot is the initial state of the application and the user can add a snapshot whenever they see fit. |
|||
133 | +24 |
- )+ #' |
||
134 | +25 |
- )+ #' For every snapshot except the initial one, a piece of UI is generated that contains |
||
135 | +26 |
- }+ #' the snapshot name, a select button to restore that snapshot, and a save button to save it to a file. |
||
136 | -! | +|||
27 | +
- datanames_adjusted <- intersect(modules$datanames, datanames)- |
- |||
137 | -! | -
- include_parent_datanames(dataname = datanames_adjusted, join_keys = join_keys)+ #' The initial snapshot is restored by a separate "reset" button. |
||
138 | +28 |
- }- |
- ||
139 | -! | -
- modules+ #' It cannot be saved directly but a user is welcome to capture the initial state as a snapshot and save that. |
||
140 | +29 |
- }+ #' |
||
141 | +30 |
- }+ #' @section Snapshot mechanics: |
||
142 | +31 |
-
+ #' When a snapshot is captured, the user is prompted to name it. |
||
143 | +32 |
- #' Check `datanames` in modules+ #' Names are displayed as is but since they are used to create button ids, |
||
144 | +33 |
- #'+ #' under the hood they are converted to syntactically valid strings. |
||
145 | +34 |
- #' This function ensures specified `datanames` in modules match those in the data object,+ #' New snapshot names are validated so that their valid versions are unique. |
||
146 | +35 |
- #' returning error messages or `TRUE` for successful validation.+ #' Leading and trailing white space is trimmed. |
||
147 | +36 |
#' |
||
148 | +37 |
- #' @param modules (`teal_modules`) object+ #' The module can read the global state of the application from `slices_global` and `mapping_matrix`. |
||
149 | +38 |
- #' @param datanames (`character`) names of datasets available in the `data` object+ #' The former provides a list of all existing `teal_slice`s and the latter says which slice is active in which module. |
||
150 | +39 |
- #'+ #' Once a name has been accepted, `slices_global` is converted to a list of lists - a snapshot. |
||
151 | +40 |
- #' @return A `character(1)` containing error message or `TRUE` if validation passes.+ #' The snapshot contains the `mapping` attribute of the initial application state |
||
152 | +41 |
- #' @keywords internal+ #' (or one that has been restored), which may not reflect the current one, |
||
153 | +42 |
- check_modules_datanames <- function(modules, datanames) {- |
- ||
154 | -17x | -
- checkmate::assert_class(modules, "teal_modules")+ #' so `mapping_matrix` is transformed to obtain the current mapping, i.e. a list that, |
||
155 | -17x | +|||
43 | +
- checkmate::assert_character(datanames)+ #' when passed to the `mapping` argument of [`teal::teal_slices`], would result in the current mapping. |
|||
156 | +44 |
-
+ #' This is substituted as the snapshot's `mapping` attribute and the snapshot is added to the snapshot list. |
||
157 | -17x | +|||
45 | +
- recursive_check_datanames <- function(modules, datanames) {+ #' |
|||
158 | +46 |
- # check teal_modules against datanames+ #' To restore app state, a snapshot is retrieved from storage and rebuilt into a `teal_slices` object. |
||
159 | -36x | +|||
47 | +
- if (inherits(modules, "teal_modules")) {+ #' Then state of all `FilteredData` objects (provided in `filtered_data_list`) is cleared |
|||
160 | -17x | +|||
48 | +
- sapply(modules$children, function(module) recursive_check_datanames(module, datanames = datanames))+ #' and set anew according to the `mapping` attribute of the snapshot. |
|||
161 | +49 |
- } else {+ #' The snapshot is then set as the current content of `slices_global`. |
||
162 | -19x | +|||
50 | +
- extra_datanames <- setdiff(modules$datanames, c("all", datanames))+ #' |
|||
163 | -19x | +|||
51 | +
- if (length(extra_datanames)) {+ #' To save a snapshot, the snapshot is retrieved and reassembled just like for restoring, |
|||
164 | -2x | +|||
52 | +
- sprintf(+ #' and then saved to file with [`slices_store`]. |
|||
165 | -2x | +|||
53 | +
- "- Module '%s' uses datanames not available in 'data': (%s) not in (%s)",+ #' |
|||
166 | -2x | +|||
54 | +
- modules$label,+ #' When a snapshot is uploaded, it will first be added to storage just like a newly created one, |
|||
167 | -2x | +|||
55 | +
- toString(dQuote(extra_datanames, q = FALSE)),+ #' and then used to restore app state much like a snapshot taken from storage. |
|||
168 | -2x | +|||
56 | +
- toString(dQuote(datanames, q = FALSE))+ #' Upon clicking the upload icon the user will be prompted for a file to upload |
|||
169 | +57 |
- )+ #' and may choose to name the new snapshot. The name defaults to the name of the file (the extension is dropped) |
||
170 | +58 |
- }+ #' and normal naming rules apply. Loading the file yields a `teal_slices` object, |
||
171 | +59 |
- }+ #' which is disassembled for storage and used directly for restoring app state. |
||
172 | +60 |
- }+ #' |
||
173 | -17x | +|||
61 | +
- check_datanames <- unlist(recursive_check_datanames(modules, datanames))+ #' @section Transferring snapshots: |
|||
174 | -17x | +|||
62 | +
- if (length(check_datanames)) {+ #' Snapshots uploaded from disk should only be used in the same application they come from, |
|||
175 | -2x | +|||
63 | +
- paste(check_datanames, collapse = "\n")+ #' _i.e._ an application that uses the same data and the same modules. |
|||
176 | +64 |
- } else {+ #' To ensure this is the case, `init` stamps `teal_slices` with an app id that is stored in the `app_id` attribute of |
||
177 | -15x | +|||
65 | +
- TRUE+ #' a `teal_slices` object. When a snapshot is restored from file, its `app_id` is compared to that |
|||
178 | +66 |
- }+ #' of the current app state and only if the match is the snapshot admitted to the session. |
||
179 | +67 |
- }+ #' |
||
180 | +68 |
-
+ #' @param id (`character(1)`) `shiny` module id |
||
181 | +69 |
- #' Check `datanames` in filters+ #' @param slices_global (`reactiveVal`) that contains a `teal_slices` object |
||
182 | +70 |
- #'+ #' containing all `teal_slice`s existing in the app, both active and inactive |
||
183 | +71 |
- #' This function checks whether `datanames` in filters correspond to those in `data`,+ #' @param mapping_matrix (`reactive`) that contains a `data.frame` representation |
||
184 | +72 |
- #' returning character vector with error messages or TRUE if all checks pass.+ #' of the mapping of filter state ids (rows) to modules labels (columns); |
||
185 | +73 |
- #'+ #' all columns are `logical` vectors |
||
186 | +74 |
- #' @param filters (`teal_slices`) object+ #' @param filtered_data_list non-nested (`named list`) that contains `FilteredData` objects |
||
187 | +75 |
- #' @param datanames (`character`) names of datasets available in the `data` object+ #' |
||
188 | +76 |
- #'+ #' @return Nothing is returned. |
||
189 | +77 |
- #' @return A `character(1)` containing error message or TRUE if validation passes.+ #' |
||
190 | +78 |
- #' @keywords internal+ #' @name snapshot_manager_module |
||
191 | +79 |
- check_filter_datanames <- function(filters, datanames) {- |
- ||
192 | -15x | -
- checkmate::assert_class(filters, "teal_slices")+ #' @aliases snapshot snapshot_manager |
||
193 | -15x | +|||
80 | +
- checkmate::assert_character(datanames)+ #' |
|||
194 | +81 |
-
+ #' @author Aleksander Chlebowski |
||
195 | +82 |
- # check teal_slices against datanames+ #' |
||
196 | -15x | +|||
83 | +
- out <- unlist(sapply(+ #' @rdname snapshot_manager_module |
|||
197 | -15x | +|||
84 | +
- filters, function(filter) {+ #' @keywords internal |
|||
198 | -3x | +|||
85 | +
- dataname <- shiny::isolate(filter$dataname)+ #' |
|||
199 | -3x | +|||
86 | +
- if (!dataname %in% datanames) {+ snapshot_manager_ui <- function(id) { |
|||
200 | -2x | +|||
87 | +! |
- sprintf(+ ns <- NS(id) |
||
201 | -2x | +|||
88 | +! |
- "- Filter '%s' refers to dataname not available in 'data':\n %s not in (%s)",+ div( |
||
202 | -2x | +|||
89 | +! |
- shiny::isolate(filter$id),+ class = "snapshot_manager_content", |
||
203 | -2x | +|||
90 | +! |
- dQuote(dataname, q = FALSE),+ div( |
||
204 | -2x | +|||
91 | +! |
- toString(dQuote(datanames, q = FALSE))+ class = "snapshot_table_row", |
||
205 | -+ | |||
92 | +! |
- )+ span(tags$b("Snapshot manager")), |
||
206 | -+ | |||
93 | +! |
- }+ actionLink(ns("snapshot_add"), label = NULL, icon = icon("camera"), title = "add snapshot"), |
||
207 | -+ | |||
94 | +! |
- }+ actionLink(ns("snapshot_load"), label = NULL, icon = icon("upload"), title = "upload snapshot"), |
||
208 | -+ | |||
95 | +! |
- ))+ actionLink(ns("snapshot_reset"), label = NULL, icon = icon("undo"), title = "reset initial state"), |
||
209 | -+ | |||
96 | +! |
-
+ NULL |
||
210 | +97 | - - | -||
211 | -15x | -
- if (length(out)) {+ ), |
||
212 | -2x | +|||
98 | +! |
- paste(out, collapse = "\n")+ uiOutput(ns("snapshot_list")) |
||
213 | +99 |
- } else {- |
- ||
214 | -13x | -
- TRUE+ ) |
||
215 | +100 |
- }+ } |
||
216 | +101 |
- }+ |
||
217 | +102 |
-
+ #' @rdname snapshot_manager_module |
||
218 | +103 |
- #' Wrapper on `teal.data::datanames`+ #' @keywords internal |
||
219 | +104 |
#' |
||
220 | +105 |
- #' Special function used in internals of `teal` to return names of datasets even if `datanames`+ snapshot_manager_srv <- function(id, slices_global, mapping_matrix, filtered_data_list) { |
||
221 | -+ | |||
106 | +6x |
- #' has not been set.+ checkmate::assert_character(id) |
||
222 | -+ | |||
107 | +6x |
- #' @param data (`teal_data`)+ checkmate::assert_true(is.reactive(slices_global)) |
||
223 | -+ | |||
108 | +6x |
- #' @return `character`+ checkmate::assert_class(isolate(slices_global()), "teal_slices") |
||
224 | -+ | |||
109 | +6x |
- #' @keywords internal+ checkmate::assert_true(is.reactive(mapping_matrix)) |
||
225 | -+ | |||
110 | +6x |
- teal_data_datanames <- function(data) {+ checkmate::assert_data_frame(isolate(mapping_matrix()), null.ok = TRUE) |
||
226 | -66x | +111 | +6x |
- checkmate::assert_class(data, "teal_data")+ checkmate::assert_list(filtered_data_list, types = "FilteredData", any.missing = FALSE, names = "named") |
227 | -66x | +|||
112 | +
- if (length(teal.data::datanames(data))) {+ |
|||
228 | -62x | -
- teal.data::datanames(data)- |
- ||
229 | -+ | 113 | +6x |
- } else {+ moduleServer(id, function(input, output, session) { |
230 | -4x | +114 | +6x |
- ls(teal.code::get_env(data), all.names = TRUE)+ ns <- session$ns |
231 | +115 |
- }+ |
||
232 | +116 |
- }+ # Store global filter states ---- |
||
233 | -+ | |||
117 | +6x |
-
+ filter <- isolate(slices_global()) |
||
234 | -+ | |||
118 | +6x |
- #' Function for validating the title parameter of `teal::init`+ snapshot_history <- reactiveVal({ |
||
235 | -+ | |||
119 | +6x |
- #'+ list( |
||
236 | -+ | |||
120 | +6x |
- #' Checks if the input of the title from `teal::init` will create a valid title and favicon tag.+ "Initial application state" = as.list(filter, recursive = TRUE) |
||
237 | +121 |
- #' @param shiny_tag (`shiny.tag`) Object to validate for a valid title.+ ) |
||
238 | +122 |
- #' @keywords internal+ }) |
||
239 | +123 |
- validate_app_title_tag <- function(shiny_tag) {+ |
||
240 | -21x | +|||
124 | +
- checkmate::assert_class(shiny_tag, "shiny.tag")+ # Snapshot current application state ---- |
|||
241 | -21x | +|||
125 | +
- checkmate::assert_true(shiny_tag$name == "head")+ # Name snaphsot. |
|||
242 | -20x | +126 | +6x |
- child_names <- vapply(shiny_tag$children, `[[`, character(1L), "name")+ observeEvent(input$snapshot_add, { |
243 | -20x | +|||
127 | +! |
- checkmate::assert_subset(c("title", "link"), child_names, .var.name = "child tags")+ showModal( |
||
244 | -18x | +|||
128 | +! |
- rel_attr <- shiny_tag$children[[which(child_names == "link")]]$attribs$rel+ modalDialog( |
||
245 | -18x | +|||
129 | +! |
- checkmate::assert_subset(+ textInput(ns("snapshot_name"), "Name the snapshot", width = "100%", placeholder = "Meaningful, unique name"), |
||
246 | -18x | +|||
130 | +! |
- rel_attr, c("icon", "shortcut icon"),+ footer = tagList( |
||
247 | -18x | +|||
131 | +! |
- .var.name = "Link tag's rel attribute",+ actionButton(ns("snapshot_name_accept"), "Accept", icon = icon("thumbs-up")), |
||
248 | -18x | +|||
132 | +! |
- empty.ok = FALSE+ modalButton(label = "Cancel", icon = icon("thumbs-down")) |
||
249 | +133 |
- )+ ), |
||
250 | -+ | |||
134 | +! |
- }+ size = "s" |
||
251 | +135 |
-
+ ) |
||
252 | +136 |
- #' Build app title with favicon+ ) |
||
253 | +137 |
- #'+ }) |
||
254 | +138 |
- #' A helper function to create the browser title along with a logo.+ # Store snaphsot. |
||
255 | -+ | |||
139 | +6x |
- #'+ observeEvent(input$snapshot_name_accept, { |
||
256 | -+ | |||
140 | +! |
- #' @param title (`character`) The browser title for the teal app+ snapshot_name <- trimws(input$snapshot_name) |
||
257 | -+ | |||
141 | +! |
- #' @param favicon (`character`) The path for the icon for the title.+ if (identical(snapshot_name, "")) { |
||
258 | -+ | |||
142 | +! |
- #' The image/icon path can be remote or the static path accessible by shiny, like the `www/`+ showNotification( |
||
259 | -+ | |||
143 | +! |
- #'+ "Please name the snapshot.", |
||
260 | -+ | |||
144 | +! |
- #' @return A `shiny.tag` containing the element that adds the title and logo to the shiny app+ type = "message" |
||
261 | +145 |
- #' @export+ ) |
||
262 | -+ | |||
146 | +! |
- build_app_title <- function(title = "Teal app", favicon = "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/nest.png") { # nolint+ updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name") |
||
263 | -16x | +|||
147 | +! |
- checkmate::assert_string(title, null.ok = TRUE)+ } else if (is.element(make.names(snapshot_name), make.names(names(snapshot_history())))) { |
||
264 | -16x | +|||
148 | +! |
- checkmate::assert_string(favicon, null.ok = TRUE)+ showNotification( |
||
265 | -16x | +|||
149 | +! |
- tags$head(+ "This name is in conflict with other snapshot names. Please choose a different one.", |
||
266 | -16x | +|||
150 | +! |
- tags$title(title),+ type = "message" |
||
267 | -16x | +|||
151 | +
- tags$link(+ ) |
|||
268 | -16x | +|||
152 | +! |
- rel = "icon",+ updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name") |
||
269 | -16x | +|||
153 | +
- href = favicon,+ } else { |
|||
270 | -16x | +|||
154 | +! |
- sizes = "any"+ snapshot <- as.list(slices_global(), recursive = TRUE) |
||
271 | -+ | |||
155 | +! |
- )+ attr(snapshot, "mapping") <- matrix_to_mapping(mapping_matrix()) |
||
272 | -+ | |||
156 | +! |
- )+ snapshot_update <- c(snapshot_history(), list(snapshot)) |
||
273 | -+ | |||
157 | +! |
- }+ names(snapshot_update)[length(snapshot_update)] <- snapshot_name |
1 | -+ | |||
158 | +! |
- #' Filter state snapshot management.+ snapshot_history(snapshot_update) |
||
2 | -+ | |||
159 | +! |
- #'+ removeModal() |
||
3 | +160 |
- #' Capture and restore snapshots of the global (app) filter state.+ # Reopen filter manager modal by clicking button in the main application. |
||
4 | -+ | |||
161 | +! |
- #'+ shinyjs::click(id = "teal-main_ui-filter_manager-show", asis = TRUE) |
||
5 | +162 |
- #' This module introduces snapshots: stored descriptions of the filter state of the entire application.+ } |
||
6 | +163 |
- #' Snapshots allow the user to save the current filter state of the application for later use in the session,+ }) |
||
7 | +164 |
- #' as well as to save it to file in order to share it with an app developer or other users,+ |
||
8 | +165 |
- #' who in turn can upload it to their own session.+ # Upload a snapshot file ---- |
||
9 | +166 |
- #'+ # Select file. |
||
10 | -+ | |||
167 | +6x |
- #' The snapshot manager is accessed through the filter manager, with the cog icon in the top right corner.+ observeEvent(input$snapshot_load, { |
||
11 | -+ | |||
168 | +! |
- #' At the beginning of a session it presents three icons: a camera, an upload, and an circular arrow.+ showModal( |
||
12 | -+ | |||
169 | +! |
- #' Clicking the camera captures a snapshot, clicking the upload adds a snapshot from a file+ modalDialog( |
||
13 | -+ | |||
170 | +! |
- #' and applies the filter states therein, and clicking the arrow resets initial application state.+ fileInput(ns("snapshot_file"), "Choose snapshot file", accept = ".json", width = "100%"), |
||
14 | -+ | |||
171 | +! |
- #' As snapshots are added, they will show up as rows in a table and each will have a select button and a save button.+ textInput( |
||
15 | -+ | |||
172 | +! |
- #'+ ns("snapshot_name"), |
||
16 | -+ | |||
173 | +! |
- #' @section Server logic:+ "Name the snapshot (optional)", |
||
17 | -+ | |||
174 | +! |
- #' Snapshots are basically `teal_slices` objects, however, since each module is served by a separate instance+ width = "100%", |
||
18 | -+ | |||
175 | +! |
- #' of `FilteredData` and these objects require shared state, `teal_slice` is a `reactiveVal` so `teal_slices`+ placeholder = "Meaningful, unique name" |
||
19 | +176 |
- #' cannot be stored as is. Therefore, `teal_slices` are reversibly converted to a list of lists representation+ ), |
||
20 | -+ | |||
177 | +! |
- #' (attributes are maintained).+ footer = tagList( |
||
21 | -+ | |||
178 | +! |
- #'+ actionButton(ns("snaphot_file_accept"), "Accept", icon = icon("thumbs-up")), |
||
22 | -+ | |||
179 | +! |
- #' Snapshots are stored in a `reactiveVal` as a named list.+ modalButton(label = "Cancel", icon = icon("thumbs-down")) |
||
23 | +180 |
- #' The first snapshot is the initial state of the application and the user can add a snapshot whenever they see fit.+ ) |
||
24 | +181 |
- #'+ ) |
||
25 | +182 |
- #' For every snapshot except the initial one, a piece of UI is generated that contains+ ) |
||
26 | +183 |
- #' the snapshot name, a select button to restore that snapshot, and a save button to save it to a file.+ }) |
||
27 | +184 |
- #' The initial snapshot is restored by a separate "reset" button.+ # Store new snapshot to list and restore filter states. |
||
28 | -+ | |||
185 | +6x |
- #' It cannot be saved directly but a user is welcome to capture the initial state as a snapshot and save that.+ observeEvent(input$snaphot_file_accept, { |
||
29 | -- |
- #'+ | ||
186 | +! | +
+ snapshot_name <- trimws(input$snapshot_name) |
||
30 | -+ | |||
187 | +! |
- #' @section Snapshot mechanics:+ if (identical(snapshot_name, "")) { |
||
31 | -+ | |||
188 | +! |
- #' When a snapshot is captured, the user is prompted to name it.+ snapshot_name <- tools::file_path_sans_ext(input$snapshot_file$name) |
||
32 | +189 |
- #' Names are displayed as is but since they are used to create button ids,+ } |
||
33 | -+ | |||
190 | +! |
- #' under the hood they are converted to syntactically valid strings.+ if (is.element(make.names(snapshot_name), make.names(names(snapshot_history())))) { |
||
34 | -+ | |||
191 | +! |
- #' New snapshot names are validated so that their valid versions are unique.+ showNotification( |
||
35 | -+ | |||
192 | +! |
- #' Leading and trailing white space is trimmed.+ "This name is in conflict with other snapshot names. Please choose a different one.", |
||
36 | -+ | |||
193 | +! |
- #'+ type = "message" |
||
37 | +194 |
- #' The module can read the global state of the application from `slices_global` and `mapping_matrix`.+ ) |
||
38 | -+ | |||
195 | +! |
- #' The former provides a list of all existing `teal_slice`s and the latter says which slice is active in which module.+ updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name") |
||
39 | +196 |
- #' Once a name has been accepted, `slices_global` is converted to a list of lists - a snapshot.+ } else { |
||
40 | +197 |
- #' The snapshot contains the `mapping` attribute of the initial application state+ # Restore snapshot and verify app compatibility. |
||
41 | -+ | |||
198 | +! |
- #' (or one that has been restored), which may not reflect the current one,+ snapshot_state <- try(slices_restore(input$snapshot_file$datapath)) |
||
42 | -+ | |||
199 | +! |
- #' so `mapping_matrix` is transformed to obtain the current mapping, i.e. a list that,+ if (!inherits(snapshot_state, "modules_teal_slices")) { |
||
43 | -+ | |||
200 | +! |
- #' when passed to the `mapping` argument of [`teal::teal_slices`], would result in the current mapping.+ showNotification( |
||
44 | -+ | |||
201 | +! |
- #' This is substituted as the snapshot's `mapping` attribute and the snapshot is added to the snapshot list.+ "File appears to be corrupt.", |
||
45 | -+ | |||
202 | +! |
- #'+ type = "error" |
||
46 | +203 |
- #' To restore app state, a snapshot is retrieved from storage and rebuilt into a `teal_slices` object.+ ) |
||
47 | -+ | |||
204 | +! |
- #' Then state of all `FilteredData` objects (provided in `filtered_data_list`) is cleared+ } else if (!identical(attr(snapshot_state, "app_id"), attr(slices_global(), "app_id"))) { |
||
48 | -+ | |||
205 | +! |
- #' and set anew according to the `mapping` attribute of the snapshot.+ showNotification( |
||
49 | -+ | |||
206 | +! |
- #' The snapshot is then set as the current content of `slices_global`.+ "This snapshot file is not compatible with the app and cannot be loaded.", |
||
50 | -+ | |||
207 | +! |
- #'+ type = "warning" |
||
51 | +208 |
- #' To save a snapshot, the snapshot is retrieved and reassembled just like for restoring,+ ) |
||
52 | +209 |
- #' and then saved to file with [`slices_store`].+ } else { |
||
53 | +210 |
- #'+ # Add to snapshot history. |
||
54 | -+ | |||
211 | +! |
- #' When a snapshot is uploaded, it will first be added to storage just like a newly created one,+ snapshot <- as.list(snapshot_state, recursive = TRUE) |
||
55 | -+ | |||
212 | +! |
- #' and then used to restore app state much like a snapshot taken from storage.+ snapshot_update <- c(snapshot_history(), list(snapshot)) |
||
56 | -+ | |||
213 | +! |
- #' Upon clicking the upload icon the user will be prompted for a file to upload+ names(snapshot_update)[length(snapshot_update)] <- snapshot_name |
||
57 | -+ | |||
214 | +! |
- #' and may choose to name the new snapshot. The name defaults to the name of the file (the extension is dropped)+ snapshot_history(snapshot_update) |
||
58 | +215 |
- #' and normal naming rules apply. Loading the file yields a `teal_slices` object,+ ### Begin simplified restore procedure. ### |
||
59 | -+ | |||
216 | +! |
- #' which is disassembled for storage and used directly for restoring app state.+ mapping_unfolded <- unfold_mapping(attr(snapshot_state, "mapping"), names(filtered_data_list)) |
||
60 | -+ | |||
217 | +! |
- #'+ mapply( |
||
61 | -+ | |||
218 | +! |
- #' @section Transferring snapshots:+ function(filtered_data, filter_ids) { |
||
62 | -+ | |||
219 | +! |
- #' Snapshots uploaded from disk should only be used in the same application they come from,+ filtered_data$clear_filter_states(force = TRUE) |
||
63 | -+ | |||
220 | +! |
- #' _i.e._ an application that uses the same data and the same modules.+ slices <- Filter(function(x) x$id %in% filter_ids, snapshot_state) |
||
64 | -+ | |||
221 | +! |
- #' To ensure this is the case, `init` stamps `teal_slices` with an app id that is stored in the `app_id` attribute of+ filtered_data$set_filter_state(slices) |
||
65 | +222 |
- #' 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 | -- |
- #' @param id (`character(1)`) `shiny` module id- |
- ||
69 | -- |
- #' @param slices_global (`reactiveVal`) that contains a `teal_slices` object- |
- ||
70 | -- |
- #' containing all `teal_slice`s existing in the app, both active and inactive- |
- ||
71 | -+ | |||
223 | +! |
- #' @param mapping_matrix (`reactive`) that contains a `data.frame` representation+ filtered_data = filtered_data_list, |
||
72 | -+ | |||
224 | +! |
- #' of the mapping of filter state ids (rows) to modules labels (columns);+ filter_ids = mapping_unfolded |
||
73 | +225 |
- #' all columns are `logical` vectors+ ) |
||
74 | -+ | |||
226 | +! |
- #' @param filtered_data_list non-nested (`named list`) that contains `FilteredData` objects+ slices_global(snapshot_state) |
||
75 | -+ | |||
227 | +! |
- #'+ removeModal() |
||
76 | +228 |
- #' @return Nothing is returned.+ ### End simplified restore procedure. ### |
||
77 | +229 |
- #'+ } |
||
78 | +230 |
- #' @name snapshot_manager_module+ } |
||
79 | +231 |
- #' @aliases snapshot snapshot_manager+ }) |
||
80 | +232 |
- #'+ # Apply newly added snapshot. |
||
81 | +233 |
- #' @author Aleksander Chlebowski+ |
||
82 | +234 |
- #'+ # Restore initial state ---- |
||
83 | -+ | |||
235 | +6x |
- #' @rdname snapshot_manager_module+ observeEvent(input$snapshot_reset, { |
||
84 | -+ | |||
236 | +! |
- #' @keywords internal+ s <- "Initial application state" |
||
85 | +237 |
- #'+ ### Begin restore procedure. ### |
||
86 | -+ | |||
238 | +! |
- snapshot_manager_ui <- function(id) {+ snapshot <- snapshot_history()[[s]] |
||
87 | +239 | ! |
- ns <- NS(id)+ snapshot_state <- as.teal_slices(snapshot) |
|
88 | +240 | ! |
- div(+ mapping_unfolded <- unfold_mapping(attr(snapshot_state, "mapping"), names(filtered_data_list)) |
|
89 | +241 | ! |
- class = "snapshot_manager_content",+ mapply( |
|
90 | +242 | ! |
- div(+ function(filtered_data, filter_ids) { |
|
91 | +243 | ! |
- class = "snapshot_table_row",+ filtered_data$clear_filter_states(force = TRUE) |
|
92 | +244 | ! |
- span(tags$b("Snapshot manager")),+ slices <- Filter(function(x) x$id %in% filter_ids, snapshot_state) |
|
93 | +245 | ! |
- actionLink(ns("snapshot_add"), label = NULL, icon = icon("camera"), title = "add snapshot"),+ filtered_data$set_filter_state(slices) |
|
94 | -! | +|||
246 | +
- actionLink(ns("snapshot_load"), label = NULL, icon = icon("upload"), title = "upload snapshot"),+ }, |
|||
95 | +247 | ! |
- actionLink(ns("snapshot_reset"), label = NULL, icon = icon("undo"), title = "reset initial state"),+ filtered_data = filtered_data_list, |
|
96 | +248 | ! |
- NULL+ filter_ids = mapping_unfolded |
|
97 | +249 |
- ),+ ) |
||
98 | +250 | ! |
- uiOutput(ns("snapshot_list"))+ slices_global(snapshot_state)+ |
+ |
251 | +! | +
+ removeModal() |
||
99 | +252 |
- )+ ### End restore procedure. ### |
||
100 | +253 |
- }+ }) |
||
101 | +254 | |||
102 | +255 |
- #' @rdname snapshot_manager_module+ # Build snapshot table ---- |
||
103 | +256 |
- #' @keywords internal+ # Create UI elements and server logic for the snapshot table. |
||
104 | +257 |
- #'+ # Observers must be tracked to avoid duplication and excess reactivity. |
||
105 | +258 |
- snapshot_manager_srv <- function(id, slices_global, mapping_matrix, filtered_data_list) {+ # Remaining elements are tracked likewise for consistency and a slight speed margin. |
||
106 | +259 | 6x |
- checkmate::assert_character(id)+ observers <- reactiveValues() |
|
107 | +260 | 6x |
- checkmate::assert_true(is.reactive(slices_global))+ handlers <- reactiveValues() |
|
108 | +261 | 6x |
- checkmate::assert_class(isolate(slices_global()), "teal_slices")+ divs <- reactiveValues() |
|
109 | -6x | +|||
262 | +
- checkmate::assert_true(is.reactive(mapping_matrix))+ |
|||
110 | +263 | 6x |
- checkmate::assert_data_frame(isolate(mapping_matrix()), null.ok = TRUE)+ observeEvent(snapshot_history(), { |
|
111 | -6x | +264 | +2x |
- checkmate::assert_list(filtered_data_list, types = "FilteredData", any.missing = FALSE, names = "named")+ lapply(names(snapshot_history())[-1L], function(s) { |
112 | -+ | |||
265 | +! |
-
+ id_pickme <- sprintf("pickme_%s", make.names(s)) |
||
113 | -6x | +|||
266 | +! |
- moduleServer(id, function(input, output, session) {+ id_saveme <- sprintf("saveme_%s", make.names(s)) |
||
114 | -6x | +|||
267 | +! |
- ns <- session$ns+ id_rowme <- sprintf("rowme_%s", make.names(s)) |
||
115 | +268 | |||
116 | +269 |
- # Store global filter states ----- |
- ||
117 | -6x | -
- filter <- isolate(slices_global())- |
- ||
118 | -6x | -
- snapshot_history <- reactiveVal({+ # Observer for restoring snapshot. |
||
119 | -6x | +|||
270 | +! |
- list(+ if (!is.element(id_pickme, names(observers))) { |
||
120 | -6x | +|||
271 | +! |
- "Initial application state" = as.list(filter, recursive = TRUE)+ observers[[id_pickme]] <- observeEvent(input[[id_pickme]], { |
||
121 | +272 |
- )+ ### Begin restore procedure. ### |
||
122 | -+ | |||
273 | +! |
- })+ snapshot <- snapshot_history()[[s]] |
||
123 | -+ | |||
274 | +! |
-
+ snapshot_state <- as.teal_slices(snapshot) |
||
124 | -+ | |||
275 | +! |
- # Snapshot current application state ----+ mapping_unfolded <- unfold_mapping(attr(snapshot_state, "mapping"), names(filtered_data_list)) |
||
125 | -+ | |||
276 | +! |
- # Name snaphsot.+ mapply( |
||
126 | -6x | +|||
277 | +! |
- observeEvent(input$snapshot_add, {+ function(filtered_data, filter_ids) { |
||
127 | +278 | ! |
- showModal(+ filtered_data$clear_filter_states(force = TRUE) |
|
128 | +279 | ! |
- modalDialog(+ slices <- Filter(function(x) x$id %in% filter_ids, snapshot_state) |
|
129 | +280 | ! |
- textInput(ns("snapshot_name"), "Name the snapshot", width = "100%", placeholder = "Meaningful, unique name"),+ filtered_data$set_filter_state(slices) |
|
130 | -! | +|||
281 | +
- footer = tagList(+ }, |
|||
131 | +282 | ! |
- actionButton(ns("snapshot_name_accept"), "Accept", icon = icon("thumbs-up")),+ filtered_data = filtered_data_list, |
|
132 | +283 | ! |
- modalButton(label = "Cancel", icon = icon("thumbs-down"))+ filter_ids = mapping_unfolded |
|
133 | +284 |
- ),+ ) |
||
134 | +285 | ! |
- size = "s"+ slices_global(snapshot_state) |
|
135 | -+ | |||
286 | +! |
- )+ removeModal() |
||
136 | +287 |
- )+ ### End restore procedure. ### |
||
137 | +288 |
- })+ }) |
||
138 | +289 |
- # Store snaphsot.- |
- ||
139 | -6x | -
- observeEvent(input$snapshot_name_accept, {+ } |
||
140 | -! | +|||
290 | +
- snapshot_name <- trimws(input$snapshot_name)+ # Create handler for downloading snapshot. |
|||
141 | +291 | ! |
- if (identical(snapshot_name, "")) {+ if (!is.element(id_saveme, names(handlers))) { |
|
142 | +292 | ! |
- showNotification(+ output[[id_saveme]] <- downloadHandler( |
|
143 | +293 | ! |
- "Please name the snapshot.",+ filename = function() { |
|
144 | +294 | ! |
- type = "message"+ sprintf("teal_snapshot_%s_%s.json", s, Sys.Date()) |
|
145 | +295 |
- )+ }, |
||
146 | +296 | ! |
- updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name")+ content = function(file) { |
|
147 | +297 | ! |
- } else if (is.element(make.names(snapshot_name), make.names(names(snapshot_history())))) {+ snapshot <- snapshot_history()[[s]] |
|
148 | +298 | ! |
- showNotification(+ snapshot_state <- as.teal_slices(snapshot) |
|
149 | +299 | ! |
- "This name is in conflict with other snapshot names. Please choose a different one.",+ slices_store(tss = snapshot_state, file = file) |
|
150 | -! | +|||
300 | +
- type = "message"+ } |
|||
151 | +301 |
- )+ ) |
||
152 | +302 | ! |
- updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name")+ handlers[[id_saveme]] <- id_saveme |
|
153 | +303 |
- } else {+ } |
||
154 | -! | +|||
304 | +
- snapshot <- as.list(slices_global(), recursive = TRUE)+ # Create a row for the snapshot table. |
|||
155 | +305 | ! |
- attr(snapshot, "mapping") <- matrix_to_mapping(mapping_matrix())+ if (!is.element(id_rowme, names(divs))) { |
|
156 | +306 | ! |
- snapshot_update <- c(snapshot_history(), list(snapshot))+ divs[[id_rowme]] <- div( |
|
157 | +307 | ! |
- names(snapshot_update)[length(snapshot_update)] <- snapshot_name+ class = "snapshot_table_row", |
|
158 | +308 | ! |
- snapshot_history(snapshot_update)+ span(h5(s)), |
|
159 | +309 | ! |
- removeModal()- |
- |
160 | -- |
- # Reopen filter manager modal by clicking button in the main application.+ actionLink(inputId = ns(id_pickme), label = icon("circle-check"), title = "select"), |
||
161 | +310 | ! |
- shinyjs::click(id = "teal-main_ui-filter_manager-show", asis = TRUE)+ downloadLink(outputId = ns(id_saveme), label = icon("save"), title = "save to file") |
|
162 | +311 |
- }+ ) |
||
163 | +312 |
- })+ } |
||
164 | +313 |
-
+ }) |
||
165 | +314 |
- # Upload a snapshot file ----+ }) |
||
166 | +315 |
- # Select file.- |
- ||
167 | -6x | -
- observeEvent(input$snapshot_load, {+ |
||
168 | -! | +|||
316 | +
- showModal(+ # Create table to display list of snapshots and their actions. |
|||
169 | -! | +|||
317 | +6x |
- modalDialog(+ output$snapshot_list <- renderUI({ |
||
170 | -! | +|||
318 | +2x |
- fileInput(ns("snapshot_file"), "Choose snapshot file", accept = ".json", width = "100%"),+ rows <- lapply(rev(reactiveValuesToList(divs)), function(d) d) |
||
171 | -! | +|||
319 | +2x |
- textInput(+ if (length(rows) == 0L) { |
||
172 | -! | +|||
320 | +2x |
- ns("snapshot_name"),+ div( |
||
173 | -! | +|||
321 | +2x |
- "Name the snapshot (optional)",+ class = "snapshot_manager_placeholder", |
||
174 | -! | +|||
322 | +2x |
- width = "100%",+ "Snapshots will appear here." |
||
175 | -! | +|||
323 | +
- placeholder = "Meaningful, unique name"+ ) |
|||
176 | +324 |
- ),+ } else { |
||
177 | +325 | ! |
- footer = tagList(+ rows |
|
178 | -! | +|||
326 | +
- actionButton(ns("snaphot_file_accept"), "Accept", icon = icon("thumbs-up")),+ } |
|||
179 | -! | +|||
327 | +
- modalButton(label = "Cancel", icon = icon("thumbs-down"))+ }) |
|||
180 | +328 |
- )+ }) |
||
181 | +329 |
- )+ } |
||
182 | +330 |
- )+ |
||
183 | +331 |
- })+ |
||
184 | +332 |
- # Store new snapshot to list and restore filter states.+ |
||
185 | -6x | +|||
333 | +
- observeEvent(input$snaphot_file_accept, {+ |
|||
186 | -! | +|||
334 | +
- snapshot_name <- trimws(input$snapshot_name)+ ### utility functions ---- |
|||
187 | -! | +|||
335 | +
- if (identical(snapshot_name, "")) {+ |
|||
188 | -! | +|||
336 | +
- snapshot_name <- tools::file_path_sans_ext(input$snapshot_file$name)+ #' Explicitly enumerate global filters. |
|||
189 | +337 |
- }+ #' |
||
190 | -! | +|||
338 | +
- if (is.element(make.names(snapshot_name), make.names(names(snapshot_history())))) {+ #' Transform module mapping such that global filters are explicitly specified for every module. |
|||
191 | -! | +|||
339 | +
- showNotification(+ #' |
|||
192 | -! | +|||
340 | +
- "This name is in conflict with other snapshot names. Please choose a different one.",+ #' @param mapping (`named list`) as stored in mapping parameter of `teal_slices` |
|||
193 | -! | +|||
341 | +
- type = "message"+ #' @param module_names (`character`) vector containing names of all modules in the app |
|||
194 | +342 |
- )+ #' @return A `named_list` with one element per module, each element containing all filters applied to that module. |
||
195 | -! | +|||
343 | +
- updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name")+ #' @keywords internal |
|||
196 | +344 |
- } else {+ #' |
||
197 | +345 |
- # Restore snapshot and verify app compatibility.+ unfold_mapping <- function(mapping, module_names) { |
||
198 | +346 | ! |
- snapshot_state <- try(slices_restore(input$snapshot_file$datapath))+ module_names <- structure(module_names, names = module_names) |
|
199 | +347 | ! |
- if (!inherits(snapshot_state, "modules_teal_slices")) {+ lapply(module_names, function(x) c(mapping[[x]], mapping[["global_filters"]])) |
|
200 | -! | +|||
348 | +
- showNotification(+ } |
|||
201 | -! | +|||
349 | +
- "File appears to be corrupt.",+ |
|||
202 | -! | +|||
350 | +
- type = "error"+ #' Convert mapping matrix to filter mapping specification. |
|||
203 | +351 |
- )- |
- ||
204 | -! | -
- } else if (!identical(attr(snapshot_state, "app_id"), attr(slices_global(), "app_id"))) {+ #' |
||
205 | -! | +|||
352 | +
- showNotification(+ #' Transform a mapping matrix, i.e. a data frame that maps each filter state to each module, |
|||
206 | -! | +|||
353 | +
- "This snapshot file is not compatible with the app and cannot be loaded.",+ #' to a list specification like the one used in the `mapping` attribute of `teal_slices`. |
|||
207 | -! | +|||
354 | +
- type = "warning"+ #' Global filters are gathered in one list element. |
|||
208 | +355 |
- )+ #' If a module has no active filters but the global ones, it will not be mentioned in the output. |
||
209 | +356 |
- } else {+ #' |
||
210 | +357 |
- # Add to snapshot history.+ #' @param mapping_matrix (`data.frame`) of logical vectors where |
||
211 | -! | +|||
358 | +
- snapshot <- as.list(snapshot_state, recursive = TRUE)+ #' columns represent modules and row represent `teal_slice`s |
|||
212 | -! | +|||
359 | +
- snapshot_update <- c(snapshot_history(), list(snapshot))+ #' @return `named list` like that in the `mapping` attribute of a `teal_slices` object. |
|||
213 | -! | +|||
360 | +
- names(snapshot_update)[length(snapshot_update)] <- snapshot_name+ #' @keywords internal |
|||
214 | -! | +|||
361 | +
- snapshot_history(snapshot_update)+ #' |
|||
215 | +362 |
- ### Begin simplified restore procedure. ###+ matrix_to_mapping <- function(mapping_matrix) { |
||
216 | +363 | ! |
- mapping_unfolded <- unfold_mapping(attr(snapshot_state, "mapping"), names(filtered_data_list))+ mapping_matrix[] <- lapply(mapping_matrix, function(x) x | is.na(x)) |
|
217 | +364 | ! |
- mapply(+ global <- vapply(as.data.frame(t(mapping_matrix)), all, logical(1L)) |
|
218 | +365 | ! |
- function(filtered_data, filter_ids) {+ global_filters <- names(global[global]) |
|
219 | +366 | ! |
- filtered_data$clear_filter_states(force = TRUE)+ local_filters <- mapping_matrix[!rownames(mapping_matrix) %in% global_filters, ]+ |
+ |
367 | ++ | + | ||
220 | +368 | ! |
- slices <- Filter(function(x) x$id %in% filter_ids, snapshot_state)+ mapping <- c(lapply(local_filters, function(x) rownames(local_filters)[x]), list(global_filters = global_filters)) |
|
221 | +369 | ! |
- filtered_data$set_filter_state(slices)+ Filter(function(x) length(x) != 0L, mapping) |
|
222 | +370 |
- },+ } |
||
223 | -! | +
1 | +
- filtered_data = filtered_data_list,+ #' @title `TealReportCard` |
|||
224 | -! | +|||
2 | +
- filter_ids = mapping_unfolded+ #' @description `r lifecycle::badge("experimental")` |
|||
225 | +3 |
- )+ #' A child of [`ReportCard`] that is used for teal specific applications. |
||
226 | -! | +|||
4 | +
- slices_global(snapshot_state)+ #' In addition to the parent methods, it supports rendering teal specific elements such as |
|||
227 | -! | +|||
5 | +
- removeModal()+ #' the source code, the encodings panel content and the filter panel content as part of the |
|||
228 | +6 |
- ### End simplified restore procedure. ###+ #' meta data. |
||
229 | +7 |
- }+ #' @export |
||
230 | +8 |
- }+ #' |
||
231 | +9 |
- })+ TealReportCard <- R6::R6Class( # nolint: object_name_linter. |
||
232 | +10 |
- # Apply newly added snapshot.+ classname = "TealReportCard", |
||
233 | +11 |
-
+ inherit = teal.reporter::ReportCard, |
||
234 | +12 |
- # Restore initial state ----+ public = list( |
||
235 | -6x | +|||
13 | +
- observeEvent(input$snapshot_reset, {+ #' @description Appends the source code to the `content` meta data of this `TealReportCard`. |
|||
236 | -! | +|||
14 | +
- s <- "Initial application state"+ #' |
|||
237 | +15 |
- ### Begin restore procedure. ###+ #' @param src (`character(1)`) code as text. |
||
238 | -! | +|||
16 | +
- snapshot <- snapshot_history()[[s]]+ #' @param ... any `rmarkdown` R chunk parameter and its value. |
|||
239 | -! | +|||
17 | +
- snapshot_state <- as.teal_slices(snapshot)+ #' But `eval` parameter is always set to `FALSE`. |
|||
240 | -! | +|||
18 | +
- mapping_unfolded <- unfold_mapping(attr(snapshot_state, "mapping"), names(filtered_data_list))+ #' @return invisibly self |
|||
241 | -! | +|||
19 | +
- mapply(+ #' @examples |
|||
242 | -! | +|||
20 | +
- function(filtered_data, filter_ids) {+ #' card <- TealReportCard$new()$append_src( |
|||
243 | -! | +|||
21 | +
- filtered_data$clear_filter_states(force = TRUE)+ #' "plot(iris)" |
|||
244 | -! | +|||
22 | +
- slices <- Filter(function(x) x$id %in% filter_ids, snapshot_state)+ #' ) |
|||
245 | -! | +|||
23 | +
- filtered_data$set_filter_state(slices)+ #' card$get_content()[[1]]$get_content() |
|||
246 | +24 |
- },+ append_src = function(src, ...) { |
||
247 | -! | +|||
25 | +4x |
- filtered_data = filtered_data_list,+ checkmate::assert_character(src, min.len = 0, max.len = 1) |
||
248 | -! | +|||
26 | +4x |
- filter_ids = mapping_unfolded+ params <- list(...) |
||
249 | -+ | |||
27 | +4x |
- )+ params$eval <- FALSE |
||
250 | -! | +|||
28 | +4x |
- slices_global(snapshot_state)+ rblock <- RcodeBlock$new(src) |
||
251 | -! | +|||
29 | +4x |
- removeModal()+ rblock$set_params(params) |
||
252 | -+ | |||
30 | +4x |
- ### End restore procedure. ###+ self$append_content(rblock) |
||
253 | -+ | |||
31 | +4x |
- })+ self$append_metadata("SRC", src)+ |
+ ||
32 | +4x | +
+ invisible(self) |
||
254 | +33 |
-
+ }, |
||
255 | +34 |
- # Build snapshot table ----+ #' @description Appends the filter state list to the `content` and `metadata` of this `TealReportCard`. |
||
256 | +35 |
- # Create UI elements and server logic for the snapshot table.+ #' If the filter state list has an attribute named `formatted`, it appends it to the card otherwise it uses |
||
257 | +36 |
- # Observers must be tracked to avoid duplication and excess reactivity.+ #' the default `yaml::as.yaml` to format the list. |
||
258 | +37 |
- # Remaining elements are tracked likewise for consistency and a slight speed margin.+ #' If the filter state list is empty, nothing is appended to the `content`. |
||
259 | -6x | +|||
38 | +
- observers <- reactiveValues()+ #' |
|||
260 | -6x | +|||
39 | +
- handlers <- reactiveValues()+ #' @param fs (`teal_slices`) object returned from [teal_slices()] function. |
|||
261 | -6x | +|||
40 | +
- divs <- reactiveValues()+ #' @return invisibly self |
|||
262 | +41 |
-
+ append_fs = function(fs) { |
||
263 | -6x | +42 | +5x |
- observeEvent(snapshot_history(), {+ checkmate::assert_class(fs, "teal_slices") |
264 | -2x | +43 | +4x |
- lapply(names(snapshot_history())[-1L], function(s) {+ self$append_text("Filter State", "header3") |
265 | -! | +|||
44 | +4x |
- id_pickme <- sprintf("pickme_%s", make.names(s))+ self$append_content(TealSlicesBlock$new(fs)) |
||
266 | -! | +|||
45 | +4x |
- id_saveme <- sprintf("saveme_%s", make.names(s))+ invisible(self) |
||
267 | -! | +|||
46 | +
- id_rowme <- sprintf("rowme_%s", make.names(s))+ }, |
|||
268 | +47 |
-
+ #' @description Appends the encodings list to the `content` and `metadata` of this `TealReportCard`. |
||
269 | +48 |
- # Observer for restoring snapshot.+ #' |
||
270 | -! | +|||
49 | +
- if (!is.element(id_pickme, names(observers))) {+ #' @param encodings (`list`) list of encodings selections of the teal app. |
|||
271 | -! | +|||
50 | +
- observers[[id_pickme]] <- observeEvent(input[[id_pickme]], {+ #' @return invisibly self |
|||
272 | +51 |
- ### Begin restore procedure. ###+ #' @examples |
||
273 | -! | +|||
52 | +
- snapshot <- snapshot_history()[[s]]+ #' card <- TealReportCard$new()$append_encodings(list(variable1 = "X")) |
|||
274 | -! | +|||
53 | +
- snapshot_state <- as.teal_slices(snapshot)+ #' card$get_content()[[1]]$get_content() |
|||
275 | -! | +|||
54 | +
- mapping_unfolded <- unfold_mapping(attr(snapshot_state, "mapping"), names(filtered_data_list))+ #' |
|||
276 | -! | +|||
55 | +
- mapply(+ append_encodings = function(encodings) { |
|||
277 | -! | +|||
56 | +4x |
- function(filtered_data, filter_ids) {+ checkmate::assert_list(encodings) |
||
278 | -! | +|||
57 | +4x |
- filtered_data$clear_filter_states(force = TRUE)+ self$append_text("Selected Options", "header3") |
||
279 | -! | +|||
58 | +4x |
- slices <- Filter(function(x) x$id %in% filter_ids, snapshot_state)+ if (requireNamespace("yaml", quietly = TRUE)) { |
||
280 | -! | +|||
59 | +4x |
- filtered_data$set_filter_state(slices)+ self$append_text(yaml::as.yaml(encodings, handlers = list( |
||
281 | -+ | |||
60 | +4x |
- },+ POSIXct = function(x) format(x, "%Y-%m-%d"), |
||
282 | -! | +|||
61 | +4x |
- filtered_data = filtered_data_list,+ POSIXlt = function(x) format(x, "%Y-%m-%d"), |
||
283 | -! | +|||
62 | +4x |
- filter_ids = mapping_unfolded+ Date = function(x) format(x, "%Y-%m-%d")+ |
+ ||
63 | +4x | +
+ )), "verbatim") |
||
284 | +64 |
- )+ } else { |
||
285 | +65 | ! |
- slices_global(snapshot_state)+ stop("yaml package is required to format the encodings list") |
|
286 | -! | +|||
66 | +
- removeModal()+ }+ |
+ |||
67 | +4x | +
+ self$append_metadata("Encodings", encodings)+ |
+ ||
68 | +4x | +
+ invisible(self) |
||
287 | +69 |
- ### End restore procedure. ###+ } |
||
288 | +70 |
- })+ ), |
||
289 | +71 |
- }+ private = list() |
||
290 | +72 |
- # Create handler for downloading snapshot.+ ) |
||
291 | -! | +|||
73 | +
- if (!is.element(id_saveme, names(handlers))) {+ |
|||
292 | -! | +|||
74 | +
- output[[id_saveme]] <- downloadHandler(+ #' @title `RcodeBlock` |
|||
293 | -! | +|||
75 | +
- filename = function() {+ #' @keywords internal |
|||
294 | -! | +|||
76 | +
- sprintf("teal_snapshot_%s_%s.json", s, Sys.Date())+ TealSlicesBlock <- R6::R6Class( # nolint: object_name_linter. |
|||
295 | +77 |
- },+ classname = "TealSlicesBlock", |
||
296 | -! | +|||
78 | +
- content = function(file) {+ inherit = teal.reporter:::TextBlock, |
|||
297 | -! | +|||
79 | +
- snapshot <- snapshot_history()[[s]]+ public = list( |
|||
298 | -! | +|||
80 | +
- snapshot_state <- as.teal_slices(snapshot)+ #' @description Returns a `TealSlicesBlock` object. |
|||
299 | -! | +|||
81 | +
- slices_store(tss = snapshot_state, file = file)+ #' |
|||
300 | +82 |
- }+ #' @details Returns a `TealSlicesBlock` object with no content and no parameters. |
||
301 | +83 |
- )+ #' |
||
302 | -! | +|||
84 | +
- handlers[[id_saveme]] <- id_saveme+ #' @param content (`teal_slices`) object returned from [teal_slices()] function. |
|||
303 | +85 |
- }+ #' @param style (`character(1)`) string specifying style to apply. |
||
304 | +86 |
- # Create a row for the snapshot table.+ #' |
||
305 | -! | +|||
87 | +
- if (!is.element(id_rowme, names(divs))) {+ #' @return `TealSlicesBlock` |
|||
306 | -! | +|||
88 | +
- divs[[id_rowme]] <- div(+ #' @examples |
|||
307 | -! | +|||
89 | +
- class = "snapshot_table_row",+ #' block <- teal:::TealSlicesBlock$new() |
|||
308 | -! | +|||
90 | +
- span(h5(s)),+ #' |
|||
309 | -! | +|||
91 | +
- actionLink(inputId = ns(id_pickme), label = icon("circle-check"), title = "select"),+ initialize = function(content = teal_slices(), style = "verbatim") { |
|||
310 | -! | +|||
92 | +10x |
- downloadLink(outputId = ns(id_saveme), label = icon("save"), title = "save to file")+ self$set_content(content)+ |
+ ||
93 | +9x | +
+ self$set_style(style)+ |
+ ||
94 | +9x | +
+ invisible(self) |
||
311 | +95 |
- )+ }, |
||
312 | +96 |
- }+ |
||
313 | +97 |
- })+ #' @description Sets content of this `TealSlicesBlock`. |
||
314 | +98 |
- })+ #' Sets content as `YAML` text which represents a list generated from `teal_slices`. |
||
315 | +99 |
-
+ #' The list displays limited number of fields from `teal_slice` objects, but this list is |
||
316 | +100 |
- # Create table to display list of snapshots and their actions.+ #' sufficient to conclude which filters were applied. |
||
317 | -6x | +|||
101 | +
- output$snapshot_list <- renderUI({+ #' When `selected` field in `teal_slice` object is a range, then it is displayed as a "min"+ |
+ |||
102 | ++ |
+ #'+ |
+ ||
103 | ++ |
+ #'+ |
+ ||
104 | ++ |
+ #' @param content (`teal_slices`) object returned from [teal_slices()] function.+ |
+ ||
105 | ++ |
+ #' @return invisibly self+ |
+ ||
106 | ++ |
+ set_content = function(content) { |
||
318 | -2x | +107 | +11x |
- rows <- lapply(rev(reactiveValuesToList(divs)), function(d) d)+ checkmate::assert_class(content, "teal_slices") |
319 | -2x | +108 | +10x |
- if (length(rows) == 0L) {+ if (length(content) != 0) { |
320 | -2x | +109 | +7x |
- div(+ states_list <- lapply(content, function(x) { |
321 | -2x | +110 | +7x |
- class = "snapshot_manager_placeholder",+ x_list <- shiny::isolate(as.list(x)) |
322 | -2x | +111 | +7x |
- "Snapshots will appear here."+ if ( |
323 | -+ | |||
112 | +7x |
- )+ inherits(x_list$choices, c("integer", "numeric", "Date", "POSIXct", "POSIXlt")) &&+ |
+ ||
113 | +7x | +
+ length(x_list$choices) == 2 &&+ |
+ ||
114 | +7x | +
+ length(x_list$selected) == 2 |
||
324 | +115 |
- } else {+ ) { |
||
325 | +116 | ! |
- rows+ x_list$range <- paste(x_list$selected, collapse = " - ") |
|
326 | -+ | |||
117 | +! |
- }+ x_list["selected"] <- NULL |
||
327 | +118 |
- })+ } |
||
328 | -+ | |||
119 | +7x |
- })+ if (!is.null(x_list$arg)) { |
||
329 | -+ | |||
120 | +! |
- }+ x_list$arg <- if (x_list$arg == "subset") "Genes" else "Samples" |
||
330 | +121 |
-
+ } |
||
331 | +122 | |||
332 | -+ | |||
123 | +7x |
-
+ x_list <- x_list[ |
||
333 | -+ | |||
124 | +7x |
-
+ c("dataname", "varname", "experiment", "arg", "expr", "selected", "range", "keep_na", "keep_inf") |
||
334 | +125 |
- ### utility functions ----+ ] |
||
335 | -+ | |||
126 | +7x |
-
+ names(x_list) <- c( |
||
336 | -+ | |||
127 | +7x |
- #' Explicitly enumerate global filters.+ "Dataset name", "Variable name", "Experiment", "Filtering by", "Applied expression", |
||
337 | -+ | |||
128 | +7x |
- #'+ "Selected Values", "Selected range", "Include NA values", "Include Inf values" |
||
338 | +129 |
- #' Transform module mapping such that global filters are explicitly specified for every module.+ ) |
||
339 | +130 |
- #'+ |
||
340 | -+ | |||
131 | +7x |
- #' @param mapping (`named list`) as stored in mapping parameter of `teal_slices`+ Filter(Negate(is.null), x_list) |
||
341 | +132 |
- #' @param module_names (`character`) vector containing names of all modules in the app+ }) |
||
342 | +133 |
- #' @return A `named_list` with one element per module, each element containing all filters applied to that module.+ |
||
343 | -+ | |||
134 | +7x |
- #' @keywords internal+ if (requireNamespace("yaml", quietly = TRUE)) { |
||
344 | -+ | |||
135 | +7x |
- #'+ super$set_content(yaml::as.yaml(states_list)) |
||
345 | +136 |
- unfold_mapping <- function(mapping, module_names) {+ } else { |
||
346 | +137 | ! |
- module_names <- structure(module_names, names = module_names)+ stop("yaml package is required to format the filter state list") |
|
347 | -! | +|||
138 | +
- lapply(module_names, function(x) c(mapping[[x]], mapping[["global_filters"]]))+ } |
|||
348 | +139 |
- }+ } |
||
349 | -+ | |||
140 | +10x |
-
+ private$teal_slices <- content |
||
350 | -+ | |||
141 | +10x |
- #' Convert mapping matrix to filter mapping specification.+ invisible(self) |
||
351 | +142 |
- #'+ }, |
||
352 | +143 |
- #' Transform a mapping matrix, i.e. a data frame that maps each filter state to each module,+ #' @description Create the `RcodeBlock` from a list. |
||
353 | +144 |
- #' to a list specification like the one used in the `mapping` attribute of `teal_slices`.+ #' @param x `named list` with two fields `c("text", "params")`. |
||
354 | +145 |
- #' Global filters are gathered in one list element.+ #' Use the `get_available_params` method to get all possible parameters. |
||
355 | +146 |
- #' If a module has no active filters but the global ones, it will not be mentioned in the output.+ #' @return invisibly self |
||
356 | +147 |
- #'+ from_list = function(x) { |
||
357 | -+ | |||
148 | +1x |
- #' @param mapping_matrix (`data.frame`) of logical vectors where+ checkmate::assert_list(x)+ |
+ ||
149 | +1x | +
+ checkmate::assert_names(names(x), must.include = c("teal_slices"))+ |
+ ||
150 | +1x | +
+ self$set_content(x$teal_slices)+ |
+ ||
151 | +1x | +
+ invisible(self) |
||
358 | +152 |
- #' columns represent modules and row represent `teal_slice`s+ }, |
||
359 | +153 |
- #' @return `named list` like that in the `mapping` attribute of a `teal_slices` object.+ #' @description Convert the `RcodeBlock` to a list. |
||
360 | +154 |
- #' @keywords internal+ #' @return `named list` with a text and `params`. |
||
361 | +155 |
- #'+ |
||
362 | +156 |
- matrix_to_mapping <- function(mapping_matrix) {+ to_list = function() { |
||
363 | -! | +|||
157 | +2x |
- mapping_matrix[] <- lapply(mapping_matrix, function(x) x | is.na(x))+ list(teal_slices = private$teal_slices) |
||
364 | -! | +|||
158 | +
- global <- vapply(as.data.frame(t(mapping_matrix)), all, logical(1L))+ } |
|||
365 | -! | +|||
159 | +
- global_filters <- names(global[global])+ ), |
|||
366 | -! | +|||
160 | +
- local_filters <- mapping_matrix[!rownames(mapping_matrix) %in% global_filters, ]+ private = list( |
|||
367 | +161 |
-
+ style = "verbatim", |
||
368 | -! | +|||
162 | +
- mapping <- c(lapply(local_filters, function(x) rownames(local_filters)[x]), list(global_filters = global_filters))+ teal_slices = NULL # teal_slices |
|||
369 | -! | +|||
163 | +
- Filter(function(x) length(x) != 0L, mapping)+ ) |
|||
370 | +164 |
- }+ ) |
64 |
- title = NULL,+ title = build_app_title(), |
65 |
- header = tags$p(""),+ header = tags$p(), |
|||
66 |
- footer = tags$p("")) {+ footer = tags$p()) { |
|||
67 | 12x |
- if (checkmate::test_string(header)) {+ checkmate::assert_character(id, max.len = 1, any.missing = FALSE) |
||
68 | -! | +
- header <- tags$h1(header)+ |
||
69 | -+ | 12x |
- }+ checkmate::assert_multi_class(splash_ui, c("shiny.tag", "shiny.tag.list", "html")) |
|
70 | -12x | +
- if (checkmate::test_string(footer)) {+ |
||
71 | -! | +12x |
- footer <- tags$p(footer)+ if (is.character(title)) { |
|
72 | -+ | ! |
- }+ title <- build_app_title(title) |
|
73 | -12x | +
- checkmate::assert(+ } else { |
||
74 | 12x |
- checkmate::check_class(splash_ui, "shiny.tag"),+ validate_app_title_tag(title) |
||
75 | -12x | +
- checkmate::check_class(splash_ui, "shiny.tag.list"),+ } |
||
76 | -12x | +
- checkmate::check_class(splash_ui, "html")+ |
||
77 | -+ | 12x |
- )+ checkmate::assert( |
|
78 | 12x |
- checkmate::assert(+ .var.name = "header", |
||
79 | 12x |
- checkmate::check_class(header, "shiny.tag"),+ checkmate::check_string(header), |
||
80 | 12x |
- checkmate::check_class(header, "shiny.tag.list"),+ checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html")) |
||
81 | -12x | +
- checkmate::check_class(header, "html")+ ) |
||
82 | -+ | 12x |
- )+ if (checkmate::test_string(header)) { |
|
83 | -12x | +! |
- checkmate::assert(+ header <- tags$p(header) |
|
84 | -12x | +
- checkmate::check_class(footer, "shiny.tag"),+ } |
||
85 | -12x | +
- checkmate::check_class(footer, "shiny.tag.list"),+ |
||
86 | 12x |
- checkmate::check_class(footer, "html")+ checkmate::assert( |
||
87 | -+ | 12x |
- )+ .var.name = "footer", |
|
88 | -+ | 12x |
-
+ checkmate::check_string(footer), |
|
89 | 12x |
- ns <- NS(id)+ checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html")) |
||
90 |
- # Once the data is loaded, we will remove this element and add the real teal UI instead+ ) |
|||
91 | 12x |
- splash_ui <- div(+ if (checkmate::test_string(footer)) { |
||
92 | -+ | ! |
- # id so we can remove the splash screen once ready, which is the first child of this container+ footer <- tags$p(footer) |
|
93 | -12x | +
- id = ns("main_ui_container"),+ } |
||
94 |
- # we put it into a div, so it can easily be removed as a whole, also when it is a tagList (and not+ |
|||
95 | -+ | 12x |
- # just the first item of the tagList)+ ns <- NS(id) |
|
96 | -12x | +
- div(splash_ui)+ |
||
97 |
- )+ # Once the data is loaded, we will remove this element and add the real teal UI instead |
|||
98 | -+ | 12x |
-
+ splash_ui <- div( |
|
99 | + |
+ # id so we can remove the splash screen once ready, which is the first child of this container+ |
+ ||
100 | +12x | +
+ id = ns("main_ui_container"),+ |
+ ||
101 | ++ |
+ # we put it into a div, so it can easily be removed as a whole, also when it is a tagList (and not+ |
+ ||
102 | ++ |
+ # just the first item of the tagList)+ |
+ ||
103 | +12x | +
+ div(splash_ui)+ |
+ ||
104 | ++ |
+ )+ |
+ ||
105 | ++ | + + | +||
106 | +
# show busy icon when shiny session is busy computing stuff |
|||
100 | +107 |
# based on https://stackoverflow.com/questions/17325521/r-shiny-display-loading-message-while-function-is-running/22475216#22475216 #nolint |
||
101 | +108 | 12x |
shiny_busy_message_panel <- conditionalPanel( |
|
102 | +109 | 12x |
condition = "(($('html').hasClass('shiny-busy')) && (document.getElementById('shiny-notification-panel') == null))", # nolint |
|
103 | +110 | 12x |
div( |
|
104 | +111 | 12x |
icon("arrows-rotate", "spin fa-spin"), |
|
105 | +112 | 12x |
"Computing ...", |
|
106 | +113 |
# CSS defined in `custom.css` |
||
107 | +114 | 12x |
class = "shinybusymessage" |
|
108 | +115 |
) |
||
109 | +116 |
) |
||
110 | +117 | |||
111 | +118 | 12x |
res <- fluidPage( |
|
112 | +119 | 12x |
title = title, |
|
113 | +120 | 12x |
theme = get_teal_bs_theme(), |
|
114 | +121 | 12x |
include_teal_css_js(), |
|
115 | +122 | 12x |
tags$header(header), |
|
116 | +123 | 12x |
tags$hr(class = "my-2"), |
|
117 | +124 | 12x |
shiny_busy_message_panel, |
|
118 | +125 | 12x |
splash_ui, |
|
119 | +126 | 12x |
tags$hr(), |
|
120 | +127 | 12x |
tags$footer( |
|
121 | +128 | 12x |
div( |
|
122 | +129 | 12x |
footer, |
|
123 | +130 | 12x |
teal.widgets::verbatim_popup_ui(ns("sessionInfo"), "Session Info", type = "link"), |
|
124 | +131 | 12x |
textOutput(ns("identifier")) |
|
125 | +132 |
) |
||
126 | +133 |
) |
||
127 | +134 |
) |
||
128 | +135 | 12x |
return(res) |
|
129 | +136 |
} |
||
130 | +137 | |||
131 | +138 | |||
132 | +139 |
#' @rdname module_teal |
||
133 | +140 |
srv_teal <- function(id, modules, teal_data_rv, filter = teal_slices()) { |
||
134 | +141 | 19x |
stopifnot(is.reactive(teal_data_rv)) |
|
135 | +142 | 18x |
moduleServer(id, function(input, output, session) { |
|
136 | +143 | 18x |
logger::log_trace("srv_teal initializing the module.") |
|
137 | +144 | |||
138 | +145 | 18x |
output$identifier <- renderText( |
|
139 | +146 | 18x |
paste0("Pid:", Sys.getpid(), " Token:", substr(session$token, 25, 32)) |
|
140 | +147 |
) |
||
141 | +148 | |||
142 | +149 | 18x |
teal.widgets::verbatim_popup_srv( |
|
143 | +150 | 18x |
"sessionInfo", |
|
144 | +151 | 18x |
verbatim_content = utils::capture.output(utils::sessionInfo()), |
|
145 | +152 | 18x |
title = "SessionInfo" |
|
146 | +153 |
) |
||
147 | +154 | |||
148 | +155 |
# `JavaScript` code |
||
149 | +156 | 18x |
- run_js_files(files = "init.js") # `JavaScript` code to make the clipboard accessible+ run_js_files(files = "init.js") |
|
150 | +157 | ++ | + + | +|
158 |
# set timezone in shiny app |
|||
151 | +159 |
# timezone is set in the early beginning so it will be available also |
||
152 | +160 |
# for `DDL` and all shiny modules |
||
153 | +161 | 18x |
get_client_timezone(session$ns) |
|
154 | +162 | 18x |
observeEvent( |
|
155 | +163 | 18x |
eventExpr = input$timezone, |
|
156 | +164 | 18x |
once = TRUE, |
|
157 | +165 | 18x |
handlerExpr = { |
|
158 | +166 | ! |
session$userData$timezone <- input$timezone |
|
159 | +167 | ! |
logger::log_trace("srv_teal@1 Timezone set to client's timezone: { input$timezone }.") |
|
160 | +168 |
} |
||
161 | +169 |
) |
||
162 | +170 | |||
163 | +171 | 18x |
reporter <- teal.reporter::Reporter$new() |
|
164 | +172 | 18x |
if (is_arg_used(modules, "reporter") && length(extract_module(modules, "teal_module_previewer")) == 0) { |
|
165 | +173 | ! |
modules <- append_module(modules, reporter_previewer_module()) |
|
166 | +174 |
} |
||
167 | +175 | |||
168 | +176 | 18x |
env <- environment() |
|
169 | +177 | 18x |
datasets_reactive <- eventReactive(teal_data_rv(), { |
|
170 | +178 | 4x |
env$progress <- shiny::Progress$new(session) |
|
171 | +179 | 4x |
env$progress$set(0.25, message = "Setting data") |
|
172 | +180 | |||
173 | +181 |
# create a list of data following structure of the nested modules list structure. |
||
174 | +182 |
# Because it's easier to unpack modules and datasets when they follow the same nested structure. |
||
175 | +183 | 4x |
datasets_singleton <- teal_data_to_filtered_data(teal_data_rv()) |
|
176 | +184 | |||
177 | +185 |
# Singleton starts with only global filters active. |
||
178 | +186 | 4x |
filter_global <- Filter(function(x) x$id %in% attr(filter, "mapping")$global_filters, filter) |
|
179 | +187 | 4x |
datasets_singleton$set_filter_state(filter_global) |
|
180 | +188 | |||
181 | +189 | 4x |
module_datasets <- function(modules) { |
|
182 | +190 | 18x |
if (inherits(modules, "teal_modules")) { |
|
183 | +191 | 7x |
datasets <- lapply(modules$children, module_datasets) |
|
184 | +192 | 7x |
labels <- vapply(modules$children, `[[`, character(1), "label") |
|
185 | +193 | 7x |
names(datasets) <- labels |
|
186 | +194 | 7x |
datasets |
|
187 | +195 | 11x |
} else if (isTRUE(attr(filter, "module_specific"))) { |
|
188 | +196 |
# we should create FilteredData even if modules$datanames is null |
||
189 | +197 |
# null controls a display of filter panel but data should be still passed |
||
190 | +198 | 3x |
datanames <- if (is.null(modules$datanames) || modules$datanames == "all") { |
|
191 | +199 | 3x |
include_parent_datanames( |
|
192 | +200 | 3x |
teal_data_datanames(teal_data_rv()), |
|
193 | +201 | 3x |
teal.data::join_keys(teal_data_rv()) |
|
194 | +202 |
) |
||
195 | +203 |
} else { |
||
196 | +204 | ! |
modules$datanames |
|
197 | +205 |
} |
||
198 | +206 |
# todo: subset teal_data to datanames |
||
199 | +207 | 3x |
datasets_module <- teal_data_to_filtered_data(teal_data_rv(), datanames = datanames) |
|
200 | +208 | |||
201 | +209 |
# set initial filters |
||
202 | +210 |
# - filtering filters for this module |
||
203 | +211 | 3x |
slices <- Filter(x = filter, f = function(x) { |
|
204 | +212 | ! |
x$id %in% unique(unlist(attr(filter, "mapping")[c(modules$label, "global_filters")])) && |
|
205 | +213 | ! |
x$dataname %in% datanames |
|
206 | +214 |
}) |
||
207 | +215 | 3x |
include_varnames <- attr(slices, "include_varnames")[names(attr(slices, "include_varnames")) %in% datanames] |
|
208 | +216 | 3x |
exclude_varnames <- attr(slices, "exclude_varnames")[names(attr(slices, "exclude_varnames")) %in% datanames] |
|
209 | +217 | 3x |
slices$include_varnames <- include_varnames |
|
210 | +218 | 3x |
slices$exclude_varnames <- exclude_varnames |
|
211 | +219 | 3x |
datasets_module$set_filter_state(slices) |
|
212 | +220 | 3x |
datasets_module |
|
213 | +221 |
} else { |
||
214 | +222 | 8x |
datasets_singleton |
|
215 | +223 |
} |
||
216 | +224 |
} |
||
217 | +225 | 4x |
module_datasets(modules) |
|
218 | +226 |
}) |
||
219 | +227 | |||
220 | +228 |
# Replace splash / welcome screen once data is loaded ---- |
||
221 | +229 |
# ignoreNULL to not trigger at the beginning when data is NULL |
||
222 | +230 |
# just handle it once because data obtained through delayed loading should |
||
223 | +231 |
# usually not change afterwards |
||
224 | +232 |
# if restored from bookmarked state, `filter` is ignored |
||
225 | +233 | |||
226 | +234 | 18x |
observeEvent(datasets_reactive(), once = TRUE, { |
|
227 | +235 | ! |
logger::log_trace("srv_teal@5 setting main ui after data was pulled") |
|
228 | +236 | ! |
on.exit(env$progress$close()) |
|
229 | +237 | ! |
env$progress$set(0.5, message = "Setting up main UI") |
|
230 | +238 | ! |
datasets <- datasets_reactive() |
|
231 | +239 | |||
232 | +240 |
# main_ui_container contains splash screen first and we remove it and replace it by the real UI |
||
233 | +241 | ! |
removeUI(sprintf("#%s > div:nth-child(1)", session$ns("main_ui_container"))) |
|
234 | +242 | ! |
insertUI( |
|
235 | +243 | ! |
selector = paste0("#", session$ns("main_ui_container")), |
|
236 | +244 | ! |
where = "beforeEnd", |
|
237 | +245 |
# we put it into a div, so it can easily be removed as a whole, also when it is a tagList (and not |
||
238 | +246 |
# just the first item of the tagList) |
||
239 | +247 | ! |
ui = div(ui_tabs_with_filters( |
|
240 | +248 | ! |
session$ns("main_ui"), |
|
241 | +249 | ! |
modules = modules, |
|
242 | +250 | ! |
datasets = datasets, |
|
243 | +251 | ! |
filter = filter |
|
244 | +252 |
)), |
||
245 | +253 |
# needed so that the UI inputs are available and can be immediately updated, otherwise, updating may not |
||
246 | +254 |
# have any effect as they are ignored when not present |
||
247 | +255 | ! |
immediate = TRUE |
|
248 | +256 |
) |
||
249 | +257 | |||
250 | +258 |
# must make sure that this is only executed once as modules assume their observers are only |
||
251 | +259 |
# registered once (calling server functions twice would trigger observers twice each time) |
||
252 | +260 | ! |
active_module <- srv_tabs_with_filters( |
|
253 | +261 | ! |
id = "main_ui", |
|
254 | +262 | ! |
datasets = datasets, |
|
255 | +263 | ! |
modules = modules, |
|
256 | +264 | ! |
reporter = reporter, |
|
257 | +265 | ! |
filter = filter |
|
258 | +266 |
) |
||
259 | +267 | ! |
return(active_module) |
|
260 | +268 |
}) |
||
261 | +269 |
}) |
||
262 | +270 |
}@@ -9003,1830 +9024,1782 @@ teal coverage - 63.82% |
1 |
- #' Store teal_slices object to a file+ # 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 doc is quite large |
||
3 |
- #' This function takes a `teal_slices` object and saves it to a file in `JSON` format.+ # and it is very end-user oriented. It may also perform more argument checking with more informative |
||
4 |
- #' The `teal_slices` object contains information about filter states and can be used to+ # error messages. |
||
5 |
- #' create, modify, and delete filter states. The saved file can be later loaded using+ |
||
6 |
- #' the `slices_restore` function.+ |
||
7 |
- #'+ #' Create the Server and UI Function For the Shiny App |
||
8 |
- #' @param tss (`teal_slices`) object to be stored.+ #' |
||
9 |
- #' @param file (`character(1)`) The file path where `teal_slices` object will be saved.+ #' @description `r lifecycle::badge("stable")` |
||
10 |
- #' The file extension should be `".json"`.+ #' End-users: This is the most important function for you to start a |
||
11 |
- #'+ #' teal app that is composed out of teal modules. |
||
12 |
- #' @details `Date` class is stored in `"ISO8601"` format (`YYYY-MM-DD`). `POSIX*t` classes are converted to a+ #' |
||
13 |
- #' character by using `format.POSIX*t(usetz = TRUE, tz = "UTC")` (`YYYY-MM-DD {N}{N}:{N}{N}:{N}{N} UTC`, where+ #' @details |
||
14 |
- #' `{N} = [0-9]` is a number and `UTC` is `Coordinated Universal Time` timezone short-code).+ #' When initializing the `teal` app, if `datanames` are not set for the `teal_data` object, |
||
15 |
- #' This format is assumed during `slices_restore`. All `POSIX*t` objects in `selected` or `choices` fields of+ #' defaults from the `teal_data` environment will be used. |
||
16 |
- #' `teal_slice` objects are always printed in `UTC` timezone as well.+ #' |
||
17 |
- #'+ #' @param data (`teal_data`, `teal_data_module`, `named list`)\cr |
||
18 |
- #' @return `NULL`, invisibly.+ #' `teal_data` object as returned by [teal.data::teal_data()] or |
||
19 |
- #'+ #' `teal_data_module` or simply a list of a named list of objects |
||
20 |
- #' @keywords internal+ #' (`data.frame` or `MultiAssayExperiment`). |
||
21 |
- #'+ #' @param modules (`list`, `teal_modules` or `teal_module`)\cr |
||
22 |
- #' @examples+ #' nested list of `teal_modules` or `teal_module` objects or a single |
||
23 |
- #' # Create a teal_slices object+ #' `teal_modules` or `teal_module` object. These are the specific output modules which |
||
24 |
- #' tss <- teal_slices(+ #' will be displayed in the teal application. See [modules()] and [module()] for |
||
25 |
- #' teal_slice(dataname = "data", varname = "var"),+ #' more details. |
||
26 |
- #' teal_slice(dataname = "data", expr = "x > 0", id = "positive_x", title = "Positive x")+ #' @param title (`shiny.tag` or `character(1)`)\cr |
||
27 |
- #' )+ #' The browser window title. Defaults to a title "teal app" with the icon of NEST. |
||
28 |
- #'+ #' Can be created using the `build_app_title()` or |
||
29 |
- #' if (interactive()) {+ #' by passing a valid `shiny.tag` which is a head tag with title and link tag. |
||
30 |
- #' # Store the teal_slices object to a file+ #' @param filter (`teal_slices`)\cr |
||
31 |
- #' slices_store(tss, "path/to/file.json")+ #' Specification of initial filter. Filters can be specified using [teal::teal_slices()]. |
||
32 |
- #' }+ #' Old way of specifying filters through a list is deprecated and will be removed in the |
||
33 |
- #'+ #' next release. Please fix your applications to use [teal::teal_slices()]. |
||
34 |
- slices_store <- function(tss, file) {+ #' @param header (`shiny.tag` or `character(1)`) \cr |
||
35 | -9x | +
- checkmate::assert_class(tss, "teal_slices")+ #' The header of the app. |
|
36 | -9x | +
- checkmate::assert_path_for_output(file, overwrite = TRUE, extension = "json")+ #' @param footer (`shiny.tag` or `character(1)`)\cr |
|
37 |
-
+ #' The footer of the app. |
||
38 | -9x | +
- cat(format(tss, trim_lines = FALSE), "\n", file = file)+ #' @param id (`character`)\cr |
|
39 |
- }+ #' module id to embed it, if provided, |
||
40 |
-
+ #' the server function must be called with [shiny::moduleServer()]; |
||
41 |
- #' Restore teal_slices object from a file+ #' See the vignette for an example. However, [ui_teal_with_splash()] |
||
42 |
- #'+ #' is then preferred to this function. |
||
43 |
- #' This function takes a file path to a `JSON` file containing a `teal_slices` object+ #' |
||
44 |
- #' and restores it to its original form. The restored `teal_slices` object can be used+ #' @return named list with `server` and `ui` function |
||
45 |
- #' to access filter states and their corresponding attributes.+ #' |
||
46 |
- #'+ #' @export |
||
47 |
- #' @param file Path to file where `teal_slices` is stored. Must have a `.json` extension and read access.+ #' |
||
48 |
- #'+ #' @include modules.R |
||
49 |
- #' @return A `teal_slices` object restored from the file.+ #' |
||
50 |
- #'+ #' @examples |
||
51 |
- #' @keywords internal+ #' app <- init( |
||
52 |
- #'+ #' data = teal_data( |
||
53 |
- #' @examples+ #' new_iris = transform(iris, id = seq_len(nrow(iris))), |
||
54 |
- #' if (interactive()) {+ #' new_mtcars = transform(mtcars, id = seq_len(nrow(mtcars))), |
||
55 |
- #' # Restore a teal_slices object from a file+ #' code = " |
||
56 |
- #' tss_restored <- slices_restore("path/to/file.json")+ #' new_iris <- transform(iris, id = seq_len(nrow(iris))) |
||
57 |
- #' }+ #' new_mtcars <- transform(mtcars, id = seq_len(nrow(mtcars))) |
||
58 |
- #'+ #' " |
||
59 |
- slices_restore <- function(file) {+ #' ), |
||
60 | -9x | +
- checkmate::assert_file_exists(file, access = "r", extension = "json")+ #' modules = modules( |
|
61 |
-
+ #' module( |
||
62 | -9x | +
- tss_json <- jsonlite::fromJSON(file, simplifyDataFrame = FALSE)+ #' label = "data source", |
|
63 | -9x | +
- tss_json$slices <-+ #' server = function(input, output, session, data) {}, |
|
64 | -9x | +
- lapply(tss_json$slices, function(slice) {+ #' ui = function(id, ...) div(p("information about data source")), |
|
65 | -9x | +
- for (field in c("selected", "choices")) {+ #' datanames = "all" |
|
66 | -18x | +
- if (!is.null(slice[[field]])) {+ #' ), |
|
67 | -12x | +
- if (length(slice[[field]]) > 0) {+ #' example_module(label = "example teal module"), |
|
68 | -9x | +
- date_partial_regex <- "^[0-9]{4}-[0-9]{2}-[0-9]{2}"+ #' module( |
|
69 | -9x | +
- time_stamp_regex <- paste0(date_partial_regex, "\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\sUTC$")+ #' "Iris Sepal.Length histogram", |
|
70 |
-
+ #' server = function(input, output, session, data) { |
||
71 | -9x | +
- slice[[field]] <-+ #' output$hist <- renderPlot( |
|
72 | -9x | +
- if (all(grepl(paste0(date_partial_regex, "$"), slice[[field]]))) {+ #' hist(data()[["new_iris"]]$Sepal.Length) |
|
73 | -3x | +
- as.Date(slice[[field]])+ #' ) |
|
74 | -9x | +
- } else if (all(grepl(time_stamp_regex, slice[[field]]))) {+ #' }, |
|
75 | -3x | +
- as.POSIXct(slice[[field]], tz = "UTC")+ #' ui = function(id, ...) { |
|
76 |
- } else {+ #' ns <- NS(id) |
||
77 | -3x | +
- slice[[field]]+ #' plotOutput(ns("hist")) |
|
78 |
- }+ #' }, |
||
79 |
- } else {+ #' datanames = "new_iris" |
||
80 | -3x | +
- slice[[field]] <- character(0)+ #' ) |
|
81 |
- }+ #' ), |
||
82 |
- }+ #' title = "App title", |
||
83 |
- }+ #' filter = teal_slices( |
||
84 | -9x | +
- slice+ #' teal_slice(dataname = "new_iris", varname = "Species"), |
|
85 |
- })+ #' teal_slice(dataname = "new_iris", varname = "Sepal.Length"), |
||
86 |
-
+ #' teal_slice(dataname = "new_mtcars", varname = "cyl"), |
||
87 | -9x | +
- tss_elements <- lapply(tss_json$slices, as.teal_slice)+ #' exclude_varnames = list(new_iris = c("Sepal.Width", "Petal.Width")), |
|
88 |
-
+ #' mapping = list( |
||
89 | -9x | +
- do.call(teal_slices, c(tss_elements, tss_json$attributes))+ #' `example teal module` = "new_iris Species", |
|
90 |
- }+ #' `Iris Sepal.Length histogram` = "new_iris Species", |
1 | +91 |
- #' Filter settings for teal applications+ #' global_filters = "new_mtcars cyl" |
||
2 | +92 |
- #'+ #' ) |
||
3 | +93 |
- #' Specify initial filter states and filtering settings for a `teal` app.+ #' ), |
||
4 | +94 |
- #'+ #' header = tags$h1("Sample App"), |
||
5 | +95 |
- #' Produces a `teal_slices` object.+ #' footer = tags$p("Copyright 2017 - 2023") |
||
6 | +96 |
- #' The `teal_slice` components will specify filter states that will be active when the app starts.+ #' ) |
||
7 | +97 |
- #' Attributes (created with the named arguments) will configure the way the app applies filters.+ #' if (interactive()) { |
||
8 | +98 |
- #' See argument descriptions for details.+ #' shinyApp(app$ui, app$server) |
||
9 | +99 |
- #'+ #' } |
||
10 | +100 |
- #' @inheritParams teal.slice::teal_slices+ #' |
||
11 | +101 |
- #'+ init <- function(data, |
||
12 | +102 |
- #' @param module_specific optional (`logical(1)`)\cr+ modules, |
||
13 | +103 |
- #' - `FALSE` (default) when one filter panel applied to all modules.+ title = build_app_title(), |
||
14 | +104 |
- #' All filters will be shared by all modules.+ filter = teal_slices(), |
||
15 | +105 |
- #' - `TRUE` when filter panel module-specific.+ header = tags$p(), |
||
16 | +106 |
- #' Modules can have different set of filters specified - see `mapping` argument.+ footer = tags$p(), |
||
17 | +107 |
- #' @param mapping `r lifecycle::badge("experimental")` _This is a new feature. Do kindly share your opinions.\cr_+ id = character(0)) { |
||
18 | -+ | |||
108 | +15x |
- #' (`named list`)\cr+ logger::log_trace("init initializing teal app with: data ('{ class(data) }').") |
||
19 | +109 |
- #' Specifies which filters will be active in which modules on app start.+ |
||
20 | +110 |
- #' Elements should contain character vector of `teal_slice` `id`s (see [teal.slice::teal_slice()]).+ # argument checking (independent) |
||
21 | +111 |
- #' Names of the list should correspond to `teal_module` `label` set in [module()] function.+ ## `data` |
||
22 | -+ | |||
112 | +15x |
- #' `id`s listed under `"global_filters` will be active in all modules.+ if (inherits(data, "TealData")) { |
||
23 | -+ | |||
113 | +! |
- #' If missing, all filters will be applied to all modules.+ lifecycle::deprecate_stop( |
||
24 | -+ | |||
114 | +! |
- #' If empty list, all filters will be available to all modules but will start inactive.+ when = "0.99.0", |
||
25 | -- |
- #' If `module_specific` is `FALSE`, only `global_filters` will be active on start.+ | ||
115 | +! | +
+ what = "init(data)", |
||
26 | -+ | |||
116 | +! |
- #' @param app_id (`character(1)`)\cr+ paste( |
||
27 | -+ | |||
117 | +! |
- #' For internal use only, do not set manually.+ "TealData is no longer supported. Use teal_data() instead.", |
||
28 | -+ | |||
118 | +! |
- #' Added by `init` so that a `teal_slices` can be matched to the app in which it was used.+ "Please follow migration instructions https://github.com/insightsengineering/teal/discussions/988." |
||
29 | +119 |
- #' Used for verifying snapshots uploaded from file. See `snapshot`.+ ) |
||
30 | +120 |
- #'+ ) |
||
31 | +121 |
- #' @param x (`list`) of lists to convert to `teal_slices`+ } |
||
32 | -+ | |||
122 | +15x |
- #'+ checkmate::assert( |
||
33 | -+ | |||
123 | +15x |
- #' @return+ .var.name = "data", |
||
34 | -+ | |||
124 | +15x |
- #' A `teal_slices` object.+ checkmate::check_multi_class(data, c("teal_data", "teal_data_module")), |
||
35 | -+ | |||
125 | +15x |
- #'+ checkmate::check_list(data, names = "named") |
||
36 | +126 |
- #' @seealso [`teal.slice::teal_slices`], [`teal.slice::teal_slice`], [`slices_store`]+ ) |
||
37 | -+ | |||
127 | +15x |
- #'+ if (is.list(data) && !inherits(data, "teal_data_module")) { |
||
38 | -+ | |||
128 | +10x |
- #' @examples+ data <- do.call(teal.data::teal_data, data) |
||
39 | +129 |
- #' filter <- teal_slices(+ } |
||
40 | +130 |
- #' teal.slice::teal_slice(dataname = "iris", varname = "Species", id = "species"),+ |
||
41 | +131 |
- #' teal.slice::teal_slice(dataname = "iris", varname = "Sepal.Length", id = "sepal_length"),+ ## `modules` |
||
42 | -+ | |||
132 | +15x |
- #' teal.slice::teal_slice(+ checkmate::assert( |
||
43 | -+ | |||
133 | +15x |
- #' dataname = "iris", id = "long_petals", title = "Long petals", expr = "Petal.Length > 5"+ .var.name = "modules", |
||
44 | -+ | |||
134 | +15x |
- #' ),+ checkmate::check_multi_class(modules, c("teal_modules", "teal_module")), |
||
45 | -+ | |||
135 | +15x |
- #' teal.slice::teal_slice(dataname = "mtcars", varname = "mpg", id = "mtcars_mpg"),+ checkmate::check_list(modules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules")) |
||
46 | +136 |
- #' mapping = list(+ ) |
||
47 | -+ | |||
137 | +15x |
- #' module1 = c("species", "sepal_length"),+ if (inherits(modules, "teal_module")) { |
||
48 | -+ | |||
138 | +1x |
- #' module2 = c("mtcars_mpg"),+ modules <- list(modules) |
||
49 | +139 |
- #' global_filters = "long_petals"+ } |
||
50 | -+ | |||
140 | +15x |
- #' )+ if (checkmate::test_list(modules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules"))) { |
||
51 | -+ | |||
141 | +4x |
- #' )+ modules <- do.call(teal::modules, modules) |
||
52 | +142 |
- #'+ } |
||
53 | +143 |
- #' app <- teal::init(+ |
||
54 | +144 |
- #' data = list(iris = iris, mtcars = mtcars),+ ## `filter` |
||
55 | -+ | |||
145 | +15x |
- #' modules = list(+ checkmate::assert( |
||
56 | -+ | |||
146 | +15x |
- #' module("module1"),+ .var.name = "filter", |
||
57 | -+ | |||
147 | +15x |
- #' module("module2")+ checkmate::check_class(filter, "teal_slices"), |
||
58 | -+ | |||
148 | +15x |
- #' ),+ checkmate::check_list(filter, names = "named") |
||
59 | +149 |
- #' filter = filter+ ) |
||
60 | +150 |
- #' )+ |
||
61 | +151 |
- #'+ ## all other arguments |
||
62 | -+ | |||
152 | +14x |
- #' if (interactive()) {+ checkmate::assert( |
||
63 | -+ | |||
153 | +14x |
- #' shinyApp(app$ui, app$server)+ .var.name = "title", |
||
64 | -+ | |||
154 | +14x |
- #' }+ checkmate::check_string(title), |
||
65 | -+ | |||
155 | +14x |
- #'+ checkmate::check_multi_class(title, c("shiny.tag", "shiny.tag.list", "html")) |
||
66 | +156 |
- #' @export+ ) |
||
67 | -- |
- teal_slices <- function(...,- |
- ||
68 | -- |
- exclude_varnames = NULL,- |
- ||
69 | -- |
- include_varnames = NULL,- |
- ||
70 | -+ | |||
157 | +14x |
- count_type = NULL,+ checkmate::assert( |
||
71 | -+ | |||
158 | +14x |
- allow_add = TRUE,+ .var.name = "header", |
||
72 | -+ | |||
159 | +14x |
- module_specific = FALSE,+ checkmate::check_string(header), |
||
73 | -+ | |||
160 | +14x |
- mapping,+ checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html")) |
||
74 | +161 |
- app_id = NULL) {- |
- ||
75 | -77x | -
- shiny::isolate({+ ) |
||
76 | -77x | +162 | +14x |
- checkmate::assert_flag(allow_add)+ checkmate::assert( |
77 | -77x | +163 | +14x |
- checkmate::assert_flag(module_specific)+ .var.name = "footer", |
78 | -37x | +164 | +14x |
- if (!missing(mapping)) checkmate::assert_list(mapping, types = c("character", "NULL"), names = "named")+ checkmate::check_string(footer), |
79 | -74x | +165 | +14x |
- checkmate::assert_string(app_id, null.ok = TRUE)+ checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html")) |
80 | +166 | - - | -||
81 | -74x | -
- slices <- list(...)+ ) |
||
82 | -74x | +167 | +14x |
- all_slice_id <- vapply(slices, `[[`, character(1L), "id")+ checkmate::assert_character(id, max.len = 1, any.missing = FALSE) |
83 | +168 | |||
84 | -74x | -
- if (missing(mapping)) {- |
- ||
85 | -40x | -
- mapping <- list(global_filters = all_slice_id)- |
- ||
86 | +169 |
- }+ # log |
||
87 | -74x | +170 | +14x |
- if (!module_specific) {+ teal.logger::log_system_info() |
88 | -70x | +|||
171 | +
- mapping[setdiff(names(mapping), "global_filters")] <- NULL+ |
|||
89 | +172 |
- }+ # argument transformations |
||
90 | +173 |
-
+ ## `modules` - landing module |
||
91 | -74x | +174 | +14x |
- failed_slice_id <- setdiff(unlist(mapping), all_slice_id)+ landing <- extract_module(modules, "teal_module_landing") |
92 | -74x | +175 | +14x |
- if (length(failed_slice_id)) {+ landing_module <- NULL |
93 | -1x | +176 | +14x |
- stop(sprintf(+ if (length(landing) == 1L) { |
94 | -1x | +|||
177 | +! |
- "Filters in mapping don't match any available filter.\n %s not in %s",+ landing_module <- landing[[1L]] |
||
95 | -1x | +|||
178 | +! |
- toString(failed_slice_id),+ modules <- drop_module(modules, "teal_module_landing") |
||
96 | -1x | +179 | +14x |
- toString(all_slice_id)+ } else if (length(landing) > 1L) {+ |
+
180 | +! | +
+ stop("Only one `landing_popup_module` can be used.") |
||
97 | +181 |
- ))+ } |
||
98 | +182 |
- }+ |
||
99 | +183 |
-
+ ## `filter` - app_id attribute |
||
100 | -73x | +184 | +14x |
- tss <- teal.slice::teal_slices(+ attr(filter, "app_id") <- create_app_id(data, modules) |
101 | +185 |
- ...,+ |
||
102 | -73x | +|||
186 | +
- exclude_varnames = exclude_varnames,+ ## `filter` - convert teal.slice::teal_slices to teal::teal_slices |
|||
103 | -73x | +187 | +14x |
- include_varnames = include_varnames,+ filter <- as.teal_slices(as.list(filter)) |
104 | -73x | +|||
188 | +
- count_type = count_type,+ |
|||
105 | -73x | +|||
189 | +
- allow_add = allow_add+ # argument checking (interdependent) |
|||
106 | +190 |
- )+ ## `filter` - `modules` |
||
107 | -73x | +191 | +14x |
- attr(tss, "mapping") <- mapping+ if (isTRUE(attr(filter, "module_specific"))) { |
108 | -73x | +|||
192 | +! |
- attr(tss, "module_specific") <- module_specific+ module_names <- unlist(c(module_labels(modules), "global_filters")) |
||
109 | -73x | +|||
193 | +! |
- attr(tss, "app_id") <- app_id+ failed_mod_names <- setdiff(names(attr(filter, "mapping")), module_names) |
||
110 | -73x | +|||
194 | +! |
- class(tss) <- c("modules_teal_slices", class(tss))+ if (length(failed_mod_names)) { |
||
111 | -73x | +|||
195 | +! |
- tss+ stop( |
||
112 | -+ | |||
196 | +! |
- })+ sprintf( |
||
113 | -+ | |||
197 | +! |
- }+ "Some module names in the mapping arguments don't match module labels.\n %s not in %s", |
||
114 | -+ | |||
198 | +! |
-
+ toString(failed_mod_names), |
||
115 | -+ | |||
199 | +! |
-
+ toString(unique(module_names)) |
||
116 | +200 |
- #' @rdname teal_slices+ ) |
||
117 | +201 |
- #' @export+ ) |
||
118 | +202 |
- #' @keywords internal+ } |
||
119 | +203 |
- #'+ |
||
120 | -+ | |||
204 | +! |
- as.teal_slices <- function(x) { # nolint+ if (anyDuplicated(module_names)) { |
||
121 | -15x | +|||
205 | +
- checkmate::assert_list(x)+ # In teal we are able to set nested modules with duplicated label. |
|||
122 | -15x | +|||
206 | +
- lapply(x, checkmate::assert_list, names = "named", .var.name = "list element")+ # Because mapping argument bases on the relationship between module-label and filter-id, |
|||
123 | +207 |
-
+ # it is possible that module-label in mapping might refer to multiple teal_module (identified by the same label) |
||
124 | -15x | +|||
208 | +! |
- attrs <- attributes(unclass(x))+ stop( |
||
125 | -15x | +|||
209 | +! |
- ans <- lapply(x, function(x) if (is.teal_slice(x)) x else as.teal_slice(x))+ sprintf( |
||
126 | -15x | +|||
210 | +! |
- do.call(teal_slices, c(ans, attrs))+ "Module labels should be unique when teal_slices(mapping = TRUE). Duplicated labels:\n%s ", |
||
127 | -+ | |||
211 | +! |
- }+ toString(module_names[duplicated(module_names)]) |
||
128 | +212 |
-
+ ) |
||
129 | +213 |
-
+ ) |
||
130 | +214 |
- #' @rdname teal_slices+ } |
||
131 | +215 |
- #' @export+ } |
||
132 | +216 |
- #' @keywords internal+ |
||
133 | +217 |
- #'+ ## `data` - `modules` |
||
134 | -+ | |||
218 | +14x |
- c.teal_slices <- function(...) {+ if (inherits(data, "teal_data")) { |
||
135 | -! | +|||
219 | +13x |
- x <- list(...)+ if (length(teal_data_datanames(data)) == 0) { |
||
136 | -! | +|||
220 | +1x |
- checkmate::assert_true(all(vapply(x, is.teal_slices, logical(1L))), .var.name = "all arguments are teal_slices")+ stop("`data` object has no datanames and its environment is empty. Specify `datanames(data)` and try again.") |
||
137 | +221 |
-
+ } |
||
138 | -! | +|||
222 | +
- all_attributes <- lapply(x, attributes)+ # in case of teal_data_module this check is postponed to the srv_teal_with_splash |
|||
139 | -! | +|||
223 | +12x |
- all_attributes <- coalesce_r(all_attributes)+ is_modules_ok <- check_modules_datanames(modules, teal_data_datanames(data)) |
||
140 | -! | +|||
224 | +12x |
- all_attributes <- all_attributes[names(all_attributes) != "class"]+ if (!isTRUE(is_modules_ok)) { |
||
141 | -+ | |||
225 | +1x |
-
+ logger::log_error(is_modules_ok) |
||
142 | -! | +|||
226 | +1x |
- do.call(+ checkmate::assert(is_modules_ok, .var.name = "modules") |
||
143 | -! | +|||
227 | +
- teal_slices,+ } |
|||
144 | -! | +|||
228 | +
- c(+ |
|||
145 | -! | +|||
229 | +11x |
- unique(unlist(x, recursive = FALSE)),+ is_filter_ok <- check_filter_datanames(filter, teal_data_datanames(data)) |
||
146 | -! | +|||
230 | +11x |
- all_attributes+ if (!isTRUE(is_filter_ok)) {+ |
+ ||
231 | +1x | +
+ warning(is_filter_ok) |
||
147 | +232 |
- )+ # we allow app to continue if applied filters are outside |
||
148 | +233 |
- )+ # of possible data range |
||
149 | +234 |
- }+ } |
||
150 | +235 |
-
+ } |
||
151 | +236 | |||
152 | +237 |
- #' Deep copy `teal_slices`+ # Note regarding case `id = character(0)`: |
||
153 | +238 |
- #'+ # rather than using `callModule` and creating a submodule of this module, we directly modify |
||
154 | +239 |
- #' it's important to create a new copy of `teal_slices` when+ # the `ui` and `server` with `id = character(0)` and calling the server function directly |
||
155 | +240 |
- #' starting a new `shiny` session. Otherwise, object will be shared+ # rather than through `callModule` |
||
156 | -+ | |||
241 | +12x |
- #' by multiple users as it is created in global environment before+ res <- list( |
||
157 | -+ | |||
242 | +12x |
- #' `shiny` session starts.+ ui = ui_teal_with_splash(id = id, data = data, title = title, header = header, footer = footer), |
||
158 | -+ | |||
243 | +12x |
- #' @param filter (`teal_slices`)+ server = function(input, output, session) { |
||
159 | -+ | |||
244 | +! |
- #' @return `teal_slices`+ if (!is.null(landing_module)) { |
||
160 | -+ | |||
245 | +! |
- #' @keywords internal+ do.call(landing_module$server, c(list(id = "landing_module_shiny_id"), landing_module$server_args)) |
||
161 | +246 |
- deep_copy_filter <- function(filter) {- |
- ||
162 | -1x | -
- checkmate::assert_class(filter, "teal_slices")+ } |
||
163 | -1x | +|||
247 | +! |
- shiny::isolate({+ srv_teal_with_splash(id = id, data = data, modules = modules, filter = deep_copy_filter(filter)) |
||
164 | -1x | +|||
248 | +
- filter_copy <- lapply(filter, function(slice) {+ } |
|||
165 | -2x | +|||
249 | +
- teal.slice::as.teal_slice(as.list(slice))+ ) |
|||
166 | +250 |
- })+ |
||
167 | -1x | +251 | +12x |
- attributes(filter_copy) <- attributes(filter)+ logger::log_trace("init teal app has been initialized.") |
168 | -1x | +|||
252 | +
- filter_copy+ |
|||
169 | -+ | |||
253 | +12x |
- })+ return(res) |
||
170 | +254 |
}@@ -10835,14 +10808,14 @@ teal coverage - 63.82% |
1 |
- #' Data Module for `teal` Applications+ #' Creates a `teal_modules` object. |
||
3 |
- #' @description+ #' @description `r lifecycle::badge("stable")` |
||
4 |
- #' `r lifecycle::badge("experimental")`+ #' This function collects a list of `teal_modules` and `teal_module` objects and returns a `teal_modules` object |
||
5 |
- #'+ #' containing the passed objects. |
||
6 |
- #' Create a `teal_data_module` object and evaluate code on it with history tracking.+ #' |
||
7 |
- #'+ #' This function dictates what modules are included in a `teal` application. The internal structure of `teal_modules` |
||
8 |
- #' @details+ #' shapes the navigation panel of a `teal` application. |
||
9 |
- #' `teal_data_module` creates a `shiny` module to supply or modify data in a `teal` application.+ #' |
||
10 |
- #' The module allows for running data pre-processing code (creation _and_ some modification) after the app starts.+ #' @param ... (`teal_module` or `teal_modules`) see [module()] and [modules()] for more details |
||
11 |
- #' The body of the server function will be run in the app rather than in the global environment.+ #' @param label (`character(1)`) label of modules collection (default `"root"`). |
||
12 |
- #' This means it will be run every time the app starts, so use sparingly.\cr+ #' If using the `label` argument then it must be explicitly named. |
||
13 |
- #' Pass this module instead of a `teal_data` object in a call to [init()].+ #' For example `modules("lab", ...)` should be converted to `modules(label = "lab", ...)` |
||
14 |
- #' Note that the server function must always return a `teal_data` object wrapped in a reactive expression.\cr+ #' |
||
15 |
- #' See vignette `vignette("data-as-shiny-module", package = "teal")` for more details.+ #' @export |
||
17 |
- #' @param ui (`function(id)`)\cr+ #' @return object of class \code{teal_modules}. Object contains following fields |
||
18 |
- #' `shiny` module `ui` function; must only take `id` argument+ #' - `label`: taken from the `label` argument |
||
19 |
- #' @param server (`function(id)`)\cr+ #' - `children`: a list containing objects passed in `...`. List elements are named after |
||
20 |
- #' `shiny` module `ui` function; must only take `id` argument;+ #' their `label` attribute converted to a valid `shiny` id. |
||
21 |
- #' must return reactive expression containing `teal_data` object+ #' @examples |
||
22 |
- #'+ #' library(shiny) |
||
23 |
- #' @return+ #' |
||
24 |
- #' `teal_data_module` returns an object of class `teal_data_module`.+ #' app <- init( |
||
25 |
- #'+ #' data = teal_data(iris = iris), |
||
26 |
- #' @examples+ #' modules = modules( |
||
27 |
- #' data <- teal_data_module(+ #' label = "Modules", |
||
28 |
- #' ui = function(id) {+ #' modules( |
||
29 |
- #' ns <- NS(id)+ #' label = "Module", |
||
30 |
- #' actionButton(ns("submit"), label = "Load data")+ #' module( |
||
31 |
- #' },+ #' label = "Inner module", |
||
32 |
- #' server = function(id) {+ #' server = function(id, data) { |
||
33 |
- #' moduleServer(id, function(input, output, session) {+ #' moduleServer( |
||
34 |
- #' eventReactive(input$submit, {+ #' id, |
||
35 |
- #' data <- within(+ #' module = function(input, output, session) { |
||
36 |
- #' teal_data(),+ #' output$data <- renderDataTable(data[["iris"]]()) |
||
37 |
- #' {+ #' } |
||
38 |
- #' dataset1 <- iris+ #' ) |
||
39 |
- #' dataset2 <- mtcars+ #' }, |
||
40 |
- #' }+ #' ui = function(id) { |
||
41 |
- #' )+ #' ns <- NS(id) |
||
42 |
- #' datanames(data) <- c("dataset1", "dataset2")+ #' tagList(dataTableOutput(ns("data"))) |
||
43 |
- #'+ #' }, |
||
44 |
- #' data+ #' datanames = "all" |
||
45 |
- #' })+ #' ) |
||
46 |
- #' })+ #' ), |
||
47 |
- #' }+ #' module( |
||
48 |
- #' )+ #' label = "Another module", |
||
49 |
- #'+ #' server = function(id) { |
||
50 |
- #' @name teal_data_module+ #' moduleServer( |
||
51 |
- #' @rdname teal_data_module+ #' id, |
||
52 |
- #'+ #' module = function(input, output, session) { |
||
53 |
- #' @seealso [`teal_data-class`], [`base::within()`], [`teal.code::within.qenv()`]+ #' output$text <- renderText("Another module") |
||
54 |
- #'+ #' } |
||
55 |
- #' @export+ #' ) |
||
56 |
- teal_data_module <- function(ui, server) {+ #' }, |
||
57 | -35x | +
- checkmate::assert_function(ui, args = "id", nargs = 1)+ #' ui = function(id) { |
|
58 | -34x | +
- checkmate::assert_function(server, args = "id", nargs = 1)+ #' ns <- NS(id) |
|
59 | -33x | +
- structure(+ #' tagList(textOutput(ns("text"))) |
|
60 | -33x | +
- list(ui = ui, server = server),+ #' }, |
|
61 | -33x | +
- class = "teal_data_module"+ #' datanames = NULL |
|
62 |
- )+ #' ) |
||
63 |
- }+ #' ) |
1 | +64 |
- #' Create a `teal` module for previewing a report+ #' ) |
||
2 | +65 |
- #'+ #' if (interactive()) { |
||
3 | +66 |
- #' @description `r lifecycle::badge("experimental")`+ #' shinyApp(app$ui, app$server) |
||
4 | +67 |
- #' This function wraps [teal.reporter::reporter_previewer_ui()] and+ #' } |
||
5 | +68 |
- #' [teal.reporter::reporter_previewer_srv()] into a `teal_module` to be+ modules <- function(..., label = "root") { |
||
6 | -+ | |||
69 | +95x |
- #' used in `teal` applications.+ checkmate::assert_string(label) |
||
7 | -+ | |||
70 | +93x |
- #'+ submodules <- list(...) |
||
8 | -+ | |||
71 | +93x |
- #' If you are creating a `teal` application using [teal::init()] then this+ if (any(vapply(submodules, is.character, FUN.VALUE = logical(1)))) { |
||
9 | -+ | |||
72 | +2x |
- #' module will be added to your application automatically if any of your `teal modules`+ stop( |
||
10 | -+ | |||
73 | +2x |
- #' support report generation.+ "The only character argument to modules() must be 'label' and it must be named, ", |
||
11 | -+ | |||
74 | +2x |
- #'+ "change modules('lab', ...) to modules(label = 'lab', ...)" |
||
12 | +75 |
- #' @inheritParams module+ ) |
||
13 | +76 |
- #' @param server_args (`named list`)\cr+ } |
||
14 | +77 |
- #' Arguments passed to [teal.reporter::reporter_previewer_srv()].+ |
||
15 | -+ | |||
78 | +91x |
- #' @return `teal_module` (extended with `teal_module_previewer` class) containing the `teal.reporter` previewer+ checkmate::assert_list(submodules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules")) |
||
16 | +79 |
- #' functionality.+ # name them so we can more easily access the children |
||
17 | +80 |
- #' @export+ # beware however that the label of the submodules should not be changed as it must be kept synced |
||
18 | -+ | |||
81 | +88x |
- reporter_previewer_module <- function(label = "Report previewer", server_args = list()) {+ labels <- vapply(submodules, function(submodule) submodule$label, character(1)) |
||
19 | -4x | +82 | +88x |
- checkmate::assert_string(label)+ names(submodules) <- make.unique(gsub("[^[:alnum:]]+", "_", labels), sep = "_") |
20 | -2x | +83 | +88x |
- checkmate::assert_list(server_args, names = "named")+ structure( |
21 | -2x | +84 | +88x |
- checkmate::assert_true(all(names(server_args) %in% names(formals(teal.reporter::reporter_previewer_srv))))+ list( |
22 | -+ | |||
85 | +88x |
-
+ label = label, |
||
23 | -2x | +86 | +88x |
- logger::log_info("Initializing reporter_previewer_module")+ children = submodules |
24 | +87 |
-
+ ), |
||
25 | -2x | +88 | +88x |
- srv <- function(id, reporter, ...) {+ class = "teal_modules" |
26 | -! | +|||
89 | +
- teal.reporter::reporter_previewer_srv(id, reporter, ...)+ ) |
|||
27 | +90 |
- }+ } |
||
28 | +91 | |||
29 | -2x | +|||
92 | +
- ui <- function(id, ...) {+ #' Append a `teal_module` to `children` of a `teal_modules` object |
|||
30 | -! | +|||
93 | +
- teal.reporter::reporter_previewer_ui(id, ...)+ #' @keywords internal |
|||
31 | +94 |
- }+ #' @param modules `teal_modules` |
||
32 | +95 |
-
+ #' @param module `teal_module` object to be appended onto the children of `modules` |
||
33 | -2x | +|||
96 | +
- module <- module(+ #' @return `teal_modules` object with `module` appended |
|||
34 | -2x | +|||
97 | +
- label = "temporary label",+ append_module <- function(modules, module) { |
|||
35 | -2x | +98 | +7x |
- server = srv, ui = ui,+ checkmate::assert_class(modules, "teal_modules") |
36 | -2x | -
- server_args = server_args, ui_args = list(), datanames = NULL- |
- ||
37 | -- |
- )- |
- ||
38 | -+ | 99 | +5x |
- # Module is created with a placeholder label and the label is changed later.+ checkmate::assert_class(module, "teal_module") |
39 | -+ | |||
100 | +3x |
- # This is to prevent another module being labeled "Report previewer".+ modules$children <- c(modules$children, list(module)) |
||
40 | -2x | +101 | +3x |
- class(module) <- c("teal_module_previewer", class(module))+ labels <- vapply(modules$children, function(submodule) submodule$label, character(1)) |
41 | -2x | +102 | +3x |
- module$label <- label+ names(modules$children) <- make.unique(gsub("[^[:alnum:]]", "_", tolower(labels)), sep = "_") |
42 | -2x | +103 | +3x |
- module+ modules |
43 | +104 |
} |
1 | +105 |
- #' Creates a `teal_modules` object.+ |
||
2 | +106 |
- #'- |
- ||
3 | -- |
- #' @description `r lifecycle::badge("stable")`+ #' Extract/Remove module(s) of specific class |
||
4 | +107 |
- #' This function collects a list of `teal_modules` and `teal_module` objects and returns a `teal_modules` object+ #' |
||
5 | +108 |
- #' containing the passed objects.+ #' Given a `teal_module` or a `teal_modules`, return the elements of the structure according to `class`. |
||
6 | +109 |
#' |
||
7 | +110 |
- #' This function dictates what modules are included in a `teal` application. The internal structure of `teal_modules`+ #' @param modules `teal_modules` |
||
8 | +111 |
- #' shapes the navigation panel of a `teal` application.+ #' @param class The class name of `teal_module` to be extracted or dropped. |
||
9 | +112 |
- #'+ #' @keywords internal |
||
10 | +113 |
- #' @param ... (`teal_module` or `teal_modules`) see [module()] and [modules()] for more details+ #' @return |
||
11 | +114 |
- #' @param label (`character(1)`) label of modules collection (default `"root"`).+ #' For `extract_module`, a `teal_module` of class `class` or `teal_modules` containing modules of class `class`. |
||
12 | +115 |
- #' If using the `label` argument then it must be explicitly named.+ #' For `drop_module`, the opposite, which is all `teal_modules` of class other than `class`. |
||
13 | +116 |
- #' For example `modules("lab", ...)` should be converted to `modules(label = "lab", ...)`+ #' @rdname module_management |
||
14 | +117 |
- #'+ extract_module <- function(modules, class) { |
||
15 | -+ | |||
118 | +30x |
- #' @export+ if (inherits(modules, class)) { |
||
16 | -+ | |||
119 | +! |
- #'+ modules |
||
17 | -+ | |||
120 | +30x |
- #' @return object of class \code{teal_modules}. Object contains following fields+ } else if (inherits(modules, "teal_module")) { |
||
18 | -+ | |||
121 | +16x |
- #' - `label`: taken from the `label` argument+ NULL |
||
19 | -+ | |||
122 | +14x |
- #' - `children`: a list containing objects passed in `...`. List elements are named after+ } else if (inherits(modules, "teal_modules")) { |
||
20 | -+ | |||
123 | +14x |
- #' their `label` attribute converted to a valid `shiny` id.+ Filter(function(x) length(x) > 0L, lapply(modules$children, extract_module, class)) |
||
21 | +124 |
- #' @examples+ } |
||
22 | +125 |
- #' library(shiny)+ } |
||
23 | +126 |
- #'+ |
||
24 | +127 |
- #' app <- init(+ #' @keywords internal |
||
25 | +128 |
- #' data = teal_data(iris = iris),+ #' @return `teal_modules` |
||
26 | +129 |
- #' modules = modules(+ #' @rdname module_management |
||
27 | +130 |
- #' label = "Modules",+ drop_module <- function(modules, class) { |
||
28 | -+ | |||
131 | +! |
- #' modules(+ if (inherits(modules, class)) { |
||
29 | -+ | |||
132 | +! |
- #' label = "Module",+ NULL |
||
30 | -+ | |||
133 | +! |
- #' module(+ } else if (inherits(modules, "teal_module")) { |
||
31 | -+ | |||
134 | +! |
- #' label = "Inner module",+ modules |
||
32 | -+ | |||
135 | +! |
- #' server = function(id, data) {+ } else if (inherits(modules, "teal_modules")) { |
||
33 | -+ | |||
136 | +! |
- #' moduleServer(+ do.call( |
||
34 | -+ | |||
137 | +! |
- #' id,+ "modules", |
||
35 | -+ | |||
138 | +! |
- #' module = function(input, output, session) {+ c(Filter(function(x) length(x) > 0L, lapply(modules$children, drop_module, class)), label = modules$label) |
||
36 | +139 |
- #' output$data <- renderDataTable(data[["iris"]]())+ ) |
||
37 | +140 |
- #' }+ } |
||
38 | +141 |
- #' )+ } |
||
39 | +142 |
- #' },+ |
||
40 | +143 |
- #' ui = function(id) {+ #' Does the object make use of the `arg` |
||
41 | +144 |
- #' ns <- NS(id)+ #' |
||
42 | +145 |
- #' tagList(dataTableOutput(ns("data")))+ #' @param modules (`teal_module` or `teal_modules`) object |
||
43 | +146 |
- #' },+ #' @param arg (`character(1)`) names of the arguments to be checked against formals of `teal` modules. |
||
44 | +147 |
- #' datanames = "all"+ #' @return `logical` whether the object makes use of `arg` |
||
45 | +148 |
- #' )+ #' @rdname is_arg_used |
||
46 | +149 |
- #' ),+ #' @keywords internal |
||
47 | +150 |
- #' module(+ is_arg_used <- function(modules, arg) { |
||
48 | -+ | |||
151 | +286x |
- #' label = "Another module",+ checkmate::assert_string(arg) |
||
49 | -+ | |||
152 | +283x |
- #' server = function(id) {+ if (inherits(modules, "teal_modules")) { |
||
50 | -+ | |||
153 | +29x |
- #' moduleServer(+ any(unlist(lapply(modules$children, is_arg_used, arg))) |
||
51 | -+ | |||
154 | +254x |
- #' id,+ } else if (inherits(modules, "teal_module")) { |
||
52 | -+ | |||
155 | +43x |
- #' module = function(input, output, session) {+ is_arg_used(modules$server, arg) || is_arg_used(modules$ui, arg) |
||
53 | -+ | |||
156 | +211x |
- #' output$text <- renderText("Another module")+ } else if (is.function(modules)) { |
||
54 | -+ | |||
157 | +209x |
- #' }+ isTRUE(arg %in% names(formals(modules))) |
||
55 | +158 |
- #' )+ } else { |
||
56 | -+ | |||
159 | +2x |
- #' },+ stop("is_arg_used function not implemented for this object") |
||
57 | +160 |
- #' ui = function(id) {+ } |
||
58 | +161 |
- #' ns <- NS(id)+ } |
||
59 | +162 |
- #' tagList(textOutput(ns("text")))+ |
||
60 | +163 |
- #' },+ |
||
61 | +164 |
- #' datanames = NULL+ #' Creates a `teal_module` object. |
||
62 | +165 |
- #' )+ #' |
||
63 | +166 |
- #' )+ #' @description `r lifecycle::badge("stable")` |
||
64 | +167 |
- #' )+ #' This function embeds a `shiny` module inside a `teal` application. One `teal_module` maps to one `shiny` module. |
||
65 | +168 |
- #' if (interactive()) {+ #' |
||
66 | +169 |
- #' shinyApp(app$ui, app$server)+ #' @param label (`character(1)`) Label shown in the navigation item for the module. Any label possible except |
||
67 | +170 |
- #' }+ #' `"global_filters"` - read more in `mapping` argument of [teal::teal_slices]. |
||
68 | +171 |
- modules <- function(..., label = "root") {+ #' @param server (`function`) `shiny` module with following arguments: |
||
69 | -109x | +|||
172 | +
- checkmate::assert_string(label)+ #' - `id` - teal will set proper shiny namespace for this module (see [shiny::moduleServer()]). |
|||
70 | -107x | +|||
173 | +
- submodules <- list(...)+ #' - `input`, `output`, `session` - (not recommended) then [shiny::callModule()] will be used to call a module. |
|||
71 | -107x | +|||
174 | +
- if (any(vapply(submodules, is.character, FUN.VALUE = logical(1)))) {+ #' - `data` (optional) module will receive a `teal_data` object, a list of reactive (filtered) data specified in |
|||
72 | -2x | +|||
175 | +
- stop(+ #' the `filters` argument. |
|||
73 | -2x | +|||
176 | +
- "The only character argument to modules() must be 'label' and it must be named, ",+ #' - `datasets` (optional) module will receive `FilteredData`. (See `[teal.slice::FilteredData]`). |
|||
74 | -2x | +|||
177 | +
- "change modules('lab', ...) to modules(label = 'lab', ...)"+ #' - `reporter` (optional) module will receive `Reporter`. (See [teal.reporter::Reporter]). |
|||
75 | +178 |
- )+ # - `filter_panel_api` (optional) module will receive `FilterPanelAPI`. (See [teal.slice::FilterPanelAPI]). |
||
76 | +179 |
- }+ #' - `...` (optional) `server_args` elements will be passed to the module named argument or to the `...`. |
||
77 | +180 |
-
+ #' @param ui (`function`) Shiny `ui` module function with following arguments: |
||
78 | -105x | +|||
181 | +
- checkmate::assert_list(submodules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules"))+ #' - `id` - teal will set proper shiny namespace for this module. |
|||
79 | +182 |
- # name them so we can more easily access the children+ #' - `...` (optional) `ui_args` elements will be passed to the module named argument or to the `...`. |
||
80 | +183 |
- # beware however that the label of the submodules should not be changed as it must be kept synced+ #' @param filters (`character`) Deprecated. Use `datanames` instead. |
||
81 | -102x | +|||
184 | +
- labels <- vapply(submodules, function(submodule) submodule$label, character(1))+ #' @param datanames (`character`) A vector with `datanames` that are relevant for the item. The |
|||
82 | -102x | +|||
185 | +
- names(submodules) <- make.unique(gsub("[^[:alnum:]]+", "_", labels), sep = "_")+ #' filter panel will automatically update the shown filters to include only |
|||
83 | -102x | +|||
186 | +
- structure(+ #' filters in the listed datasets. `NULL` will hide the filter panel, |
|||
84 | -102x | +|||
187 | +
- list(+ #' and the keyword `'all'` will show filters of all datasets. `datanames` also determines |
|||
85 | -102x | +|||
188 | +
- label = label,+ #' a subset of datasets which are appended to the `data` argument in `server` function. |
|||
86 | -102x | +|||
189 | +
- children = submodules+ #' @param server_args (named `list`) with additional arguments passed on to the |
|||
87 | +190 |
- ),+ #' `server` function. |
||
88 | -102x | +|||
191 | +
- class = "teal_modules"+ #' @param ui_args (named `list`) with additional arguments passed on to the |
|||
89 | +192 |
- )+ #' `ui` function. |
||
90 | +193 |
- }+ #' |
||
91 | +194 |
-
+ #' @return object of class `teal_module`. |
||
92 | +195 |
- #' Append a `teal_module` to `children` of a `teal_modules` object+ #' @export |
||
93 | +196 |
- #' @keywords internal+ #' @examples |
||
94 | +197 |
- #' @param modules `teal_modules`+ #' library(shiny) |
||
95 | +198 |
- #' @param module `teal_module` object to be appended onto the children of `modules`+ #' |
||
96 | +199 |
- #' @return `teal_modules` object with `module` appended+ #' app <- init( |
||
97 | +200 |
- append_module <- function(modules, module) {- |
- ||
98 | -7x | -
- checkmate::assert_class(modules, "teal_modules")+ #' data = teal_data(iris = iris), |
||
99 | -5x | +|||
201 | +
- checkmate::assert_class(module, "teal_module")+ #' modules = list( |
|||
100 | -3x | +|||
202 | +
- modules$children <- c(modules$children, list(module))+ #' module( |
|||
101 | -3x | +|||
203 | +
- labels <- vapply(modules$children, function(submodule) submodule$label, character(1))+ #' label = "Module", |
|||
102 | -3x | +|||
204 | +
- names(modules$children) <- make.unique(gsub("[^[:alnum:]]", "_", tolower(labels)), sep = "_")+ #' server = function(id, data) { |
|||
103 | -3x | +|||
205 | +
- modules+ #' moduleServer( |
|||
104 | +206 |
- }+ #' id, |
||
105 | +207 |
-
+ #' module = function(input, output, session) { |
||
106 | +208 |
- #' Extract/Remove module(s) of specific class+ #' output$data <- renderDataTable(data[["iris"]]()) |
||
107 | +209 |
- #'+ #' } |
||
108 | +210 |
- #' Given a `teal_module` or a `teal_modules`, return the elements of the structure according to `class`.+ #' ) |
||
109 | +211 |
- #'+ #' }, |
||
110 | +212 |
- #' @param modules `teal_modules`+ #' ui = function(id) { |
||
111 | +213 |
- #' @param class The class name of `teal_module` to be extracted or dropped.+ #' ns <- NS(id) |
||
112 | +214 |
- #' @keywords internal+ #' tagList(dataTableOutput(ns("data"))) |
||
113 | +215 |
- #' @return+ #' } |
||
114 | +216 |
- #' For `extract_module`, a `teal_module` of class `class` or `teal_modules` containing modules of class `class`.+ #' ) |
||
115 | +217 |
- #' For `drop_module`, the opposite, which is all `teal_modules` of class other than `class`.+ #' ) |
||
116 | +218 |
- #' @rdname module_management+ #' ) |
||
117 | +219 |
- extract_module <- function(modules, class) {+ #' if (interactive()) { |
||
118 | -30x | +|||
220 | +
- if (inherits(modules, class)) {+ #' shinyApp(app$ui, app$server) |
|||
119 | -! | +|||
221 | +
- modules+ #' } |
|||
120 | -30x | +|||
222 | +
- } else if (inherits(modules, "teal_module")) {+ module <- function(label = "module", |
|||
121 | -16x | +|||
223 | +
- NULL+ server = function(id, ...) { |
|||
122 | -14x | +|||
224 | +! |
- } else if (inherits(modules, "teal_modules")) {+ moduleServer(id, function(input, output, session) {}) # nolint |
||
123 | -14x | +|||
225 | +
- Filter(function(x) length(x) > 0L, lapply(modules$children, extract_module, class))+ }, |
|||
124 | +226 |
- }+ ui = function(id, ...) { |
||
125 | -+ | |||
227 | +! |
- }+ tags$p(paste0("This module has no UI (id: ", id, " )")) |
||
126 | +228 |
-
+ }, |
||
127 | +229 |
- #' @keywords internal+ filters, |
||
128 | +230 |
- #' @return `teal_modules`+ datanames = "all", |
||
129 | +231 |
- #' @rdname module_management+ server_args = NULL, |
||
130 | +232 |
- drop_module <- function(modules, class) {+ ui_args = NULL) { |
||
131 | -30x | +|||
233 | +
- if (inherits(modules, class)) {+ # argument checking (independent) |
|||
132 | -! | +|||
234 | +
- NULL+ ## `label` |
|||
133 | -30x | +235 | +135x |
- } else if (inherits(modules, "teal_module")) {+ checkmate::assert_string(label) |
134 | -16x | +236 | +132x |
- modules+ if (label == "global_filters") { |
135 | -14x | +237 | +1x |
- } else if (inherits(modules, "teal_modules")) {+ stop( |
136 | -14x | +238 | +1x |
- do.call(+ sprintf("module(label = \"%s\", ...\n ", label), |
137 | -14x | +239 | +1x |
- "modules",+ "Label 'global_filters' is reserved in teal. Please change to something else.", |
138 | -14x | +240 | +1x |
- c(Filter(function(x) length(x) > 0L, lapply(modules$children, drop_module, class)), label = modules$label)+ call. = FALSE |
139 | +241 |
) |
||
140 | +242 |
} |
||
141 | -+ | |||
243 | +131x |
- }+ if (label == "Report previewer") { |
||
142 | -+ | |||
244 | +! |
-
+ stop( |
||
143 | -+ | |||
245 | +! |
- #' Does the object make use of the `arg`+ sprintf("module(label = \"%s\", ...\n ", label), |
||
144 | -+ | |||
246 | +! |
- #'+ "Label 'Report previewer' is reserved in teal. Please change to something else.", |
||
145 | -+ | |||
247 | +! |
- #' @param modules (`teal_module` or `teal_modules`) object+ call. = FALSE |
||
146 | +248 |
- #' @param arg (`character(1)`) names of the arguments to be checked against formals of `teal` modules.+ ) |
||
147 | +249 |
- #' @return `logical` whether the object makes use of `arg`+ } |
||
148 | +250 |
- #' @rdname is_arg_used+ |
||
149 | +251 |
- #' @keywords internal+ ## `server` |
||
150 | -+ | |||
252 | +131x |
- is_arg_used <- function(modules, arg) {+ checkmate::assert_function(server) |
||
151 | -286x | +253 | +131x |
- checkmate::assert_string(arg)+ server_formals <- names(formals(server)) |
152 | -283x | +254 | +131x |
- if (inherits(modules, "teal_modules")) {+ if (!( |
153 | -29x | +255 | +131x |
- any(unlist(lapply(modules$children, is_arg_used, arg)))+ "id" %in% server_formals || |
154 | -254x | +256 | +131x |
- } else if (inherits(modules, "teal_module")) {+ all(c("input", "output", "session") %in% server_formals) |
155 | -43x | +|||
257 | +
- is_arg_used(modules$server, arg) || is_arg_used(modules$ui, arg)+ )) { |
|||
156 | -211x | +258 | +2x |
- } else if (is.function(modules)) {+ stop( |
157 | -209x | +259 | +2x |
- isTRUE(arg %in% names(formals(modules)))+ "\nmodule() `server` argument requires a function with following arguments:", |
158 | -+ | |||
260 | +2x |
- } else {+ "\n - id - teal will set proper shiny namespace for this module.", |
||
159 | +261 | 2x |
- stop("is_arg_used function not implemented for this object")+ "\n - input, output, session (not recommended) - then shiny::callModule will be used to call a module.", |
|
160 | -+ | |||
262 | +2x |
- }+ "\n\nFollowing arguments can be used optionaly:", |
||
161 | -+ | |||
263 | +2x |
- }+ "\n - `data` - module will receive list of reactive (filtered) data specified in the `filters` argument", |
||
162 | -+ | |||
264 | +2x |
-
+ "\n - `datasets` - module will receive `FilteredData`. See `help(teal.slice::FilteredData)`", |
||
163 | -+ | |||
265 | +2x |
-
+ "\n - `reporter` - module will receive `Reporter`. See `help(teal.reporter::Reporter)`", |
||
164 | -+ | |||
266 | +2x |
- #' Creates a `teal_module` object.+ "\n - `filter_panel_api` - module will receive `FilterPanelAPI`. (See [teal.slice::FilterPanelAPI]).", |
||
165 | -+ | |||
267 | +2x |
- #'+ "\n - `...` server_args elements will be passed to the module named argument or to the `...`" |
||
166 | +268 |
- #' @description `r lifecycle::badge("stable")`+ ) |
||
167 | +269 |
- #' This function embeds a `shiny` module inside a `teal` application. One `teal_module` maps to one `shiny` module.+ } |
||
168 | -+ | |||
270 | +129x |
- #'+ if ("datasets" %in% server_formals) { |
||
169 | -+ | |||
271 | +2x |
- #' @param label (`character(1)`) Label shown in the navigation item for the module. Any label possible except+ warning( |
||
170 | -+ | |||
272 | +2x |
- #' `"global_filters"` - read more in `mapping` argument of [teal::teal_slices].+ sprintf("Called from module(label = \"%s\", ...)\n ", label), |
||
171 | -+ | |||
273 | +2x |
- #' @param server (`function`) `shiny` module with following arguments:+ "`datasets` argument in the `server` is deprecated and will be removed in the next release. ", |
||
172 | -+ | |||
274 | +2x |
- #' - `id` - teal will set proper shiny namespace for this module (see [shiny::moduleServer()]).+ "Please use `data` instead.", |
||
173 | -+ | |||
275 | +2x |
- #' - `input`, `output`, `session` - (not recommended) then [shiny::callModule()] will be used to call a module.+ call. = FALSE |
||
174 | +276 |
- #' - `data` (optional) module will receive a `teal_data` object, a list of reactive (filtered) data specified in+ ) |
||
175 | +277 |
- #' the `filters` argument.+ } |
||
176 | +278 |
- #' - `datasets` (optional) module will receive `FilteredData`. (See `[teal.slice::FilteredData]`).+ |
||
177 | +279 |
- #' - `reporter` (optional) module will receive `Reporter`. (See [teal.reporter::Reporter]).+ |
||
178 | +280 |
- # - `filter_panel_api` (optional) module will receive `FilterPanelAPI`. (See [teal.slice::FilterPanelAPI]).+ ## `ui` |
||
179 | -+ | |||
281 | +129x |
- #' - `...` (optional) `server_args` elements will be passed to the module named argument or to the `...`.+ checkmate::assert_function(ui) |
||
180 | -+ | |||
282 | +129x |
- #' @param ui (`function`) Shiny `ui` module function with following arguments:+ ui_formals <- names(formals(ui)) |
||
181 | -+ | |||
283 | +129x |
- #' - `id` - teal will set proper shiny namespace for this module.+ if (!"id" %in% ui_formals) { |
||
182 | -+ | |||
284 | +1x |
- #' - `...` (optional) `ui_args` elements will be passed to the module named argument or to the `...`.+ stop( |
||
183 | -+ | |||
285 | +1x |
- #' @param filters (`character`) Deprecated. Use `datanames` instead.+ "\nmodule() `ui` argument requires a function with following arguments:", |
||
184 | -+ | |||
286 | +1x |
- #' @param datanames (`character`) A vector with `datanames` that are relevant for the item. The+ "\n - id - teal will set proper shiny namespace for this module.", |
||
185 | -+ | |||
287 | +1x |
- #' filter panel will automatically update the shown filters to include only+ "\n\nFollowing arguments can be used optionally:", |
||
186 | -+ | |||
288 | +1x |
- #' filters in the listed datasets. `NULL` will hide the filter panel,+ "\n - `...` ui_args elements will be passed to the module argument of the same name or to the `...`" |
||
187 | +289 |
- #' and the keyword `'all'` will show filters of all datasets. `datanames` also determines+ ) |
||
188 | +290 |
- #' a subset of datasets which are appended to the `data` argument in `server` function.+ } |
||
189 | -+ | |||
291 | +128x |
- #' @param server_args (named `list`) with additional arguments passed on to the+ if (any(c("data", "datasets") %in% ui_formals)) { |
||
190 | -+ | |||
292 | +2x |
- #' `server` function.+ stop( |
||
191 | -+ | |||
293 | +2x |
- #' @param ui_args (named `list`) with additional arguments passed on to the+ sprintf("Called from module(label = \"%s\", ...)\n ", label), |
||
192 | -+ | |||
294 | +2x |
- #' `ui` function.+ "`ui` with `data` or `datasets` argument is no longer accepted.\n ", |
||
193 | -+ | |||
295 | +2x |
- #'+ "If some `ui` inputs depend on data, please move the logic to your `server` instead.\n ", |
||
194 | -+ | |||
296 | +2x |
- #' @return object of class `teal_module`.+ "Possible solutions are renderUI() or updateXyzInput() functions." |
||
195 | +297 |
- #' @export+ ) |
||
196 | +298 |
- #' @examples+ } |
||
197 | +299 |
- #' library(shiny)+ |
||
198 | +300 |
- #'+ |
||
199 | +301 |
- #' app <- init(+ ## `filters` |
||
200 | -+ | |||
302 | +126x |
- #' data = teal_data(iris = iris),+ if (!missing(filters)) { |
||
201 | -+ | |||
303 | +! |
- #' modules = list(+ datanames <- filters |
||
202 | -+ | |||
304 | +! |
- #' module(+ msg <- |
||
203 | -+ | |||
305 | +! |
- #' label = "Module",+ "The `filters` argument is deprecated and will be removed in the next release. Please use `datanames` instead." |
||
204 | -+ | |||
306 | +! |
- #' server = function(id, data) {+ logger::log_warn(msg) |
||
205 | -+ | |||
307 | +! |
- #' moduleServer(+ warning(msg) |
||
206 | +308 |
- #' id,+ } |
||
207 | +309 |
- #' module = function(input, output, session) {+ |
||
208 | +310 |
- #' output$data <- renderDataTable(data[["iris"]]())+ ## `datanames` (also including deprecated `filters`) |
||
209 | +311 |
- #' }+ # please note a race condition between datanames set when filters is not missing and data arg in server function |
||
210 | -+ | |||
312 | +126x |
- #' )+ if (!is.element("data", server_formals) && !is.null(datanames)) { |
||
211 | -+ | |||
313 | +47x |
- #' },+ message(sprintf("module \"%s\" server function takes no data so \"datanames\" will be ignored", label)) |
||
212 | -+ | |||
314 | +47x |
- #' ui = function(id) {+ datanames <- NULL |
||
213 | +315 |
- #' ns <- NS(id)+ } |
||
214 | -+ | |||
316 | +126x |
- #' tagList(dataTableOutput(ns("data")))+ checkmate::assert_character(datanames, min.len = 1, null.ok = TRUE, any.missing = FALSE) |
||
215 | +317 |
- #' }+ |
||
216 | +318 |
- #' )+ ## `server_args` |
||
217 | -+ | |||
319 | +125x |
- #' )+ checkmate::assert_list(server_args, null.ok = TRUE, names = "named") |
||
218 | -+ | |||
320 | +123x |
- #' )+ srv_extra_args <- setdiff(names(server_args), server_formals) |
||
219 | -+ | |||
321 | +123x |
- #' if (interactive()) {+ if (length(srv_extra_args) > 0 && !"..." %in% server_formals) { |
||
220 | -+ | |||
322 | +1x |
- #' shinyApp(app$ui, app$server)+ stop( |
||
221 | -+ | |||
323 | +1x |
- #' }+ "\nFollowing `server_args` elements have no equivalent in the formals of the `server`:\n", |
||
222 | -- |
- module <- function(label = "module",- |
- ||
223 | -+ | |||
324 | +1x |
- server = function(id, ...) {+ paste(paste(" -", srv_extra_args), collapse = "\n"), |
||
224 | -! | +|||
325 | +1x |
- moduleServer(id, function(input, output, session) {}) # nolint+ "\n\nUpdate the `server` arguments by including above or add `...`" |
||
225 | +326 |
- },+ ) |
||
226 | +327 |
- ui = function(id, ...) {- |
- ||
227 | -! | -
- tags$p(paste0("This module has no UI (id: ", id, " )"))+ } |
||
228 | +328 |
- },+ |
||
229 | +329 |
- filters,+ ## `ui_args` |
||
230 | -+ | |||
330 | +122x |
- datanames = "all",+ checkmate::assert_list(ui_args, null.ok = TRUE, names = "named") |
||
231 | -+ | |||
331 | +120x |
- server_args = NULL,+ ui_extra_args <- setdiff(names(ui_args), ui_formals) |
||
232 | -+ | |||
332 | +120x |
- ui_args = NULL) {+ if (length(ui_extra_args) > 0 && !"..." %in% ui_formals) { |
||
233 | -135x | +333 | +1x |
- checkmate::assert_string(label)+ stop( |
234 | -132x | +334 | +1x |
- checkmate::assert_function(server)+ "\nFollowing `ui_args` elements have no equivalent in the formals of `ui`:\n", |
235 | -132x | +335 | +1x |
- checkmate::assert_function(ui)+ paste(paste(" -", ui_extra_args), collapse = "\n"), |
236 | -132x | +336 | +1x |
- checkmate::assert_character(datanames, min.len = 1, null.ok = TRUE, any.missing = FALSE)+ "\n\nUpdate the `ui` arguments by including above or add `...`" |
237 | -131x | +|||
337 | +
- checkmate::assert_list(server_args, null.ok = TRUE, names = "named")+ ) |
|||
238 | -129x | +|||
338 | +
- checkmate::assert_list(ui_args, null.ok = TRUE, names = "named")+ } |
|||
239 | +339 | |||
240 | -127x | +340 | +119x |
- if (!missing(filters)) {+ structure( |
241 | -! | +|||
341 | +119x |
- checkmate::assert_character(filters, min.len = 1, null.ok = TRUE, any.missing = FALSE)+ list( |
||
242 | -! | +|||
342 | +119x |
- datanames <- filters+ label = label, |
||
243 | -! | +|||
343 | +119x |
- msg <-+ server = server, ui = ui, datanames = unique(datanames), |
||
244 | -! | +|||
344 | +119x |
- "The `filters` argument is deprecated and will be removed in the next release. Please use `datanames` instead."+ server_args = server_args, ui_args = ui_args |
||
245 | -! | +|||
345 | +
- logger::log_warn(msg)+ ), |
|||
246 | -! | +|||
346 | +119x |
- warning(msg)+ class = "teal_module" |
||
247 | +347 |
- }+ ) |
||
248 | +348 | - - | -||
249 | -127x | -
- if (label == "global_filters") {+ } |
||
250 | -1x | +|||
349 | +
- stop(+ |
|||
251 | -1x | +|||
350 | +
- sprintf("module(label = \"%s\", ...\n ", label),+ |
|||
252 | -1x | +|||
351 | +
- "Label 'global_filters' is reserved in teal. Please change to something else.",+ #' Get module depth |
|||
253 | -1x | +|||
352 | +
- call. = FALSE+ #' |
|||
254 | +353 |
- )+ #' Depth starts at 0, so a single `teal.module` has depth 0. |
||
255 | +354 |
- }+ #' Nesting it increases overall depth by 1. |
||
256 | -126x | +|||
355 | +
- if (label == "Report previewer") {+ #' |
|||
257 | -! | +|||
356 | +
- stop(+ #' @inheritParams init |
|||
258 | -! | +|||
357 | +
- sprintf("module(label = \"%s\", ...\n ", label),+ #' @param depth optional, integer determining current depth level |
|||
259 | -! | +|||
358 | +
- "Label 'Report previewer' is reserved in teal.",+ #' |
|||
260 | -! | +|||
359 | +
- call. = FALSE+ #' @return depth level for given module |
|||
261 | +360 |
- )+ #' @keywords internal |
||
262 | +361 |
- }+ #' |
||
263 | -126x | +|||
362 | +
- server_formals <- names(formals(server))+ #' @examples |
|||
264 | -126x | +|||
363 | +
- if (!(+ #' mods <- modules( |
|||
265 | -126x | +|||
364 | +
- "id" %in% server_formals ||+ #' label = "d1", |
|||
266 | -126x | +|||
365 | +
- all(c("input", "output", "session") %in% server_formals)+ #' modules( |
|||
267 | +366 |
- )) {+ #' label = "d2", |
||
268 | -2x | +|||
367 | +
- stop(+ #' modules( |
|||
269 | -2x | +|||
368 | +
- "\nmodule() `server` argument requires a function with following arguments:",+ #' label = "d3", |
|||
270 | -2x | +|||
369 | +
- "\n - id - teal will set proper shiny namespace for this module.",+ #' module(label = "aaa1"), module(label = "aaa2"), module(label = "aaa3") |
|||
271 | -2x | +|||
370 | +
- "\n - input, output, session (not recommended) - then shiny::callModule will be used to call a module.",+ #' ), |
|||
272 | -2x | +|||
371 | +
- "\n\nFollowing arguments can be used optionaly:",+ #' module(label = "bbb") |
|||
273 | -2x | +|||
372 | +
- "\n - `data` - module will receive list of reactive (filtered) data specified in the `filters` argument",+ #' ), |
|||
274 | -2x | +|||
373 | +
- "\n - `datasets` - module will receive `FilteredData`. See `help(teal.slice::FilteredData)`",+ #' module(label = "ccc") |
|||
275 | -2x | +|||
374 | +
- "\n - `reporter` - module will receive `Reporter`. See `help(teal.reporter::Reporter)`",+ #' ) |
|||
276 | -2x | +|||
375 | +
- "\n - `filter_panel_api` - module will receive `FilterPanelAPI`. (See [teal.slice::FilterPanelAPI]).",+ #' stopifnot(teal:::modules_depth(mods) == 3L) |
|||
277 | -2x | +|||
376 | +
- "\n - `...` server_args elements will be passed to the module named argument or to the `...`"+ #' |
|||
278 | +377 |
- )+ #' mods <- modules( |
||
279 | +378 |
- }+ #' label = "a", |
||
280 | +379 |
-
+ #' modules( |
||
281 | -124x | +|||
380 | +
- if (!is.element("data", server_formals) && !is.null(datanames)) {+ #' label = "b1", module(label = "c") |
|||
282 | -46x | +|||
381 | +
- message(sprintf("module \"%s\" server function takes no data so \"datanames\" will be ignored", label))+ #' ), |
|||
283 | -46x | +|||
382 | +
- datanames <- NULL+ #' module(label = "b2") |
|||
284 | +383 |
- }+ #' ) |
||
285 | -124x | +|||
384 | +
- if ("datasets" %in% server_formals) {+ #' stopifnot(teal:::modules_depth(mods) == 2L) |
|||
286 | -2x | +|||
385 | +
- warning(+ modules_depth <- function(modules, depth = 0L) { |
|||
287 | -2x | +386 | +12x |
- sprintf("Called from module(label = \"%s\", ...)\n ", label),+ checkmate::assert_multi_class(modules, c("teal_module", "teal_modules")) |
288 | -2x | +387 | +12x |
- "`datasets` argument in the `server` is deprecated and will be removed in the next release. ",+ checkmate::assert_int(depth, lower = 0) |
289 | -2x | +388 | +11x |
- "Please use `data` instead.",+ if (inherits(modules, "teal_modules")) { |
290 | -2x | +389 | +4x |
- call. = FALSE+ max(vapply(modules$children, modules_depth, integer(1), depth = depth + 1L)) |
291 | +390 |
- )+ } else {+ |
+ ||
391 | +7x | +
+ depth |
||
292 | +392 |
} |
||
293 | +393 |
-
+ } |
||
294 | -124x | +|||
394 | +
- srv_extra_args <- setdiff(names(server_args), server_formals)+ |
|||
295 | -124x | +|||
395 | +
- if (length(srv_extra_args) > 0 && !"..." %in% server_formals) {+ |
|||
296 | -1x | +|||
396 | +
- stop(+ module_labels <- function(modules) { |
|||
297 | -1x | +|||
397 | +! |
- "\nFollowing `server_args` elements have no equivalent in the formals of the `server`:\n",+ if (inherits(modules, "teal_modules")) { |
||
298 | -1x | +|||
398 | +! |
- paste(paste(" -", srv_extra_args), collapse = "\n"),- |
- ||
299 | -1x | -
- "\n\nUpdate the `server` arguments by including above or add `...`"+ lapply(modules$children, module_labels) |
||
300 | +399 |
- )+ } else {+ |
+ ||
400 | +! | +
+ modules$label |
||
301 | +401 |
} |
||
302 | +402 |
-
+ } |
||
303 | -123x | +|||
403 | +
- ui_formals <- names(formals(ui))+ |
|||
304 | -123x | +|||
404 | +
- if (!"id" %in% ui_formals) {+ #' Converts `teal_modules` to a string |
|||
305 | -1x | +|||
405 | +
- stop(+ #' |
|||
306 | -1x | +|||
406 | +
- "\nmodule() `ui` argument requires a function with following arguments:",+ #' @param x (`teal_modules`) to print |
|||
307 | -1x | +|||
407 | +
- "\n - id - teal will set proper shiny namespace for this module.",+ #' @param indent (`integer`) indent level; |
|||
308 | -1x | +|||
408 | +
- "\n\nFollowing arguments can be used optionally:",+ #' each `submodule` is indented one level more |
|||
309 | -1x | +|||
409 | +
- "\n - `...` ui_args elements will be passed to the module argument of the same name or to the `...`"+ #' @param ... (optional) additional parameters to pass to recursive calls of `toString` |
|||
310 | +410 |
- )+ #' @return (`character`) |
||
311 | +411 |
- }+ #' @export |
||
312 | +412 |
-
+ #' @rdname modules |
||
313 | -122x | +|||
413 | +
- if (any(c("data", "datasets") %in% ui_formals)) {+ toString.teal_modules <- function(x, indent = 0, ...) { # nolint |
|||
314 | -2x | +|||
414 | +
- stop(+ # argument must be `x` to be consistent with base method |
|||
315 | -2x | +|||
415 | +! |
- sprintf("Called from module(label = \"%s\", ...)\n ", label),+ paste(c( |
||
316 | -2x | +|||
416 | +! |
- "`ui` with `data` or `datasets` argument is no longer accepted.\n ",+ paste0(rep(" ", indent), "+ ", x$label), |
||
317 | -2x | +|||
417 | +! |
- "If some `ui` inputs depend on data, please move the logic to your `server` instead.\n ",+ unlist(lapply(x$children, toString, indent = indent + 1, ...)) |
||
318 | -2x | +|||
418 | +! |
- "Possible solutions are renderUI() or updateXyzInput() functions."+ ), collapse = "\n") |
||
319 | +419 |
- )+ } |
||
320 | +420 |
- }+ |
||
321 | +421 |
-
+ #' Converts `teal_module` to a string |
||
322 | -120x | +|||
422 | +
- ui_extra_args <- setdiff(names(ui_args), ui_formals)+ #' |
|||
323 | -120x | +|||
423 | +
- if (length(ui_extra_args) > 0 && !"..." %in% ui_formals) {+ #' @inheritParams toString.teal_modules |
|||
324 | -1x | +|||
424 | +
- stop(+ #' @param x `teal_module` |
|||
325 | -1x | +|||
425 | +
- "\nFollowing `ui_args` elements have no equivalent in the formals of `ui`:\n",+ #' @param ... ignored |
|||
326 | -1x | +|||
426 | +
- paste(paste(" -", ui_extra_args), collapse = "\n"),+ #' @export |
|||
327 | -1x | +|||
427 | +
- "\n\nUpdate the `ui` arguments by including above or add `...`"+ #' @rdname module |
|||
328 | +428 |
- )+ toString.teal_module <- function(x, indent = 0, ...) { # nolint+ |
+ ||
429 | +! | +
+ paste0(paste(rep(" ", indent), collapse = ""), "+ ", x$label, collapse = "") |
||
329 | +430 |
- }+ } |
||
330 | +431 | |||
331 | -119x | +|||
432 | +
- structure(+ #' Prints `teal_modules` |
|||
332 | -119x | +|||
433 | +
- list(+ #' @param x `teal_modules` |
|||
333 | -119x | +|||
434 | +
- label = label,+ #' @param ... parameters passed to `toString` |
|||
334 | -119x | +|||
435 | +
- server = server, ui = ui, datanames = unique(datanames),+ #' @export |
|||
335 | -119x | +|||
436 | +
- server_args = server_args, ui_args = ui_args+ #' @rdname modules |
|||
336 | +437 |
- ),+ print.teal_modules <- function(x, ...) { |
||
337 | -119x | +|||
438 | +! |
- class = "teal_module"+ s <- toString(x, ...) |
||
338 | -+ | |||
439 | +! |
- )+ cat(s)+ |
+ ||
440 | +! | +
+ return(invisible(s)) |
||
339 | +441 |
} |
||
340 | +442 | |||
341 | +443 |
-
+ #' Prints `teal_module` |
||
342 | +444 |
- #' Get module depth+ #' @param x `teal_module` |
||
343 | +445 |
- #'+ #' @param ... parameters passed to `toString` |
||
344 | +446 |
- #' Depth starts at 0, so a single `teal.module` has depth 0.+ #' @export |
||
345 | +447 |
- #' Nesting it increases overall depth by 1.+ #' @rdname module |
||
346 | +448 |
- #'+ print.teal_module <- print.teal_modules |
347 | +1 |
- #' @inheritParams init+ #' Create a `tdata` Object |
|
348 | +2 |
- #' @param depth optional, integer determining current depth level+ #' |
|
349 | +3 |
- #'+ #' @description `r lifecycle::badge("deprecated")` |
|
350 | +4 |
- #' @return depth level for given module+ #' Create a new object called `tdata` which contains `data`, a `reactive` list of data.frames |
|
351 | +5 |
- #' @keywords internal+ #' (or `MultiAssayExperiment`), with attributes: |
|
352 | +6 |
- #'+ #' \itemize{ |
|
353 | +7 |
- #' @examples+ #' \item{`code` (`reactive`) containing code used to generate the data} |
|
354 | +8 |
- #' mods <- modules(+ #' \item{join_keys (`join_keys`) containing the relationships between the data} |
|
355 | +9 |
- #' label = "d1",+ #' \item{metadata (`named list`) containing any metadata associated with the data frames} |
|
356 | +10 |
- #' modules(+ #' } |
|
357 | +11 |
- #' label = "d2",+ #' @name tdata |
|
358 | +12 |
- #' modules(+ #' @param data A `named list` of `data.frames` (or `MultiAssayExperiment`) |
|
359 | +13 |
- #' label = "d3",+ #' which optionally can be `reactive`. |
|
360 | +14 |
- #' module(label = "aaa1"), module(label = "aaa2"), module(label = "aaa3")+ #' Inside this object all of these items will be made `reactive`. |
|
361 | +15 |
- #' ),+ #' @param code A `character` (or `reactive` which evaluates to a `character`) containing |
|
362 | +16 |
- #' module(label = "bbb")+ #' the code used to generate the data. This should be `reactive` if the code is changing |
|
363 | +17 |
- #' ),+ #' during a reactive context (e.g. if filtering changes the code). Inside this |
|
364 | +18 |
- #' module(label = "ccc")+ #' object `code` will be made reactive |
|
365 | +19 |
- #' )+ #' @param join_keys A `teal.data::join_keys` object containing relationships between the |
|
366 | +20 |
- #' stopifnot(teal:::modules_depth(mods) == 3L)+ #' datasets. |
|
367 | +21 |
- #'+ #' @param metadata A `named list` each element contains a list of metadata about the named data.frame |
|
368 | +22 |
- #' mods <- modules(+ #' Each element of these list should be atomic and length one. |
|
369 | +23 |
- #' label = "a",+ #' @return A `tdata` object |
|
370 | +24 |
- #' modules(+ #' |
|
371 | +25 |
- #' label = "b1", module(label = "c")+ #' @seealso `as_tdata` |
|
372 | +26 |
- #' ),+ #' |
|
373 | +27 |
- #' module(label = "b2")+ #' @examples |
|
374 | +28 |
- #' )+ #' |
|
375 | +29 |
- #' stopifnot(teal:::modules_depth(mods) == 2L)+ #' data <- new_tdata( |
|
376 | +30 |
- modules_depth <- function(modules, depth = 0L) {+ #' data = list(iris = iris, mtcars = reactive(mtcars), dd = data.frame(x = 1:10)), |
|
377 | -12x | +||
31 | +
- checkmate::assert(+ #' code = "iris <- iris |
||
378 | -12x | +||
32 | +
- checkmate::check_class(modules, "teal_module"),+ #' mtcars <- mtcars |
||
379 | -12x | +||
33 | +
- checkmate::check_class(modules, "teal_modules")+ #' dd <- data.frame(x = 1:10)", |
||
380 | +34 |
- )+ #' metadata = list(dd = list(author = "NEST"), iris = list(version = 1)) |
|
381 | -12x | +||
35 | +
- checkmate::assert_int(depth, lower = 0)+ #' ) |
||
382 | -11x | +||
36 | +
- if (inherits(modules, "teal_modules")) {+ #' |
||
383 | -4x | +||
37 | +
- max(vapply(modules$children, modules_depth, integer(1), depth = depth + 1L))+ #' # Extract a data.frame |
||
384 | +38 |
- } else {+ #' isolate(data[["iris"]]()) |
|
385 | -7x | +||
39 | +
- depth+ #' |
||
386 | +40 |
- }+ #' # Get code |
|
387 | +41 |
- }+ #' isolate(get_code_tdata(data)) |
|
388 | +42 |
-
+ #' |
|
389 | +43 |
-
+ #' # Get metadata |
|
390 | +44 |
- module_labels <- function(modules) {+ #' get_metadata(data, "iris") |
|
391 | -! | +||
45 | +
- if (inherits(modules, "teal_modules")) {+ #' |
||
392 | -! | +||
46 | +
- lapply(modules$children, module_labels)+ #' @export |
||
393 | +47 |
- } else {+ new_tdata <- function(data, code = "", join_keys = NULL, metadata = NULL) { |
|
394 | -! | +||
48 | +34x |
- modules$label+ lifecycle::deprecate_soft( |
|
395 | -+ | ||
49 | +34x |
- }+ when = "0.99.0", |
|
396 | -+ | ||
50 | +34x |
- }+ what = "tdata()", |
|
397 | -+ | ||
51 | +34x |
-
+ details = paste( |
|
398 | -+ | ||
52 | +34x |
- #' Converts `teal_modules` to a string+ "tdata is deprecated and will be removed in the next release. Use `teal_data` instead.\n", |
|
399 | -+ | ||
53 | +34x |
- #'+ "Please follow migration instructions https://github.com/insightsengineering/teal/discussions/987." |
|
400 | +54 |
- #' @param x (`teal_modules`) to print+ ) |
|
401 | +55 |
- #' @param indent (`integer`) indent level;+ ) |
|
402 | -+ | ||
56 | +34x |
- #' each `submodule` is indented one level more+ checkmate::assert_list( |
|
403 | -+ | ||
57 | +34x |
- #' @param ... (optional) additional parameters to pass to recursive calls of `toString`+ data, |
|
404 | -+ | ||
58 | +34x |
- #' @return (`character`)+ any.missing = FALSE, names = "unique", |
|
405 | -+ | ||
59 | +34x |
- #' @export+ types = c("data.frame", "reactive", "MultiAssayExperiment") |
|
406 | +60 |
- #' @rdname modules+ ) |
|
407 | -+ | ||
61 | +30x |
- toString.teal_modules <- function(x, indent = 0, ...) { # nolint+ checkmate::assert_class(join_keys, "join_keys", null.ok = TRUE)+ |
+ |
62 | +29x | +
+ checkmate::assert_multi_class(code, c("character", "reactive")) |
|
408 | +63 |
- # argument must be `x` to be consistent with base method+ |
|
409 | -! | +||
64 | +28x |
- paste(c(+ checkmate::assert_list(metadata, names = "unique", null.ok = TRUE) |
|
410 | -! | +||
65 | +26x |
- paste0(rep(" ", indent), "+ ", x$label),+ checkmate::assert_subset(names(metadata), names(data)) |
|
411 | -! | +||
66 | +
- unlist(lapply(x$children, toString, indent = indent + 1, ...))+ |
||
412 | -! | +||
67 | +25x |
- ), collapse = "\n")+ if (is.reactive(code)) {+ |
+ |
68 | +9x | +
+ isolate(checkmate::assert_class(code(), "character", .var.name = "code")) |
|
413 | +69 |
- }+ } |
|
414 | +70 | ||
415 | +71 |
- #' Converts `teal_module` to a string+ # create reactive data.frames |
|
416 | -+ | ||
72 | +24x |
- #'+ for (x in names(data)) { |
|
417 | -+ | ||
73 | +47x |
- #' @inheritParams toString.teal_modules+ if (!is.reactive(data[[x]])) { |
|
418 | -+ | ||
74 | +31x |
- #' @param x `teal_module`+ data[[x]] <- do.call(reactive, list(as.name(x)), envir = list2env(data[x])) |
|
419 | +75 |
- #' @param ... ignored+ } |
|
420 | +76 |
- #' @export+ } |
|
421 | +77 |
- #' @rdname module+ |
|
422 | +78 |
- toString.teal_module <- function(x, indent = 0, ...) { # nolint+ # set attributes |
|
423 | -! | +||
79 | +24x |
- paste0(paste(rep(" ", indent), collapse = ""), "+ ", x$label, collapse = "")+ attr(data, "code") <- if (is.reactive(code)) code else reactive(code) |
|
424 | -+ | ||
80 | +24x |
- }+ attr(data, "join_keys") <- join_keys |
|
425 | -+ | ||
81 | +24x |
-
+ attr(data, "metadata") <- metadata |
|
426 | +82 |
- #' Prints `teal_modules`+ |
|
427 | +83 |
- #' @param x `teal_modules`+ # set class |
|
428 | -+ | ||
84 | +24x |
- #' @param ... parameters passed to `toString`+ class(data) <- c("tdata", class(data)) |
|
429 | -+ | ||
85 | +24x |
- #' @export+ data |
|
430 | +86 |
- #' @rdname modules+ } |
|
431 | +87 |
- print.teal_modules <- function(x, ...) {- |
- |
432 | -! | -
- s <- toString(x, ...)- |
- |
433 | -! | -
- cat(s)- |
- |
434 | -! | -
- return(invisible(s))+ |
|
435 | +88 |
- }+ #' Function to convert a `tdata` object to an `environment` |
|
436 | +89 |
-
+ #' Any `reactives` inside `tdata` are first evaluated |
|
437 | +90 |
- #' Prints `teal_module`+ #' @param data a `tdata` object |
|
438 | +91 |
- #' @param x `teal_module`+ #' @return an `environment` |
|
439 | +92 |
- #' @param ... parameters passed to `toString`+ #' @examples |
|
440 | +93 |
- #' @export+ #' |
|
441 | +94 |
- #' @rdname module+ #' data <- new_tdata( |
|
442 | +95 |
- print.teal_module <- print.teal_modules+ #' data = list(iris = iris, mtcars = reactive(mtcars)), |
1 | +96 |
- #' Create a UI of nested tabs of `teal_modules`+ #' code = "iris <- iris |
|
2 | +97 |
- #'+ #' mtcars = mtcars" |
|
3 | +98 |
- #' @section `ui_nested_tabs`:+ #' ) |
|
4 | +99 |
- #' Each `teal_modules` is translated to a `tabsetPanel` and each+ #' |
|
5 | +100 |
- #' of its children is another tab-module called recursively. The UI of a+ #' my_env <- isolate(tdata2env(data)) |
|
6 | +101 |
- #' `teal_module` is obtained by calling the `ui` function on it.+ #' |
|
7 | +102 |
- #'+ #' @export |
|
8 | +103 |
- #' The `datasets` argument is required to resolve the teal arguments in an+ tdata2env <- function(data) { # nolint |
|
9 | -+ | ||
104 | +2x |
- #' isolated context (with respect to reactivity)+ checkmate::assert_class(data, "tdata") |
|
10 | -+ | ||
105 | +1x |
- #'+ list2env(lapply(data, function(x) if (is.reactive(x)) x() else x)) |
|
11 | +106 |
- #' @section `srv_nested_tabs`:+ } |
|
12 | +107 |
- #' This module calls recursively all elements of the `modules` returns one which+ |
|
13 | +108 |
- #' is currently active.+ |
|
14 | +109 |
- #' - `teal_module` returns self as a active module.+ #' Wrapper for `get_code.tdata` |
|
15 | +110 |
- #' - `teal_modules` also returns module active within self which is determined by the `input$active_tab`.+ #' This wrapper is to be used by downstream packages to extract the code of a `tdata` object |
|
16 | +111 |
#' |
|
17 | +112 |
- #' @name module_nested_tabs+ #' @param data (`tdata`) object |
|
18 | +113 |
#' |
|
19 | +114 |
- #' @inheritParams module_tabs_with_filters+ #' @return (`character`) code used in the `tdata` object. |
|
20 | +115 |
- #'+ #' @export |
|
21 | +116 |
- #' @param depth (`integer(1)`)\cr+ get_code_tdata <- function(data) { |
|
22 | -+ | ||
117 | +7x |
- #' number which helps to determine depth of the modules nesting.+ checkmate::assert_class(data, "tdata") |
|
23 | -+ | ||
118 | +5x |
- #' @param is_module_specific (`logical(1)`)\cr+ attr(data, "code")() |
|
24 | +119 |
- #' flag determining if the filter panel is global or module-specific.+ } |
|
25 | +120 |
- #' When set to `TRUE`, a filter panel is called inside of each module tab.+ |
|
26 | +121 |
- #' @return depending on class of `modules`, `ui_nested_tabs` returns:+ #' Extract `join_keys` from `tdata` |
|
27 | +122 |
- #' - `teal_module`: instantiated UI of the module+ #' @param data A `tdata` object |
|
28 | +123 |
- #' - `teal_modules`: `tabsetPanel` with each tab corresponding to recursively+ #' @param ... Additional arguments (not used) |
|
29 | +124 |
- #' calling this function on it.\cr+ #' @export |
|
30 | +125 |
- #' `srv_nested_tabs` returns a reactive which returns the active module that corresponds to the selected tab.+ join_keys.tdata <- function(data, ...) { |
|
31 | -+ | ||
126 | +2x |
- #'+ attr(data, "join_keys") |
|
32 | +127 |
- #' @examples+ } |
|
33 | +128 |
- #' mods <- teal:::example_modules()+ |
|
34 | +129 |
- #' datasets <- teal:::example_datasets()+ |
|
35 | +130 |
- #' app <- shinyApp(+ #' Function to get metadata from a `tdata` object |
|
36 | +131 |
- #' ui = function() {+ #' @param data `tdata` - object to extract the data from |
|
37 | +132 |
- #' tagList(+ #' @param dataname `character(1)` the dataset name whose metadata is requested |
|
38 | +133 |
- #' teal:::include_teal_css_js(),+ #' @return Either list of metadata or NULL if no metadata |
|
39 | +134 |
- #' textOutput("info"),+ #' @export |
|
40 | +135 |
- #' fluidPage( # needed for nice tabs+ get_metadata <- function(data, dataname) { |
|
41 | -+ | ||
136 | +4x |
- #' teal:::ui_nested_tabs("dummy", modules = mods, datasets = datasets)+ checkmate::assert_string(dataname)+ |
+ |
137 | +4x | +
+ UseMethod("get_metadata", data) |
|
42 | +138 |
- #' )+ } |
|
43 | +139 |
- #' )+ |
|
44 | +140 |
- #' },+ #' @rdname get_metadata |
|
45 | +141 |
- #' server = function(input, output, session) {+ #' @export |
|
46 | +142 |
- #' active_module <- teal:::srv_nested_tabs(+ get_metadata.tdata <- function(data, dataname) {+ |
+ |
143 | +4x | +
+ metadata <- attr(data, "metadata")+ |
+ |
144 | +4x | +
+ if (is.null(metadata)) {+ |
+ |
145 | +1x | +
+ return(NULL) |
|
47 | +146 |
- #' "dummy",+ }+ |
+ |
147 | +3x | +
+ metadata[[dataname]] |
|
48 | +148 |
- #' datasets = datasets,+ } |
|
49 | +149 |
- #' modules = mods+ |
|
50 | +150 |
- #' )+ #' @rdname get_metadata |
|
51 | +151 |
- #' output$info <- renderText({+ #' @export |
|
52 | +152 |
- #' paste0("The currently active tab name is ", active_module()$label)+ get_metadata.default <- function(data, dataname) {+ |
+ |
153 | +! | +
+ stop("get_metadata function not implemented for this object") |
|
53 | +154 |
- #' })+ } |
|
54 | +155 |
- #' }+ |
|
55 | +156 |
- #' )+ |
|
56 | +157 |
- #' if (interactive()) {+ #' Downgrade `teal_data` objects in modules for compatibility. |
|
57 | +158 |
- #' shinyApp(app$ui, app$server)+ #' |
|
58 | +159 |
- #' }+ #' Convert `teal_data` to `tdata` in `teal` modules. |
|
59 | +160 |
- #' @keywords internal+ #' |
|
60 | +161 |
- NULL+ #' Recent changes in `teal` cause modules to fail because modules expect a `tdata` object |
|
61 | +162 |
-
+ #' to be passed to the `data` argument but instead they receive a `teal_data` object, |
|
62 | +163 |
- #' @rdname module_nested_tabs+ #' which is additionally wrapped in a reactive expression in the server functions. |
|
63 | +164 |
- ui_nested_tabs <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) {+ #' In order to easily adapt such modules without a proper refactor, |
|
64 | -! | +||
165 | +
- checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"))+ #' use this function to downgrade the `data` argument. |
||
65 | -! | +||
166 | +
- checkmate::assert_count(depth)+ #' |
||
66 | -! | +||
167 | +
- UseMethod("ui_nested_tabs", modules)+ #' @param x data object, either `tdata` or `teal_data`, the latter possibly in a reactive expression |
||
67 | +168 |
- }+ #' |
|
68 | +169 |
-
+ #' @return Object of class `tdata`. |
|
69 | +170 |
- #' @rdname module_nested_tabs+ #' |
|
70 | +171 |
- #' @export+ #' @examples |
|
71 | +172 |
- ui_nested_tabs.default <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) {+ #' td <- teal_data() |
|
72 | -! | +||
173 | +
- stop("Modules class not supported: ", paste(class(modules), collapse = " "))+ #' td <- within(td, iris <- iris) %>% within(mtcars <- mtcars) |
||
73 | +174 |
- }+ #' td |
|
74 | +175 |
-
+ #' as_tdata(td) |
|
75 | +176 |
- #' @rdname module_nested_tabs+ #' as_tdata(reactive(td)) |
|
76 | +177 |
- #' @export+ #' |
|
77 | +178 |
- ui_nested_tabs.teal_modules <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) {+ #' @export |
|
78 | -! | +||
179 | +
- checkmate::assert_list(datasets, types = c("list", "FilteredData"))+ #' @rdname tdata_deprecation |
||
79 | -! | +||
180 | +
- ns <- NS(id)+ #' |
||
80 | -! | +||
181 | +
- do.call(+ as_tdata <- function(x) { |
||
81 | -! | +||
182 | +8x |
- tabsetPanel,+ if (inherits(x, "tdata")) { |
|
82 | -! | +||
183 | +2x |
- c(+ return(x) |
|
83 | +184 |
- # by giving an id, we can reactively respond to tab changes+ } |
|
84 | -! | -
- list(+ | |
185 | +6x | +
+ if (is.reactive(x)) { |
|
85 | -! | +||
186 | +1x |
- id = ns("active_tab"),+ checkmate::assert_class(isolate(x()), "teal_data") |
|
86 | -! | +||
187 | +1x |
- type = if (modules$label == "root") "pills" else "tabs"+ datanames <- isolate(teal_data_datanames(x())) |
|
87 | -+ | ||
188 | +1x |
- ),+ datasets <- sapply(datanames, function(dataname) reactive(x()[[dataname]]), simplify = FALSE) |
|
88 | -! | +||
189 | +1x |
- lapply(+ code <- reactive(teal.code::get_code(x())) |
|
89 | -! | +||
190 | +1x |
- names(modules$children),+ join_keys <- isolate(teal.data::join_keys(x())) |
|
90 | -! | +||
191 | +5x |
- function(module_id) {+ } else if (inherits(x, "teal_data")) { |
|
91 | -! | +||
192 | +5x |
- module_label <- modules$children[[module_id]]$label+ datanames <- teal_data_datanames(x) |
|
92 | -! | +||
193 | +5x |
- tabPanel(+ datasets <- sapply(datanames, function(dataname) reactive(x[[dataname]]), simplify = FALSE) |
|
93 | -! | +||
194 | +5x |
- title = module_label,+ code <- reactive(teal.code::get_code(x)) |
|
94 | -! | +||
195 | +5x |
- value = module_id, # when clicked this tab value changes input$<tabset panel id>+ join_keys <- isolate(teal.data::join_keys(x)) |
|
95 | -! | +||
196 | +
- ui_nested_tabs(+ } |
||
96 | -! | +||
197 | +
- id = ns(module_id),+ |
||
97 | -! | +||
198 | +6x |
- modules = modules$children[[module_id]],+ new_tdata(data = datasets, code = code, join_keys = join_keys) |
|
98 | -! | +||
199 | +
- datasets = datasets[[module_label]],+ } |
||
99 | -! | +
1 | +
- depth = depth + 1L,+ #' Create a `teal` module for previewing a report |
||
100 | -! | +||
2 | +
- is_module_specific = is_module_specific+ #' |
||
101 | +3 |
- )+ #' @description `r lifecycle::badge("experimental")` |
|
102 | +4 |
- )+ #' This function wraps [teal.reporter::reporter_previewer_ui()] and |
|
103 | +5 |
- }+ #' [teal.reporter::reporter_previewer_srv()] into a `teal_module` to be |
|
104 | +6 |
- )+ #' used in `teal` applications. |
|
105 | +7 |
- )+ #' |
|
106 | +8 |
- )+ #' If you are creating a `teal` application using [teal::init()] then this |
|
107 | +9 |
- }+ #' module will be added to your application automatically if any of your `teal modules` |
|
108 | +10 |
-
+ #' support report generation. |
|
109 | +11 |
- #' @rdname module_nested_tabs+ #' |
|
110 | +12 |
- #' @export+ #' @inheritParams module |
|
111 | +13 |
- ui_nested_tabs.teal_module <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) {+ #' @param server_args (`named list`)\cr |
|
112 | -! | +||
14 | +
- checkmate::assert_class(datasets, classes = "FilteredData")+ #' Arguments passed to [teal.reporter::reporter_previewer_srv()]. |
||
113 | -! | +||
15 | +
- ns <- NS(id)+ #' @return `teal_module` (extended with `teal_module_previewer` class) containing the `teal.reporter` previewer |
||
114 | +16 |
-
+ #' functionality. |
|
115 | -! | +||
17 | +
- args <- c(list(id = ns("module")), modules$ui_args)+ #' @export |
||
116 | +18 |
-
+ reporter_previewer_module <- function(label = "Report previewer", server_args = list()) { |
|
117 | -! | +||
19 | +4x |
- teal_ui <- tags$div(+ checkmate::assert_string(label) |
|
118 | -! | +||
20 | +2x |
- id = id,+ checkmate::assert_list(server_args, names = "named") |
|
119 | -! | +||
21 | +2x |
- class = "teal_module",+ checkmate::assert_true(all(names(server_args) %in% names(formals(teal.reporter::reporter_previewer_srv)))) |
|
120 | -! | +||
22 | +
- uiOutput(ns("data_reactive"), inline = TRUE),+ |
||
121 | -! | +||
23 | +2x |
- tagList(+ logger::log_info("Initializing reporter_previewer_module") |
|
122 | -! | +||
24 | +
- if (depth >= 2L) div(style = "mt-6"),+ + |
+ ||
25 | +2x | +
+ srv <- function(id, reporter, ...) { |
|
123 | +26 | ! |
- do.call(modules$ui, args)+ teal.reporter::reporter_previewer_srv(id, reporter, ...) |
124 | +27 |
- )+ } |
|
125 | +28 |
- )+ |
|
126 | -+ | ||
29 | +2x |
-
+ ui <- function(id, ...) { |
|
127 | +30 | ! |
- if (!is.null(modules$datanames) && is_module_specific) {+ teal.reporter::reporter_previewer_ui(id, ...) |
128 | -! | +||
31 | +
- fluidRow(+ } |
||
129 | -! | +||
32 | +
- column(width = 9, teal_ui, class = "teal_primary_col"),+ |
||
130 | -! | +||
33 | +2x |
- column(+ module <- module( |
|
131 | -! | +||
34 | +2x |
- width = 3,+ label = "temporary label", |
|
132 | -! | +||
35 | +2x |
- datasets$ui_filter_panel(ns("module_filter_panel")),+ server = srv, ui = ui, |
|
133 | -! | +||
36 | +2x |
- class = "teal_secondary_col"+ server_args = server_args, ui_args = list(), datanames = NULL |
|
134 | +37 |
- )+ ) |
|
135 | +38 |
- )+ # Module is created with a placeholder label and the label is changed later. |
|
136 | +39 |
- } else {+ # This is to prevent another module being labeled "Report previewer". |
|
137 | -! | +||
40 | +2x |
- teal_ui+ class(module) <- c("teal_module_previewer", class(module)) |
|
138 | -+ | ||
41 | +2x |
- }+ module$label <- label |
|
139 | -+ | ||
42 | +2x |
- }+ module |
|
140 | +43 |
-
+ } |
141 | +1 |
- #' @rdname module_nested_tabs+ #' Get Client Timezone |
||
142 | +2 |
- srv_nested_tabs <- function(id, datasets, modules, is_module_specific = FALSE,+ #' |
||
143 | +3 |
- reporter = teal.reporter::Reporter$new()) {- |
- ||
144 | -50x | -
- checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"))- |
- ||
145 | -50x | -
- checkmate::assert_class(reporter, "Reporter")- |
- ||
146 | -49x | -
- UseMethod("srv_nested_tabs", modules)+ #' Local timezone in the browser may differ from the system timezone from the server. |
||
147 | +4 |
- }+ #' This script can be run to register a shiny input which contains information about |
||
148 | +5 |
-
+ #' the timezone in the browser. |
||
149 | +6 |
- #' @rdname module_nested_tabs+ #' |
||
150 | +7 |
- #' @export+ #' @param ns (`function`) namespace function passed from the `session` object in the |
||
151 | +8 |
- srv_nested_tabs.default <- function(id, datasets, modules, is_module_specific = FALSE,+ #' Shiny server. For Shiny modules this will allow for proper name spacing of the |
||
152 | +9 |
- reporter = teal.reporter::Reporter$new()) {+ #' registered input. |
||
153 | -! | +|||
10 | +
- stop("Modules class not supported: ", paste(class(modules), collapse = " "))+ #' |
|||
154 | +11 |
- }+ #' @return (`Shiny`) input variable accessible with `input$tz` which is a (`character`) |
||
155 | +12 |
-
+ #' string containing the timezone of the browser/client. |
||
156 | +13 |
- #' @rdname module_nested_tabs+ #' @keywords internal |
||
157 | +14 |
- #' @export+ get_client_timezone <- function(ns) { |
||
158 | -+ | |||
15 | +18x |
- srv_nested_tabs.teal_modules <- function(id, datasets, modules, is_module_specific = FALSE,+ script <- sprintf( |
||
159 | -+ | |||
16 | +18x |
- reporter = teal.reporter::Reporter$new()) {+ "Shiny.setInputValue(`%s`, Intl.DateTimeFormat().resolvedOptions().timeZone)", |
||
160 | -22x | +17 | +18x |
- checkmate::assert_list(datasets, types = c("list", "FilteredData"))+ ns("timezone") |
161 | +18 |
-
+ ) |
||
162 | -22x | +19 | +18x |
- moduleServer(id = id, module = function(input, output, session) {+ shinyjs::runjs(script) # function does not return anything |
163 | -22x | +20 | +18x |
- logger::log_trace("srv_nested_tabs.teal_modules initializing the module { deparse1(modules$label) }.")+ return(invisible(NULL)) |
164 | +21 |
-
+ } |
||
165 | -22x | +|||
22 | +
- labels <- vapply(modules$children, `[[`, character(1), "label")+ |
|||
166 | -22x | +|||
23 | +
- modules_reactive <- sapply(+ #' Resolve the expected bootstrap theme |
|||
167 | -22x | +|||
24 | +
- names(modules$children),+ #' @keywords internal |
|||
168 | -22x | +|||
25 | +
- function(module_id) {+ get_teal_bs_theme <- function() { |
|||
169 | -33x | +26 | +16x |
- srv_nested_tabs(+ bs_theme <- getOption("teal.bs_theme") |
170 | -33x | +27 | +16x |
- id = module_id,+ if (is.null(bs_theme)) { |
171 | -33x | +28 | +13x |
- datasets = datasets[[labels[module_id]]],+ NULL |
172 | -33x | +29 | +3x |
- modules = modules$children[[module_id]],+ } else if (!inherits(bs_theme, "bs_theme")) { |
173 | -33x | +30 | +2x |
- is_module_specific = is_module_specific,+ warning("teal.bs_theme has to be of a bslib::bs_theme class, the default shiny bootstrap is used.") |
174 | -33x | +31 | +2x |
- reporter = reporter+ NULL |
175 | +32 |
- )+ } else { |
||
176 | -+ | |||
33 | +1x |
- },+ bs_theme |
||
177 | -22x | +|||
34 | +
- simplify = FALSE+ } |
|||
178 | +35 |
- )+ } |
||
179 | +36 | |||
180 | +37 |
- # when not ready input$active_tab would return NULL - this would fail next reactive+ include_parent_datanames <- function(dataname, join_keys) { |
||
181 | -22x | +38 | +3x |
- input_validated <- eventReactive(input$active_tab, input$active_tab, ignoreNULL = TRUE)+ parents <- character(0) |
182 | -22x | +39 | +3x |
- get_active_module <- reactive({+ for (i in dataname) { |
183 | -12x | +40 | +6x |
- if (length(modules$children) == 1L) {+ while (length(i) > 0) { |
184 | -+ | |||
41 | +6x |
- # single tab is active by default+ parent_i <- teal.data::parent(join_keys, i) |
||
185 | -1x | +42 | +6x |
- modules_reactive[[1]]()+ parents <- c(parent_i, parents)+ |
+
43 | +6x | +
+ i <- parent_i |
||
186 | +44 |
- } else {+ } |
||
187 | +45 |
- # switch to active tab+ }+ |
+ ||
46 | ++ | + | ||
188 | -11x | +47 | +3x |
- modules_reactive[[input_validated()]]()+ return(unique(c(parents, dataname))) |
189 | +48 |
- }+ } |
||
190 | +49 |
- })+ |
||
191 | +50 | |||
192 | -22x | +|||
51 | +
- get_active_module+ |
|||
193 | +52 |
- })+ #' Create a `FilteredData` |
||
194 | +53 |
- }+ #' |
||
195 | +54 |
-
+ #' Create a `FilteredData` object from a `teal_data` object |
||
196 | +55 |
- #' @rdname module_nested_tabs+ #' @param x (`teal_data`) object |
||
197 | +56 |
- #' @export+ #' @param datanames (`character`) vector of data set names to include; must be subset of `datanames(x)` |
||
198 | +57 |
- srv_nested_tabs.teal_module <- function(id, datasets, modules, is_module_specific = TRUE,+ #' @return (`FilteredData`) object |
||
199 | +58 |
- reporter = teal.reporter::Reporter$new()) {+ #' @keywords internal+ |
+ ||
59 | ++ |
+ teal_data_to_filtered_data <- function(x, datanames = teal_data_datanames(x)) { |
||
200 | -27x | +60 | +13x |
- checkmate::assert_class(datasets, "FilteredData")+ checkmate::assert_class(x, "teal_data") |
201 | -27x | +61 | +13x |
- logger::log_trace("srv_nested_tabs.teal_module initializing the module: { deparse1(modules$label) }.")+ checkmate::assert_character(datanames, min.chars = 1L, any.missing = FALSE) |
202 | +62 | |||
203 | -27x | +63 | +13x |
- moduleServer(id = id, module = function(input, output, session) {+ ans <- teal.slice::init_filtered_data( |
204 | -27x | +64 | +13x |
- if (!is.null(modules$datanames) && is_module_specific) {+ x = sapply(datanames, function(dn) x[[dn]], simplify = FALSE), |
205 | -! | +|||
65 | +13x |
- datasets$srv_filter_panel("module_filter_panel")+ join_keys = teal.data::join_keys(x) |
||
206 | +66 |
- }+ ) |
||
207 | +67 |
-
+ # Piggy-back entire pre-processing code so that filtering code can be appended later. |
||
208 | -- |
- # Create two triggers to limit reactivity between filter-panel and modules.+ | ||
68 | +13x | +
+ attr(ans, "preprocessing_code") <- teal.code::get_code(x) |
||
209 | -+ | |||
69 | +13x |
- # We want to recalculate only visible modules+ ans |
||
210 | +70 |
- # - trigger the data when the tab is selected+ } |
||
211 | +71 |
- # - trigger module to be called when the tab is selected for the first time+ |
||
212 | -27x | +|||
72 | +
- trigger_data <- reactiveVal(1L)+ #' Template Function for `TealReportCard` Creation and Customization |
|||
213 | -27x | +|||
73 | +
- trigger_module <- reactiveVal(NULL)+ #' |
|||
214 | -27x | +|||
74 | +
- output$data_reactive <- renderUI({+ #' This function generates a report card with a title, |
|||
215 | -17x | +|||
75 | +
- lapply(datasets$datanames(), function(x) {+ #' an optional description, and the option to append the filter state list. |
|||
216 | -21x | +|||
76 | +
- datasets$get_data(x, filtered = TRUE)+ #' |
|||
217 | +77 |
- })+ #' @param title (`character(1)`) title of the card (unless overwritten by label) |
||
218 | -17x | +|||
78 | +
- isolate(trigger_data(trigger_data() + 1))+ #' @param label (`character(1)`) label provided by the user when adding the card |
|||
219 | -17x | +|||
79 | +
- isolate(trigger_module(TRUE))+ #' @param description (`character(1)`) optional additional description |
|||
220 | +80 |
-
+ #' @param with_filter (`logical(1)`) flag indicating to add filter state |
||
221 | -17x | +|||
81 | +
- NULL+ #' @param filter_panel_api (`FilterPanelAPI`) object with API that allows the generation |
|||
222 | +82 |
- })+ #' of the filter state in the report |
||
223 | +83 |
-
+ #' |
||
224 | +84 |
- # collect arguments to run teal_module+ #' @return (`TealReportCard`) populated with a title, description and filter state |
||
225 | -27x | +|||
85 | +
- args <- c(list(id = "module"), modules$server_args)+ #' |
|||
226 | -27x | +|||
86 | +
- if (is_arg_used(modules$server, "reporter")) {+ #' @export |
|||
227 | -! | +|||
87 | +
- args <- c(args, list(reporter = reporter))+ report_card_template <- function(title, label, description = NULL, with_filter, filter_panel_api) { |
|||
228 | -+ | |||
88 | +2x |
- }+ checkmate::assert_string(title) |
||
229 | -+ | |||
89 | +2x |
-
+ checkmate::assert_string(label) |
||
230 | -27x | +90 | +2x |
- if (is_arg_used(modules$server, "datasets")) {+ checkmate::assert_string(description, null.ok = TRUE) |
231 | -1x | +91 | +2x |
- args <- c(args, datasets = datasets)+ checkmate::assert_flag(with_filter) |
232 | -+ | |||
92 | +2x |
- }+ checkmate::assert_class(filter_panel_api, classes = "FilterPanelAPI") |
||
233 | +93 | |||
234 | -27x | +94 | +2x |
- if (is_arg_used(modules$server, "data")) {+ card <- teal::TealReportCard$new() |
235 | -7x | +95 | +2x |
- data <- eventReactive(trigger_data(), .datasets_to_data(modules, datasets))+ title <- if (label == "") title else label |
236 | -7x | +96 | +2x |
- args <- c(args, data = list(data))+ card$set_name(title) |
237 | -+ | |||
97 | +2x |
- }+ card$append_text(title, "header2") |
||
238 | -+ | |||
98 | +1x |
-
+ if (!is.null(description)) card$append_text(description, "header3") |
||
239 | -27x | +99 | +1x |
- if (is_arg_used(modules$server, "filter_panel_api")) {+ if (with_filter) card$append_fs(filter_panel_api$get_filter_state()) |
240 | +100 | 2x |
- filter_panel_api <- teal.slice::FilterPanelAPI$new(datasets)+ card |
|
241 | -2x | +|||
101 | +
- args <- c(args, filter_panel_api = filter_panel_api)+ } |
|||
242 | +102 |
- }+ #' Resolve `datanames` for the modules |
||
243 | +103 |
-
+ #' |
||
244 | +104 |
- # observe the trigger_module above to induce the module once the renderUI is triggered+ #' Modifies `module$datanames` to include names of the parent dataset (taken from `join_keys`). |
||
245 | -27x | +|||
105 | +
- observeEvent(+ #' When `datanames` is set to `"all"` it is replaced with all available datasets names. |
|||
246 | -27x | +|||
106 | +
- ignoreNULL = TRUE,+ #' @param modules (`teal_modules`) object |
|||
247 | -27x | +|||
107 | +
- once = TRUE,+ #' @param datanames (`character`) names of datasets available in the `data` object |
|||
248 | -27x | +|||
108 | +
- eventExpr = trigger_module(),+ #' @param join_keys (`join_keys`) object |
|||
249 | -27x | +|||
109 | +
- handlerExpr = {+ #' @return `teal_modules` with resolved `datanames` |
|||
250 | -17x | +|||
110 | +
- module_output <- if (is_arg_used(modules$server, "id")) {+ #' @keywords internal |
|||
251 | -17x | +|||
111 | +
- do.call(modules$server, args)+ resolve_modules_datanames <- function(modules, datanames, join_keys) { |
|||
252 | -+ | |||
112 | +! |
- } else {+ if (inherits(modules, "teal_modules")) { |
||
253 | +113 | ! |
- do.call(callModule, c(args, list(module = modules$server)))+ modules$children <- sapply( |
|
254 | -+ | |||
114 | +! |
- }+ modules$children, |
||
255 | -+ | |||
115 | +! |
- }+ resolve_modules_datanames, |
||
256 | -+ | |||
116 | +! |
- )+ simplify = FALSE, |
||
257 | -+ | |||
117 | +! |
-
+ datanames = datanames, |
||
258 | -27x | +|||
118 | +! |
- reactive(modules)+ join_keys = join_keys |
||
259 | +119 |
- })+ ) |
||
260 | -+ | |||
120 | +! |
- }+ modules |
||
261 | +121 |
-
+ } else { |
||
262 | -+ | |||
122 | +! |
- #' Convert `FilteredData` to reactive list of datasets of the `teal_data` type.+ modules$datanames <- if (identical(modules$datanames, "all")) { |
||
263 | -+ | |||
123 | +! |
- #'+ datanames |
||
264 | -+ | |||
124 | +! |
- #' Converts `FilteredData` object to `teal_data` object containing datasets needed for a specific module.+ } else if (is.character(modules$datanames)) { |
||
265 | -+ | |||
125 | +! |
- #' Please note that if module needs dataset which has a parent, then parent will be also returned.+ extra_datanames <- setdiff(modules$datanames, datanames) |
||
266 | -+ | |||
126 | +! |
- #' A hash per `dataset` is calculated internally and returned in the code.+ if (length(extra_datanames)) { |
||
267 | -+ | |||
127 | +! |
- #'+ stop( |
||
268 | -+ | |||
128 | +! |
- #' @param module (`teal_module`) module where needed filters are taken from+ sprintf( |
||
269 | -+ | |||
129 | +! |
- #' @param datasets (`FilteredData`) object where needed data are taken from+ "Module %s has datanames that are not available in a 'data':\n %s not in %s", |
||
270 | -+ | |||
130 | +! |
- #' @return A `teal_data` object.+ modules$label,+ |
+ ||
131 | +! | +
+ toString(extra_datanames),+ |
+ ||
132 | +! | +
+ toString(datanames) |
||
271 | +133 |
- #'+ ) |
||
272 | +134 |
- #' @keywords internal+ ) |
||
273 | +135 |
- .datasets_to_data <- function(module, datasets) {+ } |
||
274 | -4x | +|||
136 | +! |
- checkmate::assert_class(module, "teal_module")+ datanames_adjusted <- intersect(modules$datanames, datanames) |
||
275 | -4x | +|||
137 | +! |
- checkmate::assert_class(datasets, "FilteredData")+ include_parent_datanames(dataname = datanames_adjusted, join_keys = join_keys) |
||
276 | +138 |
-
+ } |
||
277 | -4x | +|||
139 | +! |
- datanames <- if (is.null(module$datanames) || identical(module$datanames, "all")) {+ modules |
||
278 | -1x | +|||
140 | +
- datasets$datanames()+ } |
|||
279 | +141 |
- } else {+ } |
||
280 | -3x | +|||
142 | +
- unique(module$datanames) # todo: include parents! unique shouldn't be needed here!+ |
|||
281 | +143 |
- }+ #' Check `datanames` in modules |
||
282 | +144 |
-
+ #' |
||
283 | +145 |
- # list of reactive filtered data+ #' This function ensures specified `datanames` in modules match those in the data object, |
||
284 | -4x | +|||
146 | +
- data <- sapply(datanames, function(x) datasets$get_data(x, filtered = TRUE), simplify = FALSE)+ #' returning error messages or `TRUE` for successful validation. |
|||
285 | +147 |
-
+ #' |
||
286 | -4x | +|||
148 | +
- hashes <- calculate_hashes(datanames, datasets)+ #' @param modules (`teal_modules`) object |
|||
287 | +149 |
-
+ #' @param datanames (`character`) names of datasets available in the `data` object |
||
288 | -4x | +|||
150 | +
- code <- c(+ #' |
|||
289 | -4x | +|||
151 | +
- get_rcode_str_install(),+ #' @return A `character(1)` containing error message or `TRUE` if validation passes. |
|||
290 | -4x | +|||
152 | +
- get_rcode_libraries(),+ #' @keywords internal+ |
+ |||
153 | ++ |
+ check_modules_datanames <- function(modules, datanames) { |
||
291 | -4x | +154 | +17x |
- get_datasets_code(datanames, datasets, hashes)+ checkmate::assert_class(modules, "teal_modules") |
292 | -+ | |||
155 | +17x |
- )+ checkmate::assert_character(datanames) |
||
293 | +156 | |||
294 | -4x | +157 | +17x |
- do.call(+ recursive_check_datanames <- function(modules, datanames) {+ |
+
158 | ++ |
+ # check teal_modules against datanames |
||
295 | -4x | +159 | +36x |
- teal.data::teal_data,+ if (inherits(modules, "teal_modules")) { |
296 | -4x | +160 | +17x |
- args = c(data, code = list(code), join_keys = list(datasets$get_join_keys()[datanames]))+ sapply(modules$children, function(module) recursive_check_datanames(module, datanames = datanames)) |
297 | +161 |
- )+ } else { |
||
298 | -+ | |||
162 | +19x |
- }+ extra_datanames <- setdiff(modules$datanames, c("all", datanames)) |
||
299 | -+ | |||
163 | +19x |
-
+ if (length(extra_datanames)) { |
||
300 | -+ | |||
164 | +2x |
- #' Get the hash of a dataset+ sprintf( |
||
301 | -+ | |||
165 | +2x |
- #'+ "- Module '%s' uses datanames not available in 'data': (%s) not in (%s)", |
||
302 | -+ | |||
166 | +2x |
- #' @param datanames (`character`) names of datasets+ modules$label, |
||
303 | -+ | |||
167 | +2x |
- #' @param datasets (`FilteredData`) object holding the data+ toString(dQuote(extra_datanames, q = FALSE)), |
||
304 | -+ | |||
168 | +2x |
- #'+ toString(dQuote(datanames, q = FALSE)) |
||
305 | +169 |
- #' @return A list of hashes per dataset+ ) |
||
306 | +170 |
- #' @keywords internal+ } |
||
307 | +171 |
- #'+ } |
||
308 | +172 |
- calculate_hashes <- function(datanames, datasets) {+ } |
||
309 | -7x | +173 | +17x |
- sapply(datanames, function(x) rlang::hash(datasets$get_data(x, filtered = FALSE)), simplify = FALSE)+ check_datanames <- unlist(recursive_check_datanames(modules, datanames)) |
310 | -+ | |||
174 | +17x |
- }+ if (length(check_datanames)) { |
1 | -+ | |||
175 | +2x |
- #' @title `TealReportCard`+ paste(check_datanames, collapse = "\n") |
||
2 | +176 |
- #' @description `r lifecycle::badge("experimental")`+ } else { |
||
3 | -+ | |||
177 | +15x |
- #' A child of [`ReportCard`] that is used for teal specific applications.+ TRUE |
||
4 | +178 |
- #' In addition to the parent methods, it supports rendering teal specific elements such as+ } |
||
5 | +179 |
- #' the source code, the encodings panel content and the filter panel content as part of the+ } |
||
6 | +180 |
- #' meta data.+ |
||
7 | +181 |
- #' @export+ #' Check `datanames` in filters |
||
8 | +182 |
#' |
||
9 | +183 |
- TealReportCard <- R6::R6Class( # nolint: object_name_linter.+ #' This function checks whether `datanames` in filters correspond to those in `data`, |
||
10 | +184 |
- classname = "TealReportCard",+ #' returning character vector with error messages or TRUE if all checks pass. |
||
11 | +185 |
- inherit = teal.reporter::ReportCard,+ #' |
||
12 | +186 |
- public = list(+ #' @param filters (`teal_slices`) object |
||
13 | +187 |
- #' @description Appends the source code to the `content` meta data of this `TealReportCard`.+ #' @param datanames (`character`) names of datasets available in the `data` object |
||
14 | +188 |
- #'+ #' |
||
15 | -- |
- #' @param src (`character(1)`) code as text.- |
- ||
16 | -- |
- #' @param ... any `rmarkdown` R chunk parameter and its value.- |
- ||
17 | +189 |
- #' But `eval` parameter is always set to `FALSE`.+ #' @return A `character(1)` containing error message or TRUE if validation passes. |
||
18 | +190 |
- #' @return invisibly self+ #' @keywords internal |
||
19 | +191 |
- #' @examples+ check_filter_datanames <- function(filters, datanames) { |
||
20 | -+ | |||
192 | +15x |
- #' card <- TealReportCard$new()$append_src(+ checkmate::assert_class(filters, "teal_slices") |
||
21 | -+ | |||
193 | +15x |
- #' "plot(iris)"+ checkmate::assert_character(datanames) |
||
22 | +194 |
- #' )+ |
||
23 | +195 |
- #' card$get_content()[[1]]$get_content()+ # check teal_slices against datanames |
||
24 | -+ | |||
196 | +15x |
- append_src = function(src, ...) {+ out <- unlist(sapply( |
||
25 | -4x | +197 | +15x |
- checkmate::assert_character(src, min.len = 0, max.len = 1)+ filters, function(filter) { |
26 | -4x | +198 | +3x |
- params <- list(...)+ dataname <- shiny::isolate(filter$dataname) |
27 | -4x | +199 | +3x |
- params$eval <- FALSE+ if (!dataname %in% datanames) { |
28 | -4x | +200 | +2x |
- rblock <- RcodeBlock$new(src)+ sprintf( |
29 | -4x | +201 | +2x |
- rblock$set_params(params)+ "- Filter '%s' refers to dataname not available in 'data':\n %s not in (%s)", |
30 | -4x | +202 | +2x |
- self$append_content(rblock)+ shiny::isolate(filter$id), |
31 | -4x | +203 | +2x |
- self$append_metadata("SRC", src)+ dQuote(dataname, q = FALSE), |
32 | -4x | +204 | +2x |
- invisible(self)+ toString(dQuote(datanames, q = FALSE)) |
33 | +205 |
- },+ ) |
||
34 | +206 |
- #' @description Appends the filter state list to the `content` and `metadata` of this `TealReportCard`.+ } |
||
35 | +207 |
- #' If the filter state list has an attribute named `formatted`, it appends it to the card otherwise it uses+ } |
||
36 | +208 |
- #' the default `yaml::as.yaml` to format the list.+ )) |
||
37 | +209 |
- #' If the filter state list is empty, nothing is appended to the `content`.+ |
||
38 | +210 |
- #'+ |
||
39 | -+ | |||
211 | +15x |
- #' @param fs (`teal_slices`) object returned from [teal_slices()] function.+ if (length(out)) { |
||
40 | -+ | |||
212 | +2x |
- #' @return invisibly self+ paste(out, collapse = "\n") |
||
41 | +213 |
- append_fs = function(fs) {- |
- ||
42 | -5x | -
- checkmate::assert_class(fs, "teal_slices")- |
- ||
43 | -4x | -
- self$append_text("Filter State", "header3")+ } else { |
||
44 | -4x | +214 | +13x |
- self$append_content(TealSlicesBlock$new(fs))+ TRUE |
45 | -4x | +|||
215 | +
- invisible(self)+ } |
|||
46 | +216 |
- },+ } |
||
47 | +217 |
- #' @description Appends the encodings list to the `content` and `metadata` of this `TealReportCard`.+ |
||
48 | +218 |
- #'+ #' Wrapper on `teal.data::datanames` |
||
49 | +219 |
- #' @param encodings (`list`) list of encodings selections of the teal app.+ #' |
||
50 | +220 |
- #' @return invisibly self+ #' Special function used in internals of `teal` to return names of datasets even if `datanames` |
||
51 | +221 |
- #' @examples+ #' has not been set. |
||
52 | +222 |
- #' card <- TealReportCard$new()$append_encodings(list(variable1 = "X"))+ #' @param data (`teal_data`) |
||
53 | +223 |
- #' card$get_content()[[1]]$get_content()+ #' @return `character` |
||
54 | +224 |
- #'+ #' @keywords internal |
||
55 | +225 |
- append_encodings = function(encodings) {+ teal_data_datanames <- function(data) { |
||
56 | -4x | +226 | +66x |
- checkmate::assert_list(encodings)+ checkmate::assert_class(data, "teal_data") |
57 | -4x | +227 | +66x |
- self$append_text("Selected Options", "header3")+ if (length(teal.data::datanames(data))) { |
58 | -4x | +228 | +62x |
- if (requireNamespace("yaml", quietly = TRUE)) {+ teal.data::datanames(data) |
59 | -4x | +|||
229 | +
- self$append_text(yaml::as.yaml(encodings, handlers = list(+ } else { |
|||
60 | +230 | 4x |
- POSIXct = function(x) format(x, "%Y-%m-%d"),+ ls(teal.code::get_env(data), all.names = TRUE) |
|
61 | -4x | +|||
231 | +
- POSIXlt = function(x) format(x, "%Y-%m-%d"),+ } |
|||
62 | -4x | +|||
232 | +
- Date = function(x) format(x, "%Y-%m-%d")+ } |
|||
63 | -4x | +|||
233 | +
- )), "verbatim")+ |
|||
64 | +234 |
- } else {+ #' Function for validating the title parameter of `teal::init` |
||
65 | -! | +|||
235 | +
- stop("yaml package is required to format the encodings list")+ #' |
|||
66 | +236 |
- }+ #' Checks if the input of the title from `teal::init` will create a valid title and favicon tag. |
||
67 | -4x | +|||
237 | +
- self$append_metadata("Encodings", encodings)+ #' @param shiny_tag (`shiny.tag`) Object to validate for a valid title. |
|||
68 | -4x | +|||
238 | +
- invisible(self)+ #' @keywords internal |
|||
69 | +239 |
- }+ validate_app_title_tag <- function(shiny_tag) { |
||
70 | -+ | |||
240 | +19x |
- ),+ checkmate::assert_class(shiny_tag, "shiny.tag") |
||
71 | -+ | |||
241 | +19x |
- private = list()+ checkmate::assert_true(shiny_tag$name == "head") |
||
72 | -+ | |||
242 | +18x |
- )+ child_names <- vapply(shiny_tag$children, `[[`, character(1L), "name") |
||
73 | -+ | |||
243 | +18x |
-
+ checkmate::assert_subset(c("title", "link"), child_names, .var.name = "child tags") |
||
74 | -+ | |||
244 | +16x |
- #' @title `RcodeBlock`+ rel_attr <- shiny_tag$children[[which(child_names == "link")]]$attribs$rel |
||
75 | -+ | |||
245 | +16x |
- #' @keywords internal+ checkmate::assert_subset( |
||
76 | -+ | |||
246 | +16x |
- TealSlicesBlock <- R6::R6Class( # nolint: object_name_linter.+ rel_attr, |
||
77 | -+ | |||
247 | +16x |
- classname = "TealSlicesBlock",+ c("icon", "shortcut icon"),+ |
+ ||
248 | +16x | +
+ .var.name = "Link tag's rel attribute",+ |
+ ||
249 | +16x | +
+ empty.ok = FALSE |
||
78 | +250 |
- inherit = teal.reporter:::TextBlock,+ ) |
||
79 | +251 |
- public = list(+ } |
||
80 | +252 |
- #' @description Returns a `TealSlicesBlock` object.+ |
||
81 | +253 |
- #'+ #' Build app title with favicon |
||
82 | +254 |
- #' @details Returns a `TealSlicesBlock` object with no content and no parameters.+ #' |
||
83 | +255 |
- #'+ #' A helper function to create the browser title along with a logo. |
||
84 | +256 |
- #' @param content (`teal_slices`) object returned from [teal_slices()] function.+ #' |
||
85 | +257 |
- #' @param style (`character(1)`) string specifying style to apply.+ #' @param title (`character`) The browser title for the teal app |
||
86 | +258 |
- #'+ #' @param favicon (`character`) The path for the icon for the title. |
||
87 | +259 |
- #' @return `TealSlicesBlock`+ #' The image/icon path can be remote or the static path accessible by shiny, like the `www/` |
||
88 | +260 |
- #' @examples+ #' |
||
89 | +261 |
- #' block <- teal:::TealSlicesBlock$new()+ #' @return A `shiny.tag` containing the element that adds the title and logo to the shiny app |
||
90 | +262 |
- #'+ #' @export |
||
91 | +263 |
- initialize = function(content = teal_slices(), style = "verbatim") {+ build_app_title <- function(title = "teal app", favicon = "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/nest.png") { # nolint |
||
92 | -10x | +264 | +16x |
- self$set_content(content)+ checkmate::assert_string(title, null.ok = TRUE) |
93 | -9x | +265 | +16x |
- self$set_style(style)+ checkmate::assert_string(favicon, null.ok = TRUE) |
94 | -9x | +266 | +16x |
- invisible(self)+ tags$head( |
95 | -+ | |||
267 | +16x |
- },+ tags$title(title), |
||
96 | -+ | |||
268 | +16x |
-
+ tags$link( |
||
97 | -+ | |||
269 | +16x |
- #' @description Sets content of this `TealSlicesBlock`.+ rel = "icon", |
||
98 | -+ | |||
270 | +16x |
- #' Sets content as `YAML` text which represents a list generated from `teal_slices`.+ href = favicon, |
||
99 | -+ | |||
271 | +16x |
- #' The list displays limited number of fields from `teal_slice` objects, but this list is+ sizes = "any" |
||
100 | +272 |
- #' sufficient to conclude which filters were applied.+ ) |
||
101 | +273 |
- #' When `selected` field in `teal_slice` object is a range, then it is displayed as a "min"+ ) |
||
102 | +274 |
- #'+ } |
||
103 | +275 |
- #'+ |
||
104 | +276 |
- #' @param content (`teal_slices`) object returned from [teal_slices()] function.+ #' Application ID |
||
105 | +277 |
- #' @return invisibly self+ #' |
||
106 | +278 |
- set_content = function(content) {+ #' Creates App ID used to match filter snapshots to application. |
||
107 | -11x | +|||
279 | +
- checkmate::assert_class(content, "teal_slices")+ #' |
|||
108 | -10x | +|||
280 | +
- if (length(content) != 0) {+ #' Calculate app ID that will be used to stamp filter state snapshots. |
|||
109 | -7x | +|||
281 | +
- states_list <- lapply(content, function(x) {+ #' App ID is a hash of the app's data and modules. |
|||
110 | -7x | +|||
282 | +
- x_list <- shiny::isolate(as.list(x))+ #' See "transferring snapshots" section in ?snapshot. |
|||
111 | -7x | +|||
283 | +
- if (+ #' |
|||
112 | -7x | +|||
284 | +
- inherits(x_list$choices, c("integer", "numeric", "Date", "POSIXct", "POSIXlt")) &&+ #' @param data `teal_data` or `teal_data_module` as accepted by `init` |
|||
113 | -7x | +|||
285 | +
- length(x_list$choices) == 2 &&+ #' @param modules `teal_modules` object as accepted by `init` |
|||
114 | -7x | +|||
286 | +
- length(x_list$selected) == 2+ #' |
|||
115 | +287 |
- ) {+ #' @return A single character string. |
||
116 | -! | +|||
288 | +
- x_list$range <- paste(x_list$selected, collapse = " - ")+ #' |
|||
117 | -! | +|||
289 | +
- x_list["selected"] <- NULL+ #' @keywords internal |
|||
118 | +290 |
- }+ create_app_id <- function(data, modules) { |
||
119 | -7x | -
- if (!is.null(x_list$arg)) {- |
- ||
120 | -! | +291 | +14x |
- x_list$arg <- if (x_list$arg == "subset") "Genes" else "Samples"+ checkmate::assert_multi_class(data, c("teal_data", "teal_data_module")) |
121 | -+ | |||
292 | +14x |
- }+ checkmate::assert_class(modules, "teal_modules") |
||
122 | +293 | |||
123 | -7x | +294 | +14x |
- x_list <- x_list[+ hashables <- c(data, modules) |
124 | -7x | +295 | +14x |
- c("dataname", "varname", "experiment", "arg", "expr", "selected", "range", "keep_na", "keep_inf")+ hashables$data <- if (inherits(hashables$data, "teal_data")) { |
125 | -+ | |||
296 | +! |
- ]+ as.list(hashables$data@env) |
||
126 | -7x | +297 | +14x |
- names(x_list) <- c(+ } else if (inherits(data, "teal_data_module")) { |
127 | -7x | +298 | +1x |
- "Dataset name", "Variable name", "Experiment", "Filtering by", "Applied expression",+ body(data$server)+ |
+
299 | ++ |
+ } |
||
128 | -7x | +300 | +14x |
- "Selected Values", "Selected range", "Include NA values", "Include Inf values"+ rlang::hash(hashables) |
129 | +301 |
- )+ } |
130 | +1 |
-
+ #' Filter manager modal |
|
131 | -7x | +||
2 | +
- Filter(Negate(is.null), x_list)+ #' |
||
132 | +3 |
- })+ #' Opens modal containing the filter manager UI. |
|
133 | +4 |
-
+ #' |
|
134 | -7x | +||
5 | +
- if (requireNamespace("yaml", quietly = TRUE)) {+ #' @name module_filter_manager_modal |
||
135 | -7x | +||
6 | +
- super$set_content(yaml::as.yaml(states_list))+ #' @inheritParams filter_manager_srv |
||
136 | +7 |
- } else {+ #' @examples |
|
137 | -! | +||
8 | +
- stop("yaml package is required to format the filter state list")+ #' fd1 <- teal.slice::init_filtered_data(list(iris = list(dataset = iris))) |
||
138 | +9 |
- }+ #' fd2 <- teal.slice::init_filtered_data( |
|
139 | +10 |
- }+ #' list(iris = list(dataset = iris), mtcars = list(dataset = mtcars)) |
|
140 | -10x | +||
11 | +
- private$teal_slices <- content+ #' ) |
||
141 | -10x | +||
12 | +
- invisible(self)+ #' fd3 <- teal.slice::init_filtered_data( |
||
142 | +13 |
- },+ #' list(iris = list(dataset = iris), women = list(dataset = women)) |
|
143 | +14 |
- #' @description Create the `RcodeBlock` from a list.+ #' ) |
|
144 | +15 |
- #' @param x `named list` with two fields `c("text", "params")`.+ #' filter <- teal_slices( |
|
145 | +16 |
- #' Use the `get_available_params` method to get all possible parameters.- |
- |
146 | -- |
- #' @return invisibly self- |
- |
147 | -- |
- from_list = function(x) {- |
- |
148 | -1x | -
- checkmate::assert_list(x)- |
- |
149 | -1x | -
- checkmate::assert_names(names(x), must.include = c("teal_slices"))- |
- |
150 | -1x | -
- self$set_content(x$teal_slices)- |
- |
151 | -1x | -
- invisible(self)- |
- |
152 | -- |
- },- |
- |
153 | -- |
- #' @description Convert the `RcodeBlock` to a list.- |
- |
154 | -- |
- #' @return `named list` with a text and `params`.- |
- |
155 | -- | - - | -|
156 | -- |
- to_list = function() {- |
- |
157 | -2x | -
- list(teal_slices = private$teal_slices)- |
- |
158 | -- |
- }- |
- |
159 | -- |
- ),- |
- |
160 | -- |
- private = list(- |
- |
161 | -- |
- style = "verbatim",- |
- |
162 | -- |
- teal_slices = NULL # teal_slices- |
- |
163 | -- |
- )- |
- |
164 | -- |
- )- |
-
1 | -- |
- #' Filter manager modal- |
-
2 | -- |
- #'- |
-
3 | -- |
- #' Opens modal containing the filter manager UI.- |
-
4 | -- |
- #'- |
-
5 | -- |
- #' @name module_filter_manager_modal- |
-
6 | -- |
- #' @inheritParams filter_manager_srv- |
-
7 | -- |
- #' @examples- |
-
8 | -- |
- #' fd1 <- teal.slice::init_filtered_data(list(iris = list(dataset = iris)))- |
-
9 | -- |
- #' fd2 <- teal.slice::init_filtered_data(- |
-
10 | -- |
- #' list(iris = list(dataset = iris), mtcars = list(dataset = mtcars))- |
-
11 | -- |
- #' )- |
-
12 | -- |
- #' fd3 <- teal.slice::init_filtered_data(- |
-
13 | -- |
- #' list(iris = list(dataset = iris), women = list(dataset = women))- |
-
14 | -- |
- #' )- |
-
15 | -- |
- #' filter <- teal_slices(- |
-
16 | -- |
- #' teal.slice::teal_slice(dataname = "iris", varname = "Sepal.Length"),+ #' teal.slice::teal_slice(dataname = "iris", varname = "Sepal.Length"), |
1 |
- #' Add right filter panel into each of the top-level `teal_modules` UIs.+ #' Store teal_slices object to a file |
||
3 |
- #' The [ui_nested_tabs] function returns a nested tabbed UI corresponding+ #' This function takes a `teal_slices` object and saves it to a file in `JSON` format. |
||
4 |
- #' to the nested modules.+ #' The `teal_slices` object contains information about filter states and can be used to |
||
5 |
- #' This function adds the right filter panel to each main tab.+ #' create, modify, and delete filter states. The saved file can be later loaded using |
||
6 |
- #'+ #' the `slices_restore` function. |
||
7 |
- #' The right filter panel's filter choices affect the `datasets` object. Therefore,+ #' |
||
8 |
- #' all modules using the same `datasets` share the same filters.+ #' @param tss (`teal_slices`) object to be stored. |
||
9 |
- #'+ #' @param file (`character(1)`) The file path where `teal_slices` object will be saved. |
||
10 |
- #' This works with nested modules of depth greater than 2, though the filter+ #' The file extension should be `".json"`. |
||
11 |
- #' panel is inserted at the right of the modules at depth 1 and not at the leaves.+ #' |
||
12 |
- #'+ #' @details `Date` class is stored in `"ISO8601"` format (`YYYY-MM-DD`). `POSIX*t` classes are converted to a |
||
13 |
- #' @name module_tabs_with_filters+ #' character by using `format.POSIX*t(usetz = TRUE, tz = "UTC")` (`YYYY-MM-DD {N}{N}:{N}{N}:{N}{N} UTC`, where |
||
14 |
- #'+ #' `{N} = [0-9]` is a number and `UTC` is `Coordinated Universal Time` timezone short-code). |
||
15 |
- #' @inheritParams module_teal+ #' This format is assumed during `slices_restore`. All `POSIX*t` objects in `selected` or `choices` fields of |
||
16 |
- #'+ #' `teal_slice` objects are always printed in `UTC` timezone as well. |
||
17 |
- #' @param datasets (`named list` of `FilteredData`)\cr+ #' |
||
18 |
- #' object to store filter state and filtered datasets, shared across modules. For more+ #' @return `NULL`, invisibly. |
||
19 |
- #' details see [`teal.slice::FilteredData`]. Structure of the list must be the same as structure+ #' |
||
20 |
- #' of the `modules` argument and list names must correspond to the labels in `modules`.+ #' @keywords internal |
||
21 |
- #' When filter is not module-specific then list contains the same object in all elements.+ #' |
||
22 |
- #' @param reporter (`Reporter`) object from `teal.reporter`+ #' @examples |
||
23 |
- #'+ #' # Create a teal_slices object |
||
24 |
- #' @return A `tagList` of The main menu, place holders for filters and+ #' tss <- teal_slices( |
||
25 |
- #' place holders for the teal modules+ #' teal_slice(dataname = "data", varname = "var"), |
||
26 |
- #'+ #' teal_slice(dataname = "data", expr = "x > 0", id = "positive_x", title = "Positive x") |
||
27 |
- #'+ #' ) |
||
28 |
- #' @keywords internal+ #' |
||
29 |
- #'+ #' if (interactive()) { |
||
30 |
- #' @examples+ #' # Store the teal_slices object to a file |
||
31 |
- #'+ #' slices_store(tss, "path/to/file.json") |
||
32 |
- #' mods <- teal:::example_modules()+ #' } |
||
33 |
- #' datasets <- teal:::example_datasets()+ #' |
||
34 |
- #'+ slices_store <- function(tss, file) { |
||
35 | -+ | 9x |
- #' app <- shinyApp(+ checkmate::assert_class(tss, "teal_slices") |
36 | -+ | 9x |
- #' ui = function() {+ checkmate::assert_path_for_output(file, overwrite = TRUE, extension = "json") |
37 |
- #' tagList(+ |
||
38 | -+ | 9x |
- #' teal:::include_teal_css_js(),+ cat(format(tss, trim_lines = FALSE), "\n", file = file) |
39 |
- #' textOutput("info"),+ } |
||
40 |
- #' fluidPage( # needed for nice tabs+ |
||
41 |
- #' ui_tabs_with_filters("dummy", modules = mods, datasets = datasets)+ #' Restore teal_slices object from a file |
||
42 |
- #' )+ #' |
||
43 |
- #' )+ #' This function takes a file path to a `JSON` file containing a `teal_slices` object |
||
44 |
- #' },+ #' and restores it to its original form. The restored `teal_slices` object can be used |
||
45 |
- #' server = function(input, output, session) {+ #' to access filter states and their corresponding attributes. |
||
46 |
- #' output$info <- renderText({+ #' |
||
47 |
- #' paste0("The currently active tab name is ", active_module()$label)+ #' @param file Path to file where `teal_slices` is stored. Must have a `.json` extension and read access. |
||
48 |
- #' })+ #' |
||
49 |
- #' active_module <- srv_tabs_with_filters(id = "dummy", datasets = datasets, modules = mods)+ #' @return A `teal_slices` object restored from the file. |
||
50 |
- #' }+ #' |
||
51 |
- #' )+ #' @keywords internal |
||
52 |
- #' if (interactive()) {+ #' |
||
53 |
- #' shinyApp(app$ui, app$server)+ #' @examples |
||
54 |
- #' }+ #' if (interactive()) { |
||
55 |
- #'+ #' # Restore a teal_slices object from a file |
||
56 |
- NULL+ #' tss_restored <- slices_restore("path/to/file.json") |
||
57 |
-
+ #' } |
||
58 |
- #' @rdname module_tabs_with_filters+ #' |
||
59 |
- ui_tabs_with_filters <- function(id, modules, datasets, filter = teal_slices()) {+ slices_restore <- function(file) { |
||
60 | -! | +9x |
- checkmate::assert_class(modules, "teal_modules")+ checkmate::assert_file_exists(file, access = "r", extension = "json") |
61 | -! | +
- checkmate::assert_list(datasets, types = c("list", "FilteredData"))+ |
|
62 | -! | +9x |
- checkmate::assert_class(filter, "teal_slices")+ tss_json <- jsonlite::fromJSON(file, simplifyDataFrame = FALSE) |
63 | -+ | 9x |
-
+ tss_json$slices <- |
64 | -! | +9x |
- ns <- NS(id)+ lapply(tss_json$slices, function(slice) { |
65 | -! | +9x |
- is_module_specific <- isTRUE(attr(filter, "module_specific"))+ for (field in c("selected", "choices")) { |
66 | -+ | 18x |
-
+ if (!is.null(slice[[field]])) { |
67 | -! | +12x |
- teal_ui <- ui_nested_tabs(ns("root"), modules = modules, datasets, is_module_specific = is_module_specific)+ if (length(slice[[field]]) > 0) { |
68 | -! | +9x |
- filter_panel_btns <- tags$li(+ date_partial_regex <- "^[0-9]{4}-[0-9]{2}-[0-9]{2}" |
69 | -! | +9x |
- class = "flex-grow",+ time_stamp_regex <- paste0(date_partial_regex, "\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\sUTC$") |
70 | -! | +
- tags$button(+ |
|
71 | -! | +9x |
- class = "btn action-button filter_hamburger", # see sidebar.css for style filter_hamburger+ slice[[field]] <- |
72 | -! | +9x |
- href = "javascript:void(0)",+ if (all(grepl(paste0(date_partial_regex, "$"), slice[[field]]))) { |
73 | -! | +3x |
- onclick = "toggleFilterPanel();", # see sidebar.js+ as.Date(slice[[field]]) |
74 | -! | +9x |
- title = "Toggle filter panels",+ } else if (all(grepl(time_stamp_regex, slice[[field]]))) { |
75 | -! | +3x |
- icon("fas fa-bars")+ as.POSIXct(slice[[field]], tz = "UTC") |
76 |
- ),+ } else { |
||
77 | -! | +3x |
- filter_manager_modal_ui(ns("filter_manager"))+ slice[[field]] |
78 |
- )+ } |
||
79 | -! | +
- teal_ui$children[[1]] <- tagAppendChild(teal_ui$children[[1]], filter_panel_btns)+ } else { |
|
80 | -+ | 3x |
-
+ slice[[field]] <- character(0) |
81 | -! | +
- if (!is_module_specific) {+ } |
|
82 |
- # need to rearrange html so that filter panel is within tabset+ } |
||
83 | -! | +
- tabset_bar <- teal_ui$children[[1]]+ } |
|
84 | -! | +9x |
- teal_modules <- teal_ui$children[[2]]+ slice |
85 | -! | +
- filter_ui <- unlist(datasets)[[1]]$ui_filter_panel(ns("filter_panel"))+ }) |
|
86 | -! | +
- list(+ |
|
87 | -! | +9x |
- tabset_bar,+ tss_elements <- lapply(tss_json$slices, as.teal_slice) |
88 | -! | +
- tags$hr(class = "my-2"),+ |
|
89 | -! | -
- fluidRow(- |
- |
90 | -! | -
- column(width = 9, teal_modules, class = "teal_primary_col"),- |
- |
91 | -! | +9x |
- column(width = 3, filter_ui, class = "teal_secondary_col")+ do.call(teal_slices, c(tss_elements, tss_json$attributes)) |
92 | +90 |
- )+ } |
93 | +1 |
- )+ #' Include `CSS` files from `/inst/css/` package directory to application header |
||
94 | +2 |
- } else {- |
- ||
95 | -! | -
- teal_ui+ #' |
||
96 | +3 |
- }+ #' `system.file` should not be used to access files in other packages, it does |
||
97 | +4 |
- }+ #' not work with `devtools`. Therefore, we redefine this method in each package |
||
98 | +5 |
-
+ #' as needed. Thus, we do not export this method |
||
99 | +6 |
- #' @rdname module_tabs_with_filters+ #' |
||
100 | +7 |
- srv_tabs_with_filters <- function(id,+ #' @param pattern (`character`) pattern of files to be included |
||
101 | +8 |
- datasets,+ #' |
||
102 | +9 |
- modules,+ #' @return HTML code that includes `CSS` files |
||
103 | +10 |
- reporter = teal.reporter::Reporter$new(),+ #' @keywords internal |
||
104 | +11 |
- filter = teal_slices()) {+ include_css_files <- function(pattern = "*") { |
||
105 | -5x | +12 | +12x |
- checkmate::assert_class(modules, "teal_modules")+ css_files <- list.files( |
106 | -5x | +13 | +12x |
- checkmate::assert_list(datasets, types = c("list", "FilteredData"))+ system.file("css", package = "teal", mustWork = TRUE), |
107 | -5x | +14 | +12x |
- checkmate::assert_class(reporter, "Reporter")+ pattern = pattern, full.names = TRUE |
108 | -3x | +|||
15 | +
- checkmate::assert_class(filter, "teal_slices")+ ) |
|||
109 | -+ | |||
16 | +12x |
-
+ return( |
||
110 | -3x | +17 | +12x |
- moduleServer(id, function(input, output, session) {+ shiny::singleton( |
111 | -3x | +18 | +12x |
- logger::log_trace("srv_tabs_with_filters initializing the module.")+ shiny::tags$head(lapply(css_files, shiny::includeCSS)) |
112 | +19 |
-
+ ) |
||
113 | -3x | +|||
20 | +
- is_module_specific <- isTRUE(attr(filter, "module_specific"))+ ) |
|||
114 | -3x | +|||
21 | +
- manager_out <- filter_manager_modal_srv("filter_manager", filtered_data_list = datasets, filter = filter)+ } |
|||
115 | +22 | |||
116 | -3x | +|||
23 | +
- active_module <- srv_nested_tabs(+ #' Include `JS` files from `/inst/js/` package directory to application header |
|||
117 | -3x | +|||
24 | +
- id = "root",+ #' |
|||
118 | -3x | +|||
25 | +
- datasets = datasets,+ #' `system.file` should not be used to access files in other packages, it does |
|||
119 | -3x | +|||
26 | +
- modules = modules,+ #' not work with `devtools`. Therefore, we redefine this method in each package |
|||
120 | -3x | +|||
27 | +
- reporter = reporter,+ #' as needed. Thus, we do not export this method |
|||
121 | -3x | +|||
28 | +
- is_module_specific = is_module_specific+ #' |
|||
122 | +29 |
- )+ #' @param pattern (`character`) pattern of files to be included, passed to `system.file` |
||
123 | +30 |
-
+ #' @param except (`character`) vector of basename filenames to be excluded |
||
124 | -3x | +|||
31 | +
- if (!is_module_specific) {+ #' |
|||
125 | -3x | +|||
32 | +
- active_datanames <- reactive({- |
- |||
126 | -6x | -
- if (identical(active_module()$datanames, "all")) {- |
- ||
127 | -! | -
- singleton$datanames()+ #' @return HTML code that includes `JS` files |
||
128 | +33 |
- } else {- |
- ||
129 | -5x | -
- active_module()$datanames+ #' @keywords internal |
||
130 | +34 |
- }+ include_js_files <- function(pattern = NULL, except = NULL) { |
||
131 | -+ | |||
35 | +12x |
- })+ checkmate::assert_character(except, min.len = 1, any.missing = FALSE, null.ok = TRUE) |
||
132 | -3x | +36 | +12x |
- singleton <- unlist(datasets)[[1]]+ js_files <- list.files(system.file("js", package = "teal", mustWork = TRUE), pattern = pattern, full.names = TRUE) |
133 | -3x | +37 | +12x |
- singleton$srv_filter_panel("filter_panel", active_datanames = active_datanames)+ js_files <- js_files[!(basename(js_files) %in% except)] # no-op if except is NULL |
134 | +38 | |||
135 | -3x | +39 | +12x |
- observeEvent(+ return(singleton(lapply(js_files, includeScript))) |
136 | -3x | +|||
40 | +
- eventExpr = active_datanames(),+ } |
|||
137 | -3x | +|||
41 | +
- handlerExpr = {+ |
|||
138 | -4x | +|||
42 | +
- script <- if (length(active_datanames()) == 0 || is.null(active_datanames())) {+ #' Run `JS` file from `/inst/js/` package directory |
|||
139 | +43 |
- # hide the filter panel and disable the burger button+ #' |
||
140 | -! | +|||
44 | +
- "handleNoActiveDatasets();"+ #' This is triggered from the server to execute on the client |
|||
141 | +45 |
- } else {+ #' rather than triggered directly on the client. |
||
142 | +46 |
- # show the filter panel and enable the burger button+ #' Unlike `include_js_files` which includes `JavaScript` functions, |
||
143 | -4x | +|||
47 | +
- "handleActiveDatasetsPresent();"+ #' the `run_js` actually executes `JavaScript` functions. |
|||
144 | +48 |
- }+ #' |
||
145 | -4x | +|||
49 | +
- shinyjs::runjs(script)+ #' `system.file` should not be used to access files in other packages, it does |
|||
146 | +50 |
- },+ #' not work with `devtools`. Therefore, we redefine this method in each package |
||
147 | -3x | +|||
51 | +
- ignoreNULL = FALSE+ #' as needed. Thus, we do not export this method |
|||
148 | +52 |
- )+ #' |
||
149 | +53 |
- }+ #' @param files (`character`) vector of filenames |
||
150 | +54 |
-
+ #' @keywords internal+ |
+ ||
55 | ++ |
+ run_js_files <- function(files) { |
||
151 | -3x | +56 | +18x |
- showNotification("Data loaded - App fully started up")+ checkmate::assert_character(files, min.len = 1, any.missing = FALSE) |
152 | -3x | +57 | +18x |
- logger::log_trace("srv_tabs_with_filters initialized the module")+ lapply(files, function(file) { |
153 | -3x | +58 | +18x |
- return(active_module)+ shinyjs::runjs(paste0(readLines(system.file("js", file, package = "teal", mustWork = TRUE)), collapse = "\n")) |
154 | +59 |
}) |
||
60 | +18x | +
+ return(invisible(NULL))+ |
+ ||
155 | +61 |
} |
1 | +62 |
- #' Include `CSS` files from `/inst/css/` package directory to application header+ |
|
2 | +63 |
- #'+ #' Code to include teal `CSS` and `JavaScript` files |
|
3 | +64 |
- #' `system.file` should not be used to access files in other packages, it does+ #' |
|
4 | +65 |
- #' not work with `devtools`. Therefore, we redefine this method in each package+ #' This is useful when you want to use the same `JavaScript` and `CSS` files that are |
|
5 | +66 |
- #' as needed. Thus, we do not export this method+ #' used with the teal application. |
|
6 | +67 |
- #'+ #' This is also useful for running standalone modules in teal with the correct |
|
7 | +68 |
- #' @param pattern (`character`) pattern of files to be included+ #' styles. |
|
8 | +69 |
- #'+ #' Also initializes `shinyjs` so you can use it. |
|
9 | +70 |
- #' @return HTML code that includes `CSS` files+ #' |
|
10 | +71 |
- #' @keywords internal+ #' @return HTML code to include |
|
11 | +72 |
- include_css_files <- function(pattern = "*") {+ #' @examples |
|
12 | -12x | +||
73 | +
- css_files <- list.files(+ #' shiny_ui <- tagList( |
||
13 | -12x | +||
74 | +
- system.file("css", package = "teal", mustWork = TRUE),+ #' teal:::include_teal_css_js(), |
||
14 | -12x | +||
75 | +
- pattern = pattern, full.names = TRUE+ #' p("Hello") |
||
15 | +76 |
- )+ #' ) |
|
16 | -12x | +||
77 | +
- return(+ #' @keywords internal |
||
17 | -12x | +||
78 | +
- shiny::singleton(+ include_teal_css_js <- function() { |
||
18 | +79 | 12x |
- shiny::tags$head(lapply(css_files, shiny::includeCSS))+ tagList( |
19 | -+ | ||
80 | +12x |
- )+ shinyjs::useShinyjs(), |
|
20 | -+ | ||
81 | +12x |
- )+ include_css_files(), |
|
21 | +82 |
- }+ # init.js is executed from the server |
|
22 | -+ | ||
83 | +12x |
-
+ include_js_files(except = "init.js"), |
|
23 | -+ | ||
84 | +12x |
- #' Include `JS` files from `/inst/js/` package directory to application header+ shinyjs::hidden(icon("gear")), # add hidden icon to load font-awesome css for icons |
|
24 | +85 |
- #'+ ) |
|
25 | +86 |
- #' `system.file` should not be used to access files in other packages, it does+ } |
26 | +1 |
- #' not work with `devtools`. Therefore, we redefine this method in each package+ # This file contains Shiny modules useful for debugging and developing teal. |
|
27 | +2 |
- #' as needed. Thus, we do not export this method+ # We do not export the functions in this file. They are for |
|
28 | +3 |
- #'+ # developers only and can be accessed via `:::`. |
|
29 | +4 |
- #' @param pattern (`character`) pattern of files to be included, passed to `system.file`+ |
|
30 | +5 |
- #' @param except (`character`) vector of basename filenames to be excluded+ #' Dummy module to show the filter calls generated by the right encoding panel |
|
31 | +6 |
#' |
|
32 | +7 |
- #' @return HTML code that includes `JS` files+ #' |
|
33 | +8 |
- #' @keywords internal+ #' Please do not remove, this is useful for debugging teal without |
|
34 | +9 |
- include_js_files <- function(pattern = NULL, except = NULL) {- |
- |
35 | -12x | -
- checkmate::assert_character(except, min.len = 1, any.missing = FALSE, null.ok = TRUE)- |
- |
36 | -12x | -
- js_files <- list.files(system.file("js", package = "teal", mustWork = TRUE), pattern = pattern, full.names = TRUE)- |
- |
37 | -12x | -
- js_files <- js_files[!(basename(js_files) %in% except)] # no-op if except is NULL+ #' dependencies and simplifies `\link[devtools]{load_all}` which otherwise fails |
|
38 | +10 | - - | -|
39 | -12x | -
- return(singleton(lapply(js_files, includeScript)))+ #' and avoids session restarts! |
|
40 | +11 |
- }+ #' |
|
41 | +12 |
-
+ #' @param label `character` label of module |
|
42 | +13 |
- #' Run `JS` file from `/inst/js/` package directory+ #' @keywords internal |
|
43 | +14 |
#' |
|
44 | +15 |
- #' This is triggered from the server to execute on the client+ #' @examples |
|
45 | +16 |
- #' rather than triggered directly on the client.+ #' app <- init( |
|
46 | +17 |
- #' Unlike `include_js_files` which includes `JavaScript` functions,+ #' data = teal_data(iris = iris, mtcars = mtcars), |
|
47 | +18 |
- #' the `run_js` actually executes `JavaScript` functions.+ #' modules = teal:::filter_calls_module(), |
|
48 | +19 |
- #'+ #' header = "Simple teal app" |
|
49 | +20 |
- #' `system.file` should not be used to access files in other packages, it does+ #' ) |
|
50 | +21 |
- #' not work with `devtools`. Therefore, we redefine this method in each package+ #' if (interactive()) { |
|
51 | +22 |
- #' as needed. Thus, we do not export this method+ #' shinyApp(app$ui, app$server) |
|
52 | +23 |
- #'+ #' } |
|
53 | +24 |
- #' @param files (`character`) vector of filenames+ filter_calls_module <- function(label = "Filter Calls Module") { # nolint |
|
54 | -+ | ||
25 | +! |
- #' @keywords internal+ checkmate::assert_string(label) |
|
55 | +26 |
- run_js_files <- function(files) {+ |
|
56 | -18x | +||
27 | +! |
- checkmate::assert_character(files, min.len = 1, any.missing = FALSE)+ module( |
|
57 | -18x | +||
28 | +! |
- lapply(files, function(file) {+ label = label, |
|
58 | -18x | +||
29 | +! |
- shinyjs::runjs(paste0(readLines(system.file("js", file, package = "teal", mustWork = TRUE)), collapse = "\n"))+ server = function(input, output, session, data) { |
|
59 | -+ | ||
30 | +! |
- })+ checkmate::assert_class(data, "reactive") |
|
60 | -18x | +||
31 | +! |
- return(invisible(NULL))+ checkmate::assert_class(isolate(data()), "teal_data") |
|
61 | +32 |
- }+ |
|
62 | -+ | ||
33 | +! |
-
+ output$filter_calls <- renderText({ |
|
63 | -- |
- #' Code to include teal `CSS` and `JavaScript` files- |
- |
64 | -- |
- #'- |
- |
65 | -- |
- #' This is useful when you want to use the same `JavaScript` and `CSS` files that are- |
- |
66 | -- |
- #' used with the teal application.- |
- |
67 | -- |
- #' This is also useful for running standalone modules in teal with the correct- |
- |
68 | -- |
- #' styles.- |
- |
69 | -- |
- #' Also initializes `shinyjs` so you can use it.- |
- |
70 | -+ | ||
34 | +! |
- #'+ teal.data::get_code(data()) |
|
71 | +35 |
- #' @return HTML code to include+ }) |
|
72 | +36 |
- #' @examples+ }, |
|
73 | -+ | ||
37 | +! |
- #' shiny_ui <- tagList(+ ui = function(id, ...) { |
|
74 | -+ | ||
38 | +! |
- #' teal:::include_teal_css_js(),+ ns <- NS(id) |
|
75 | -+ | ||
39 | +! |
- #' p("Hello")+ div( |
|
76 | -+ | ||
40 | +! |
- #' )+ h2("The following filter calls are generated:"), |
|
77 | -+ | ||
41 | +! |
- #' @keywords internal+ verbatimTextOutput(ns("filter_calls")) |
|
78 | +42 |
- include_teal_css_js <- function() {- |
- |
79 | -12x | -
- tagList(- |
- |
80 | -12x | -
- shinyjs::useShinyjs(),- |
- |
81 | -12x | -
- include_css_files(),+ ) |
|
82 | +43 |
- # init.js is executed from the server- |
- |
83 | -12x | -
- include_js_files(except = "init.js"),+ }, |
|
84 | -12x | +||
44 | +! |
- shinyjs::hidden(icon("gear")), # add hidden icon to load font-awesome css for icons+ datanames = "all" |
|
85 | +45 |
) |
|
86 | +46 |
}@@ -21481,70 +21104,70 @@ teal coverage - 63.82% |
1 |
- #' Validate that dataset has a minimum number of observations+ # This file adds a splash screen for delayed data loading on top of teal |
||
2 |
- #'+ |
||
3 |
- #' @description `r lifecycle::badge("stable")`+ #' UI to show a splash screen in the beginning, then delegate to [srv_teal()] |
||
4 |
- #' @param x a data.frame+ #' |
||
5 |
- #' @param min_nrow minimum number of rows in \code{x}+ #' @description `r lifecycle::badge("stable")` |
||
6 |
- #' @param complete \code{logical} default \code{FALSE} when set to \code{TRUE} then complete cases are checked.+ #' The splash screen could be used to query for a password to fetch the data. |
||
7 |
- #' @param allow_inf \code{logical} default \code{TRUE} when set to \code{FALSE} then error thrown if any values are+ #' [init()] is a very thin wrapper around this module useful for end-users which |
||
8 |
- #' infinite.+ #' assumes that it is a top-level module and cannot be embedded. |
||
9 |
- #' @param msg (`character(1)`) additional message to display alongside the default message.+ #' This function instead adheres to the Shiny module conventions. |
||
11 |
- #' @details This function is a wrapper for `shiny::validate`.+ #' If data is obtained through delayed loading, its splash screen is used. Otherwise, |
||
12 |
- #'+ #' a default splash screen is shown. |
||
13 |
- #' @export+ #' |
||
14 |
- #'+ #' Please also refer to the doc of [init()]. |
||
15 |
- #' @examples+ #' |
||
16 |
- #' library(teal)+ #' @param id (`character(1)`)\cr |
||
17 |
- #' ui <- fluidPage(+ #' module id |
||
18 |
- #' sliderInput("len", "Max Length of Sepal",+ #' @inheritParams init |
||
19 |
- #' min = 4.3, max = 7.9, value = 5+ #' @export |
||
20 |
- #' ),+ ui_teal_with_splash <- function(id, |
||
21 |
- #' plotOutput("plot")+ data, |
||
22 |
- #' )+ title = build_app_title(), |
||
23 |
- #'+ header = tags$p(), |
||
24 |
- #' server <- function(input, output) {+ footer = tags$p()) { |
||
25 | -+ | 12x |
- #' output$plot <- renderPlot({+ checkmate::assert_character(id, max.len = 1, any.missing = FALSE) |
26 | -+ | 12x |
- #' df <- iris[iris$Sepal.Length <= input$len, ]+ checkmate::assert_multi_class(data, c("teal_data", "teal_data_module")) |
27 | -+ | 12x |
- #' validate_has_data(+ checkmate::assert( |
28 | -+ | 12x |
- #' iris_f,+ .var.name = "title", |
29 | -+ | 12x |
- #' min_nrow = 10,+ checkmate::check_string(title), |
30 | -+ | 12x |
- #' complete = FALSE,+ checkmate::check_multi_class(title, c("shiny.tag", "shiny.tag.list", "html")) |
31 |
- #' msg = "Please adjust Max Length of Sepal"+ ) |
||
32 | -+ | 12x |
- #' )+ checkmate::assert( |
33 | -+ | 12x |
- #'+ .var.name = "header", |
34 | -+ | 12x |
- #' hist(iris_f$Sepal.Length, breaks = 5)+ checkmate::check_string(header), |
35 | -+ | 12x |
- #' })+ checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html")) |
36 |
- #' }+ ) |
||
37 | -+ | 12x |
- #' if (interactive()) {+ checkmate::assert( |
38 | -+ | 12x |
- #' shinyApp(ui, server)+ .var.name = "footer", |
39 | -+ | 12x |
- #' }+ checkmate::check_string(footer), |
40 | -+ | 12x |
- #'+ checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html")) |
41 |
- validate_has_data <- function(x,+ ) |
||
42 |
- min_nrow = NULL,+ |
||
43 | -+ | 12x |
- complete = FALSE,+ ns <- NS(id) |
44 |
- allow_inf = TRUE,+ |
||
45 |
- msg = NULL) {+ # Startup splash screen for delayed loading |
||
46 | -17x | +
- checkmate::assert_string(msg, null.ok = TRUE)+ # We use delayed loading in all cases, even when the data does not need to be fetched. |
|
47 | -15x | +
- checkmate::assert_data_frame(x)+ # This has the benefit that when filtering the data takes a lot of time initially, the |
|
48 | -15x | +
- if (!is.null(min_nrow)) {+ # Shiny app does not time out. |
|
49 | -15x | +12x |
- if (complete) {+ splash_ui <- if (inherits(data, "teal_data_module")) { |
50 | -5x | +1x |
- complete_index <- stats::complete.cases(x)+ data$ui(ns("teal_data_module")) |
51 | -5x | +12x |
- validate(need(+ } else if (inherits(data, "teal_data")) { |
52 | -5x | +11x |
- sum(complete_index) > 0 && nrow(x[complete_index, , drop = FALSE]) >= min_nrow,+ div() |
53 | -5x | +
- paste(c(paste("Number of complete cases is less than:", min_nrow), msg), collapse = "\n")+ } |
|
54 | -+ | 12x |
- ))+ ui_teal( |
55 | -+ | 12x |
- } else {+ id = ns("teal"), |
56 | -10x | +12x |
- validate(need(+ splash_ui = div(splash_ui, uiOutput(ns("error"))), |
57 | -10x | +12x |
- nrow(x) >= min_nrow,+ title = title, |
58 | -10x | +12x |
- paste(+ header = header, |
59 | -10x | +12x |
- c(paste("Minimum number of records not met: >=", min_nrow, "records required."), msg),+ footer = footer |
60 | -10x | +
- collapse = "\n"+ ) |
|
61 |
- )+ } |
||
62 |
- ))+ |
||
63 |
- }+ #' Server function that loads the data through reactive loading and then delegates |
||
64 |
-
+ #' to [srv_teal()]. |
||
65 | -10x | +
- if (!allow_inf) {+ #' |
|
66 | -6x | +
- validate(need(+ #' @description `r lifecycle::badge("stable")` |
|
67 | -6x | +
- all(vapply(x, function(col) !is.numeric(col) || !any(is.infinite(col)), logical(1))),+ #' Please also refer to the doc of [init()]. |
|
68 | -6x | +
- "Dataframe contains Inf values which is not allowed."+ #' |
|
69 |
- ))+ #' @inheritParams init |
||
70 |
- }+ #' @param modules `teal_modules` object containing the output modules which |
||
71 |
- }+ #' will be displayed in the teal application. See [modules()] and [module()] for |
||
72 |
- }+ #' more details. |
||
73 |
-
+ #' @inheritParams shiny::moduleServer |
||
74 |
- #' Validate that dataset has unique rows for key variables+ #' @return `reactive` containing `teal_data` object when data is loaded. |
||
75 |
- #'+ #' If data is not loaded yet, `reactive` returns `NULL`. |
||
76 |
- #' @description `r lifecycle::badge("stable")`+ #' @export |
||
77 |
- #' @param x a data.frame+ srv_teal_with_splash <- function(id, data, modules, filter = teal_slices()) { |
||
78 | -+ | 15x |
- #' @param key a vector of ID variables from \code{x} that identify unique records+ checkmate::assert_character(id, max.len = 1, any.missing = FALSE) |
79 | -+ | 15x |
- #'+ checkmate::check_multi_class(data, c("teal_data", "teal_data_module")) |
80 | -+ | 15x |
- #' @details This function is a wrapper for `shiny::validate`.+ checkmate::assert_class(modules, "teal_modules") |
81 | -+ | 15x |
- #'+ checkmate::assert_class(filter, "teal_slices") |
82 |
- #' @export+ |
||
83 | -+ | 15x |
- #'+ moduleServer(id, function(input, output, session) { |
84 | -+ | 15x |
- #' @examples+ logger::log_trace("srv_teal_with_splash initializing module with data.") |
85 |
- #' iris$id <- rep(1:50, times = 3)+ |
||
86 | -+ | 15x |
- #' ui <- fluidPage(+ if (getOption("teal.show_js_log", default = FALSE)) { |
87 | -+ | ! |
- #' selectInput(+ shinyjs::showLog() |
88 |
- #' inputId = "species",+ } |
||
89 |
- #' label = "Select species",+ |
||
90 |
- #' choices = c("setosa", "versicolor", "virginica"),+ # teal_data_rv contains teal_data object |
||
91 |
- #' selected = "setosa",+ # either passed to teal::init or returned from teal_data_module |
||
92 | -+ | 15x |
- #' multiple = TRUE+ teal_data_rv <- if (inherits(data, "teal_data_module")) { |
93 | -+ | 10x |
- #' ),+ data <- data$server(id = "teal_data_module") |
94 | -+ | 10x |
- #' plotOutput("plot")+ if (!is.reactive(data)) { |
95 | -+ | 1x |
- #' )+ stop("The `teal_data_module` passed to `data` must return a reactive expression.", call. = FALSE) |
96 |
- #' server <- function(input, output) {+ } |
||
97 | -+ | 9x |
- #' output$plot <- renderPlot({+ data |
98 | -+ | 15x |
- #' iris_f <- iris[iris$Species %in% input$species, ]+ } else if (inherits(data, "teal_data")) { |
99 | -+ | 5x |
- #' validate_one_row_per_id(iris_f, key = c("id"))+ reactiveVal(data) |
100 |
- #'+ } |
||
101 |
- #' hist(iris_f$Sepal.Length, breaks = 5)+ |
||
102 | -+ | 14x |
- #' })+ teal_data_rv_validate <- reactive({ |
103 |
- #' }+ # custom module can return error |
||
104 | -+ | 11x |
- #' if (interactive()) {+ data <- tryCatch(teal_data_rv(), error = function(e) e) |
105 |
- #' shinyApp(ui, server)+ |
||
106 |
- #' }+ # there is an empty reactive cycle on init! |
||
107 | -+ | 11x |
- #'+ if (inherits(data, "shiny.silent.error") && identical(data$message, "")) { |
108 | -+ | ! |
- validate_one_row_per_id <- function(x, key = c("USUBJID", "STUDYID")) {+ return(NULL) |
109 | -! | +
- validate(need(!any(duplicated(x[key])), paste("Found more than one row per id.")))+ } |
|
110 |
- }+ |
||
111 |
-
+ # to handle qenv.error |
||
112 | -+ | 11x |
- #' Validates that vector includes all expected values+ if (inherits(data, "qenv.error")) { |
113 | -+ | 2x |
- #'+ validate( |
114 | -+ | 2x |
- #' @description `r lifecycle::badge("stable")`+ need( |
115 | -+ | 2x |
- #' @param x values to test. All must be in \code{choices}+ FALSE, |
116 | -+ | 2x |
- #' @param choices a vector to test for values of \code{x}+ paste( |
117 | -+ | 2x |
- #' @param msg warning message to display+ "Error when executing `teal_data_module` passed to `data`:\n ", |
118 | -+ | 2x |
- #'+ paste(data$message, collapse = "\n"), |
119 | -+ | 2x |
- #' @details This function is a wrapper for `shiny::validate`.+ "\n Check your inputs or contact app developer if error persists." |
120 |
- #'+ ) |
||
121 |
- #' @export+ ) |
||
122 |
- #'+ ) |
||
123 |
- #' @examples+ } |
||
124 |
- #' ui <- fluidPage(+ |
||
125 |
- #' selectInput(+ # to handle module non-qenv errors |
||
126 | -+ | 9x |
- #' "species",+ if (inherits(data, "error")) { |
127 | -+ | 1x |
- #' "Select species",+ validate( |
128 | -+ | 1x |
- #' choices = c("setosa", "versicolor", "virginica", "unknown species"),+ need( |
129 | -+ | 1x |
- #' selected = "setosa",+ FALSE, |
130 | -+ | 1x |
- #' multiple = FALSE+ paste( |
131 | -+ | 1x |
- #' ),+ "Error when executing `teal_data_module` passed to `data`:\n ", |
132 | -+ | 1x |
- #' verbatimTextOutput("summary")+ paste(data$message, collpase = "\n"), |
133 | -+ | 1x |
- #' )+ "\n Check your inputs or contact app developer if error persists." |
134 |
- #'+ ) |
||
135 |
- #' server <- function(input, output) {+ ) |
||
136 |
- #' output$summary <- renderPrint({+ ) |
||
137 |
- #' validate_in(input$species, iris$Species, "Species does not exist.")+ } |
||
138 |
- #' nrow(iris[iris$Species == input$species, ])+ |
||
139 | -+ | 8x |
- #' })+ validate( |
140 | -+ | 8x |
- #' }+ need( |
141 | -+ | 8x |
- #' if (interactive()) {+ inherits(data, "teal_data"), |
142 | -+ | 8x |
- #' shinyApp(ui, server)+ paste( |
143 | -+ | 8x |
- #' }+ "Error: `teal_data_module` passed to `data` failed to return `teal_data` object, returned", |
144 | -+ | 8x |
- #'+ toString(sQuote(class(data))), |
145 | -+ | 8x |
- validate_in <- function(x, choices, msg) {+ "instead.", |
146 | -! | +8x |
- validate(need(length(x) > 0 && length(choices) > 0 && all(x %in% choices), msg))+ "\n Check your inputs or contact app developer if error persists." |
147 |
- }+ ) |
||
148 |
-
+ ) |
||
149 |
- #' Validates that vector has length greater than 0+ ) |
||
150 |
- #'+ |
||
151 | -+ | 5x |
- #' @description `r lifecycle::badge("stable")`+ if (!length(teal.data::datanames(data))) { |
152 | -+ | 1x |
- #' @param x vector+ warning("`data` object has no datanames. Default datanames are set using `teal_data`'s environment.") |
153 |
- #' @param msg message to display+ } |
||
154 |
- #'+ |
||
155 | -+ | 5x |
- #' @details This function is a wrapper for `shiny::validate`.+ is_modules_ok <- check_modules_datanames(modules, teal_data_datanames(data)) |
156 | -+ | 5x |
- #'+ if (!isTRUE(is_modules_ok)) { |
157 | -+ | 1x |
- #' @export+ validate(need(isTRUE(is_modules_ok), sprintf("%s. Contact app developer.", is_modules_ok))) |
158 |
- #'+ } |
||
159 |
- #' @examples+ |
||
160 | -+ | 4x |
- #' data <- data.frame(+ is_filter_ok <- check_filter_datanames(filter, teal_data_datanames(data)) |
161 | -+ | 4x |
- #' id = c(1:10, 11:20, 1:10),+ if (!isTRUE(is_filter_ok)) { |
162 | -+ | 1x |
- #' strata = rep(c("A", "B"), each = 15)+ showNotification( |
163 | -+ | 1x |
- #' )+ "Some filters were not applied because of incompatibility with data. Contact app developer.", |
164 | -+ | 1x |
- #' ui <- fluidPage(+ type = "warning", |
165 | -+ | 1x |
- #' selectInput("ref1", "Select strata1 to compare",+ duration = 10 |
166 |
- #' choices = c("A", "B", "C"), selected = "A"+ ) |
||
167 | -+ | 1x |
- #' ),+ warning(is_filter_ok) |
168 |
- #' selectInput("ref2", "Select strata2 to compare",+ } |
||
169 |
- #' choices = c("A", "B", "C"), selected = "B"+ |
||
170 | -+ | 4x |
- #' ),+ teal_data_rv() |
171 |
- #' verbatimTextOutput("arm_summary")+ }) |
||
172 |
- #' )+ |
||
173 | -+ | 14x |
- #'+ output$error <- renderUI({ |
174 | -+ | ! |
- #' server <- function(input, output) {+ teal_data_rv_validate() |
175 | -+ | ! |
- #' output$arm_summary <- renderText({+ NULL |
176 |
- #' sample_1 <- data$id[data$strata == input$ref1]+ }) |
||
177 |
- #' sample_2 <- data$id[data$strata == input$ref2]+ |
||
178 |
- #'+ |
||
179 | -+ | 14x |
- #' validate_has_elements(sample_1, "No subjects in strata1.")+ res <- srv_teal(id = "teal", modules = modules, teal_data_rv = teal_data_rv_validate, filter = filter) |
180 | -+ | 14x |
- #' validate_has_elements(sample_2, "No subjects in strata2.")+ logger::log_trace("srv_teal_with_splash initialized module with data.") |
181 | -+ | 14x |
- #'+ return(res) |
182 |
- #' paste0(+ }) |
||
183 |
- #' "Number of samples in: strata1=", length(sample_1),+ } |
184 | +1 |
- #' " comparions strata2=", length(sample_2)+ .onLoad <- function(libname, pkgname) { # nolint |
|
185 | +2 |
- #' )+ # adapted from https://github.com/r-lib/devtools/blob/master/R/zzz.R |
|
186 | -+ | ||
3 | +! |
- #' })+ teal_default_options <- list(teal.show_js_log = FALSE) |
|
187 | +4 |
- #' }+ |
|
188 | -+ | ||
5 | +! |
- #' if (interactive()) {+ op <- options() |
|
189 | -+ | ||
6 | +! |
- #' shinyApp(ui, server)+ toset <- !(names(teal_default_options) %in% names(op)) |
|
190 | -+ | ||
7 | +! |
- #' }+ if (any(toset)) options(teal_default_options[toset]) |
|
191 | +8 |
- validate_has_elements <- function(x, msg) {+ |
|
192 | +9 | ! |
- validate(need(length(x) > 0, msg))+ options("shiny.sanitize.errors" = FALSE) |
193 | +10 |
- }+ |
|
194 | +11 |
-
+ # Set up the teal logger instance |
|
195 | -+ | ||
12 | +! |
- #' Validates no intersection between two vectors+ teal.logger::register_logger("teal") |
|
196 | +13 |
- #'+ |
|
197 | -+ | ||
14 | +! |
- #' @description `r lifecycle::badge("stable")`+ invisible() |
|
198 | +15 |
- #' @param x vector+ } |
|
199 | +16 |
- #' @param y vector+ |
|
200 | +17 |
- #' @param msg message to display if \code{x} and \code{y} intersect+ .onAttach <- function(libname, pkgname) { # nolint |
|
201 | -+ | ||
18 | +2x |
- #'+ packageStartupMessage( |
|
202 | -+ | ||
19 | +2x |
- #' @details This function is a wrapper for `shiny::validate`.+ "\nYou are using teal version ", |
|
203 | +20 |
- #'+ # `system.file` uses the `shim` of `system.file` by `teal` |
|
204 | +21 |
- #' @export+ # we avoid `desc` dependency here to get the version |
|
205 | -+ | ||
22 | +2x |
- #'+ read.dcf(system.file("DESCRIPTION", package = "teal"))[, "Version"] |
|
206 | +23 |
- #' @examples+ ) |
|
207 | +24 |
- #' data <- data.frame(+ } |
|
208 | +25 |
- #' id = c(1:10, 11:20, 1:10),+ |
|
209 | +26 |
- #' strata = rep(c("A", "B", "C"), each = 10)+ # This one is here because setdiff_teal_slice should not be exported from teal.slice. |
|
210 | +27 |
- #' )+ setdiff_teal_slices <- getFromNamespace("setdiff_teal_slices", "teal.slice") |
|
211 | +28 |
- #'+ # This one is here because it is needed by c.teal_slices but we don't want it exported from teal.slice. |
|
212 | +29 |
- #' ui <- fluidPage(- |
- |
213 | -- |
- #' selectInput("ref1", "Select strata1 to compare",+ coalesce_r <- getFromNamespace("coalesce_r", "teal.slice") |
|
214 | +30 |
- #' choices = c("A", "B", "C"),+ # all *Block objects are private in teal.reporter |
|
215 | +31 |
- #' selected = "A"+ RcodeBlock <- getFromNamespace("RcodeBlock", "teal.reporter") # nolint |
|
216 | +32 |
- #' ),+ |
|
217 | +33 |
- #' selectInput("ref2", "Select strata2 to compare",+ # Use non-exported function(s) from teal.code |
|
218 | +34 |
- #' choices = c("A", "B", "C"),+ # This one is here because lang2calls should not be exported from teal.code |
|
219 | +35 |
- #' selected = "B"+ lang2calls <- getFromNamespace("lang2calls", "teal.code") |
220 | +1 |
- #' ),+ #' Filter settings for teal applications |
|
221 | +2 |
- #' verbatimTextOutput("summary")+ #' |
|
222 | +3 |
- #' )+ #' Specify initial filter states and filtering settings for a `teal` app. |
|
223 | +4 |
#' |
|
224 | +5 |
- #' server <- function(input, output) {+ #' Produces a `teal_slices` object. |
|
225 | +6 |
- #' output$summary <- renderText({+ #' The `teal_slice` components will specify filter states that will be active when the app starts. |
|
226 | +7 |
- #' sample_1 <- data$id[data$strata == input$ref1]+ #' Attributes (created with the named arguments) will configure the way the app applies filters. |
|
227 | +8 |
- #' sample_2 <- data$id[data$strata == input$ref2]+ #' See argument descriptions for details. |
|
228 | +9 |
#' |
|
229 | +10 |
- #' validate_no_intersection(+ #' @inheritParams teal.slice::teal_slices |
|
230 | +11 |
- #' sample_1, sample_2,+ #' |
|
231 | +12 |
- #' "subjects within strata1 and strata2 cannot overlap"+ #' @param module_specific optional (`logical(1)`)\cr |
|
232 | +13 |
- #' )+ #' - `FALSE` (default) when one filter panel applied to all modules. |
|
233 | +14 |
- #' paste0(+ #' All filters will be shared by all modules. |
|
234 | +15 |
- #' "Number of subject in: reference treatment=", length(sample_1),+ #' - `TRUE` when filter panel module-specific. |
|
235 | +16 |
- #' " comparions treatment=", length(sample_2)+ #' Modules can have different set of filters specified - see `mapping` argument. |
|
236 | +17 |
- #' )+ #' @param mapping `r lifecycle::badge("experimental")` _This is a new feature. Do kindly share your opinions.\cr_ |
|
237 | +18 |
- #' })+ #' (`named list`)\cr |
|
238 | +19 |
- #' }+ #' Specifies which filters will be active in which modules on app start. |
|
239 | +20 |
- #' if (interactive()) {+ #' Elements should contain character vector of `teal_slice` `id`s (see [teal.slice::teal_slice()]). |
|
240 | +21 |
- #' shinyApp(ui, server)+ #' Names of the list should correspond to `teal_module` `label` set in [module()] function. |
|
241 | +22 |
- #' }+ #' `id`s listed under `"global_filters` will be active in all modules. |
|
242 | +23 |
- #'+ #' If missing, all filters will be applied to all modules. |
|
243 | +24 |
- validate_no_intersection <- function(x, y, msg) {+ #' If empty list, all filters will be available to all modules but will start inactive. |
|
244 | -! | +||
25 | +
- validate(need(length(intersect(x, y)) == 0, msg))+ #' If `module_specific` is `FALSE`, only `global_filters` will be active on start. |
||
245 | +26 |
- }+ #' @param app_id (`character(1)`)\cr |
|
246 | +27 |
-
+ #' For internal use only, do not set manually. |
|
247 | +28 |
-
+ #' Added by `init` so that a `teal_slices` can be matched to the app in which it was used. |
|
248 | +29 |
- #' Validates that dataset contains specific variable+ #' Used for verifying snapshots uploaded from file. See `snapshot`. |
|
249 | +30 |
#' |
|
250 | +31 |
- #' @description `r lifecycle::badge("stable")`+ #' @param x (`list`) of lists to convert to `teal_slices` |
|
251 | +32 |
- #' @param data a data.frame+ #' |
|
252 | +33 |
- #' @param varname name of variable in \code{data}+ #' @return |
|
253 | +34 |
- #' @param msg message to display if \code{data} does not include \code{varname}+ #' A `teal_slices` object. |
|
254 | +35 |
#' |
|
255 | +36 |
- #' @details This function is a wrapper for `shiny::validate`.+ #' @seealso [`teal.slice::teal_slices`], [`teal.slice::teal_slice`], [`slices_store`] |
|
256 | +37 |
#' |
|
257 | +38 |
- #' @export+ #' @examples |
|
258 | +39 |
- #'+ #' filter <- teal_slices( |
|
259 | +40 |
- #' @examples+ #' teal.slice::teal_slice(dataname = "iris", varname = "Species", id = "species"), |
|
260 | +41 |
- #' data <- data.frame(+ #' teal.slice::teal_slice(dataname = "iris", varname = "Sepal.Length", id = "sepal_length"), |
|
261 | +42 |
- #' one = rep("a", length.out = 20),+ #' teal.slice::teal_slice( |
|
262 | +43 |
- #' two = rep(c("a", "b"), length.out = 20)+ #' dataname = "iris", id = "long_petals", title = "Long petals", expr = "Petal.Length > 5" |
|
263 | +44 |
- #' )+ #' ), |
|
264 | +45 |
- #' ui <- fluidPage(+ #' teal.slice::teal_slice(dataname = "mtcars", varname = "mpg", id = "mtcars_mpg"), |
|
265 | +46 |
- #' selectInput(+ #' mapping = list( |
|
266 | +47 |
- #' "var",+ #' module1 = c("species", "sepal_length"), |
|
267 | +48 |
- #' "Select variable",+ #' module2 = c("mtcars_mpg"), |
|
268 | +49 |
- #' choices = c("one", "two", "three", "four"),+ #' global_filters = "long_petals" |
|
269 | +50 |
- #' selected = "one"+ #' ) |
|
270 | +51 |
- #' ),+ #' ) |
|
271 | +52 |
- #' verbatimTextOutput("summary")+ #' |
|
272 | +53 |
- #' )+ #' app <- teal::init( |
|
273 | +54 |
- #'+ #' data = list(iris = iris, mtcars = mtcars), |
|
274 | +55 |
- #' server <- function(input, output) {+ #' modules = list( |
|
275 | +56 |
- #' output$summary <- renderText({+ #' module("module1"), |
|
276 | +57 |
- #' validate_has_variable(data, input$var)+ #' module("module2") |
|
277 | +58 |
- #' paste0("Selected treatment variables: ", paste(input$var, collapse = ", "))+ #' ), |
|
278 | +59 |
- #' })+ #' filter = filter |
|
279 | +60 |
- #' }+ #' ) |
|
280 | +61 |
- #' if (interactive()) {+ #' |
|
281 | +62 |
- #' shinyApp(ui, server)+ #' if (interactive()) { |
|
282 | +63 |
- #' }+ #' shinyApp(app$ui, app$server) |
|
283 | +64 |
- validate_has_variable <- function(data, varname, msg) {+ #' } |
|
284 | -! | +||
65 | +
- if (length(varname) != 0) {+ #' |
||
285 | -! | +||
66 | +
- has_vars <- varname %in% names(data)+ #' @export |
||
286 | +67 |
-
+ teal_slices <- function(..., |
|
287 | -! | +||
68 | +
- if (!all(has_vars)) {+ exclude_varnames = NULL, |
||
288 | -! | +||
69 | +
- if (missing(msg)) {+ include_varnames = NULL, |
||
289 | -! | +||
70 | +
- msg <- sprintf(+ count_type = NULL, |
||
290 | -! | +||
71 | +
- "%s does not have the required variables: %s.",+ allow_add = TRUE, |
||
291 | -! | +||
72 | +
- deparse(substitute(data)),+ module_specific = FALSE, |
||
292 | -! | +||
73 | +
- toString(varname[!has_vars])+ mapping, |
||
293 | +74 |
- )+ app_id = NULL) { |
|
294 | -+ | ||
75 | +88x |
- }+ shiny::isolate({ |
|
295 | -! | +||
76 | +88x |
- validate(need(FALSE, msg))+ checkmate::assert_flag(allow_add) |
|
296 | -+ | ||
77 | +88x |
- }+ checkmate::assert_flag(module_specific) |
|
297 | -+ | ||
78 | +37x |
- }+ if (!missing(mapping)) checkmate::assert_list(mapping, types = c("character", "NULL"), names = "named") |
|
298 | -+ | ||
79 | +85x |
- }+ checkmate::assert_string(app_id, null.ok = TRUE) |
|
299 | +80 | ||
300 | -+ | ||
81 | +85x |
- #' Validate that variables has expected number of levels+ slices <- list(...) |
|
301 | -+ | ||
82 | +85x |
- #'+ all_slice_id <- vapply(slices, `[[`, character(1L), "id") |
|
302 | +83 |
- #' @description `r lifecycle::badge("stable")`+ |
|
303 | -+ | ||
84 | +85x |
- #' @param x variable name. If \code{x} is not a factor, the unique values+ if (missing(mapping)) { |
|
304 | -+ | ||
85 | +51x |
- #' are treated as levels.+ mapping <- list(global_filters = all_slice_id) |
|
305 | +86 |
- #' @param min_levels cutoff for minimum number of levels of \code{x}+ } |
|
306 | -+ | ||
87 | +85x |
- #' @param max_levels cutoff for maximum number of levels of \code{x}+ if (!module_specific) { |
|
307 | -+ | ||
88 | +81x |
- #' @param var_name name of variable being validated for use in+ mapping[setdiff(names(mapping), "global_filters")] <- NULL |
|
308 | +89 |
- #' validation message+ } |
|
309 | +90 |
- #'+ |
|
310 | -+ | ||
91 | +85x |
- #' @details If the number of levels of \code{x} is less than \code{min_levels}+ failed_slice_id <- setdiff(unlist(mapping), all_slice_id) |
|
311 | -+ | ||
92 | +85x |
- #' or greater than \code{max_levels} the validation will fail.+ if (length(failed_slice_id)) { |
|
312 | -+ | ||
93 | +1x |
- #' This function is a wrapper for `shiny::validate`.+ stop(sprintf( |
|
313 | -+ | ||
94 | +1x |
- #'+ "Filters in mapping don't match any available filter.\n %s not in %s", |
|
314 | -+ | ||
95 | +1x |
- #' @export+ toString(failed_slice_id), |
|
315 | -+ | ||
96 | +1x |
- #' @examples+ toString(all_slice_id) |
|
316 | +97 |
- #' data <- data.frame(+ )) |
|
317 | +98 |
- #' one = rep("a", length.out = 20),+ } |
|
318 | +99 |
- #' two = rep(c("a", "b"), length.out = 20),+ |
|
319 | -+ | ||
100 | +84x |
- #' three = rep(c("a", "b", "c"), length.out = 20),+ tss <- teal.slice::teal_slices( |
|
320 | +101 |
- #' four = rep(c("a", "b", "c", "d"), length.out = 20),+ ..., |
|
321 | -+ | ||
102 | +84x |
- #' stringsAsFactors = TRUE+ exclude_varnames = exclude_varnames, |
|
322 | -+ | ||
103 | +84x |
- #' )+ include_varnames = include_varnames, |
|
323 | -+ | ||
104 | +84x |
- #' ui <- fluidPage(+ count_type = count_type,+ |
+ |
105 | +84x | +
+ allow_add = allow_add |
|
324 | +106 |
- #' selectInput(+ )+ |
+ |
107 | +84x | +
+ attr(tss, "mapping") <- mapping+ |
+ |
108 | +84x | +
+ attr(tss, "module_specific") <- module_specific+ |
+ |
109 | +84x | +
+ attr(tss, "app_id") <- app_id+ |
+ |
110 | +84x | +
+ class(tss) <- c("modules_teal_slices", class(tss))+ |
+ |
111 | +84x | +
+ tss |
|
325 | +112 |
- #' "var",+ }) |
|
326 | +113 |
- #' "Select variable",+ } |
|
327 | +114 |
- #' choices = c("one", "two", "three", "four"),+ |
|
328 | +115 |
- #' selected = "one"+ |
|
329 | +116 |
- #' ),+ #' @rdname teal_slices |
|
330 | +117 |
- #' verbatimTextOutput("summary")+ #' @export |
|
331 | +118 |
- #' )+ #' @keywords internal |
|
332 | +119 |
#' |
|
333 | +120 |
- #' server <- function(input, output) {+ as.teal_slices <- function(x) { # nolint |
|
334 | -+ | ||
121 | +15x |
- #' output$summary <- renderText({+ checkmate::assert_list(x) |
|
335 | -+ | ||
122 | +15x |
- #' validate_n_levels(data[[input$var]], min_levels = 2, max_levels = 15, var_name = input$var)+ lapply(x, checkmate::assert_list, names = "named", .var.name = "list element") |
|
336 | +123 |
- #' paste0(+ |
|
337 | -+ | ||
124 | +15x |
- #' "Levels of selected treatment variable: ",+ attrs <- attributes(unclass(x)) |
|
338 | -+ | ||
125 | +15x |
- #' paste(levels(data[[input$var]]),+ ans <- lapply(x, function(x) if (is.teal_slice(x)) x else as.teal_slice(x)) |
|
339 | -+ | ||
126 | +15x |
- #' collapse = ", "+ do.call(teal_slices, c(ans, attrs)) |
|
340 | +127 |
- #' )+ } |
|
341 | +128 |
- #' )+ |
|
342 | +129 |
- #' })+ |
|
343 | +130 |
- #' }+ #' @rdname teal_slices |
|
344 | +131 |
- #' if (interactive()) {+ #' @export |
|
345 | +132 |
- #' shinyApp(ui, server)+ #' @keywords internal |
|
346 | +133 |
- #' }+ #' |
|
347 | +134 |
- validate_n_levels <- function(x, min_levels = 1, max_levels = 12, var_name) {+ c.teal_slices <- function(...) { |
|
348 | +135 | ! |
- x_levels <- if (is.factor(x)) {+ x <- list(...) |
349 | +136 | ! |
- levels(x)+ checkmate::assert_true(all(vapply(x, is.teal_slices, logical(1L))), .var.name = "all arguments are teal_slices") |
350 | +137 |
- } else {+ |
|
351 | +138 | ! |
- unique(x)+ all_attributes <- lapply(x, attributes) |
352 | -+ | ||
139 | +! |
- }+ all_attributes <- coalesce_r(all_attributes)+ |
+ |
140 | +! | +
+ all_attributes <- all_attributes[names(all_attributes) != "class"] |
|
353 | +141 | ||
354 | +142 | ! |
- if (!is.null(min_levels) && !(is.null(max_levels))) {+ do.call( |
355 | +143 | ! |
- validate(need(+ teal_slices, |
356 | +144 | ! |
- length(x_levels) >= min_levels && length(x_levels) <= max_levels,+ c( |
357 | +145 | ! |
- sprintf(+ unique(unlist(x, recursive = FALSE)), |
358 | +146 | ! |
- "%s variable needs minimum %s level(s) and maximum %s level(s).",+ all_attributes |
359 | -! | +||
147 | +
- var_name, min_levels, max_levels+ ) |
||
360 | +148 |
- )+ ) |
|
361 | +149 |
- ))+ } |
|
362 | -! | +||
150 | +
- } else if (!is.null(min_levels)) {+ |
||
363 | -! | +||
151 | +
- validate(need(+ |
||
364 | -! | +||
152 | +
- length(x_levels) >= min_levels,+ #' Deep copy `teal_slices` |
||
365 | -! | +||
153 | +
- sprintf("%s variable needs minimum %s levels(s)", var_name, min_levels)+ #' |
||
366 | +154 |
- ))+ #' it's important to create a new copy of `teal_slices` when |
|
367 | -! | +||
155 | +
- } else if (!is.null(max_levels)) {+ #' starting a new `shiny` session. Otherwise, object will be shared |
||
368 | -! | +||
156 | +
- validate(need(+ #' by multiple users as it is created in global environment before |
||
369 | -! | +||
157 | +
- length(x_levels) <= max_levels,+ #' `shiny` session starts. |
||
370 | -! | +||
158 | +
- sprintf("%s variable needs maximum %s level(s)", var_name, max_levels)+ #' @param filter (`teal_slices`) |
||
371 | +159 |
- ))+ #' @return `teal_slices` |
|
372 | +160 |
- }+ #' @keywords internal |
|
373 | +161 | ++ |
+ deep_copy_filter <- function(filter) {+ |
+
162 | +1x | +
+ checkmate::assert_class(filter, "teal_slices")+ |
+ |
163 | +1x | +
+ shiny::isolate({+ |
+ |
164 | +1x | +
+ filter_copy <- lapply(filter, function(slice) {+ |
+ |
165 | +2x | +
+ teal.slice::as.teal_slice(as.list(slice))+ |
+ |
166 | ++ |
+ })+ |
+ |
167 | +1x | +
+ attributes(filter_copy) <- attributes(filter)+ |
+ |
168 | +1x | +
+ filter_copy+ |
+ |
169 | ++ |
+ })+ |
+ |
170 |
}@@ -24098,6185 +23838,7004 @@ teal coverage - 63.82% |
1 |
- # This is the main function from teal to be used by the end-users. Although it delegates+ #' Get dummy `CDISC` data |
||
2 |
- # directly to `module_teal_with_splash.R`, we keep it in a separate file because its doc is quite large+ #' |
||
3 |
- # and it is very end-user oriented. It may also perform more argument checking with more informative+ #' Get dummy `CDISC` data including `ADSL`, `ADAE` and `ADLB`. |
||
4 |
- # error messages.+ #' Some NAs are also introduced to stress test. |
||
5 |
-
+ #' |
||
6 |
-
+ #' @return `cdisc_data` |
||
7 |
- #' Create the Server and UI Function For the Shiny App+ #' @keywords internal |
||
8 |
- #'+ example_cdisc_data <- function() { # nolint |
||
9 | -+ | ! |
- #' @description `r lifecycle::badge("stable")`+ ADSL <- data.frame( # nolint |
10 | -+ | ! |
- #' End-users: This is the most important function for you to start a+ STUDYID = "study", |
11 | -+ | ! |
- #' teal app that is composed out of teal modules.+ USUBJID = 1:10, |
12 | -+ | ! |
- #'+ SEX = sample(c("F", "M"), 10, replace = TRUE), |
13 | -+ | ! |
- #' @details+ AGE = stats::rpois(10, 40) |
14 |
- #' When initializing the `teal` app, if `datanames` are not set for the `teal_data` object,+ ) |
||
15 | -+ | ! |
- #' defaults from the `teal_data` environment will be used.+ ADTTE <- rbind(ADSL, ADSL, ADSL) # nolint |
16 | -+ | ! |
- #'+ ADTTE$PARAMCD <- rep(c("OS", "EFS", "PFS"), each = 10) # nolint |
17 | -+ | ! |
- #' @param data (`teal_data`, `teal_data_module`, `named list`)\cr+ ADTTE$AVAL <- c( # nolint |
18 | -+ | ! |
- #' `teal_data` object as returned by [teal.data::teal_data()] or+ stats::rnorm(10, mean = 700, sd = 200), # dummy OS level |
19 | -+ | ! |
- #' `teal_data_module` or simply a list of a named list of objects+ stats::rnorm(10, mean = 400, sd = 100), # dummy EFS level |
20 | -+ | ! |
- #' (`data.frame` or `MultiAssayExperiment`).+ stats::rnorm(10, mean = 450, sd = 200) # dummy PFS level |
21 |
- #' @param modules (`list`, `teal_modules` or `teal_module`)\cr+ ) |
||
22 |
- #' nested list of `teal_modules` or `teal_module` objects or a single+ |
||
23 | -+ | ! |
- #' `teal_modules` or `teal_module` object. These are the specific output modules which+ ADSL$logical_test <- sample(c(TRUE, FALSE, NA), size = nrow(ADSL), replace = TRUE) # nolint |
24 | -+ | ! |
- #' will be displayed in the teal application. See [modules()] and [module()] for+ ADSL$SEX[c(2, 5)] <- NA # nolint |
25 |
- #' more details.+ |
||
26 | -+ | ! |
- #' @param title (`shiny.tag` or `character`)\cr+ res <- teal.data::cdisc_data( |
27 | -+ | ! |
- #' The browser window title. Defaults to a title "Teal app" with the icon of NEST.+ ADSL = ADSL, |
28 | -+ | ! |
- #' Can be created using the `build_app_title()` or+ ADTTE = ADTTE, |
29 | -+ | ! |
- #' by passing a valid `shiny.tag` which is a head tag with title and link tag.+ code = ' |
30 | -+ | ! |
- #' @param filter (`teal_slices`)\cr+ ADSL <- data.frame( |
31 | -+ | ! |
- #' Specification of initial filter. Filters can be specified using [teal::teal_slices()].+ STUDYID = "study", |
32 | -+ | ! |
- #' Old way of specifying filters through a list is deprecated and will be removed in the+ USUBJID = 1:10, |
33 | -+ | ! |
- #' next release. Please fix your applications to use [teal::teal_slices()].+ SEX = sample(c("F", "M"), 10, replace = TRUE), |
34 | -+ | ! |
- #' @param header (`shiny.tag` or `character`) \cr+ AGE = rpois(10, 40) |
35 |
- #' the header of the app. Note shiny code placed here (and in the footer+ ) |
||
36 | -+ | ! |
- #' argument) will be placed in the app's `ui` function so code which needs to be placed in the `ui` function+ ADTTE <- rbind(ADSL, ADSL, ADSL) |
37 | -+ | ! |
- #' (such as loading `CSS` via [htmltools::htmlDependency()]) should be included here.+ ADTTE$PARAMCD <- rep(c("OS", "EFS", "PFS"), each = 10) |
38 | -+ | ! |
- #' @param footer (`shiny.tag` or `character`)\cr+ ADTTE$AVAL <- c( |
39 | -+ | ! |
- #' the footer of the app+ rnorm(10, mean = 700, sd = 200), |
40 | -+ | ! |
- #' @param id (`character`)\cr+ rnorm(10, mean = 400, sd = 100), |
41 | -+ | ! |
- #' module id to embed it, if provided,+ rnorm(10, mean = 450, sd = 200) |
42 |
- #' the server function must be called with [shiny::moduleServer()];+ ) |
||
43 |
- #' See the vignette for an example. However, [ui_teal_with_splash()]+ |
||
44 | -+ | ! |
- #' is then preferred to this function.+ ADSL$logical_test <- sample(c(TRUE, FALSE, NA), size = nrow(ADSL), replace = TRUE) |
45 | -+ | ! |
- #'+ ADSL$SEX[c(2, 5)] <- NA |
46 |
- #' @return named list with `server` and `ui` function+ ' |
||
47 |
- #'+ ) |
||
48 | -+ | ! |
- #' @export+ return(res) |
49 |
- #'+ } |
||
50 |
- #' @include modules.R+ |
||
51 |
- #'+ #' Get datasets to go with example modules. |
||
52 |
- #' @examples+ #' |
||
53 |
- #' app <- init(+ #' Creates a nested list, the structure of which matches the module hierarchy created by `example_modules`. |
||
54 |
- #' data = teal_data(+ #' Each list leaf is the same `FilteredData` object. |
||
55 |
- #' new_iris = transform(iris, id = seq_len(nrow(iris))),+ #' |
||
56 |
- #' new_mtcars = transform(mtcars, id = seq_len(nrow(mtcars))),+ #' @return named list of `FilteredData` objects, each with `ADSL` set. |
||
57 |
- #' code = "+ #' @keywords internal |
||
58 |
- #' new_iris <- transform(iris, id = seq_len(nrow(iris)))+ example_datasets <- function() { # nolint |
||
59 | -+ | ! |
- #' new_mtcars <- transform(mtcars, id = seq_len(nrow(mtcars)))+ dummy_cdisc_data <- example_cdisc_data() |
60 | -+ | ! |
- #' "+ datasets <- teal_data_to_filtered_data(dummy_cdisc_data) |
61 | -+ | ! |
- #' ),+ list( |
62 | -+ | ! |
- #' modules = modules(+ "d2" = list( |
63 | -+ | ! |
- #' module(+ "d3" = list( |
64 | -+ | ! |
- #' label = "data source",+ "aaa1" = datasets, |
65 | -+ | ! |
- #' server = function(input, output, session, data) {},+ "aaa2" = datasets, |
66 | -+ | ! |
- #' ui = function(id, ...) div(p("information about data source")),+ "aaa3" = datasets |
67 |
- #' datanames = "all"+ ), |
||
68 | -+ | ! |
- #' ),+ "bbb" = datasets |
69 |
- #' example_module(label = "example teal module"),+ ), |
||
70 | -+ | ! |
- #' module(+ "ccc" = datasets |
71 |
- #' "Iris Sepal.Length histogram",+ ) |
||
72 |
- #' server = function(input, output, session, data) {+ } |
||
73 |
- #' output$hist <- renderPlot(+ |
||
74 |
- #' hist(data()[["new_iris"]]$Sepal.Length)+ #' An example `teal` module |
||
75 |
- #' )+ #' |
||
76 |
- #' },+ #' @description `r lifecycle::badge("experimental")` |
||
77 |
- #' ui = function(id, ...) {+ #' @inheritParams module |
||
78 |
- #' ns <- NS(id)+ #' @return A `teal` module which can be included in the `modules` argument to [teal::init()]. |
||
79 |
- #' plotOutput(ns("hist"))+ #' @examples |
||
80 |
- #' },+ #' app <- init( |
||
81 |
- #' datanames = "new_iris"+ #' data = teal_data(IRIS = iris, MTCARS = mtcars), |
||
82 |
- #' )+ #' modules = example_module() |
||
83 |
- #' ),+ #' ) |
||
84 |
- #' title = "App title",+ #' if (interactive()) { |
||
85 |
- #' filter = teal_slices(+ #' shinyApp(app$ui, app$server) |
||
86 |
- #' teal_slice(dataname = "new_iris", varname = "Species"),+ #' } |
||
87 |
- #' teal_slice(dataname = "new_iris", varname = "Sepal.Length"),+ #' @export |
||
88 |
- #' teal_slice(dataname = "new_mtcars", varname = "cyl"),+ example_module <- function(label = "example teal module", datanames = "all") { |
||
89 | -+ | 44x |
- #' exclude_varnames = list(new_iris = c("Sepal.Width", "Petal.Width")),+ checkmate::assert_string(label) |
90 | -+ | 44x |
- #' mapping = list(+ module( |
91 | -+ | 44x |
- #' `example teal module` = "new_iris Species",+ label, |
92 | -+ | 44x |
- #' `Iris Sepal.Length histogram` = "new_iris Species",+ server = function(id, data) { |
93 | -+ | ! |
- #' global_filters = "new_mtcars cyl"+ checkmate::assert_class(data(), "teal_data") |
94 | -+ | ! |
- #' )+ moduleServer(id, function(input, output, session) { |
95 | -+ | ! |
- #' ),+ ns <- session$ns |
96 | -+ | ! |
- #' header = tags$h1("Sample App"),+ updateSelectInput(session, "dataname", choices = isolate(teal.data::datanames(data()))) |
97 | -+ | ! |
- #' footer = tags$p("Copyright 2017 - 2023")+ output$text <- renderPrint({ |
98 | -+ | ! |
- #' )+ req(input$dataname) |
99 | -+ | ! |
- #' if (interactive()) {+ data()[[input$dataname]] |
100 |
- #' shinyApp(app$ui, app$server)+ }) |
||
101 | -+ | ! |
- #' }+ teal.widgets::verbatim_popup_srv( |
102 | -+ | ! |
- #'+ id = "rcode", |
103 | -+ | ! |
- init <- function(data,+ verbatim_content = reactive(teal.code::get_code(data())), |
104 | -+ | ! |
- modules,+ title = "Association Plot" |
105 |
- title = build_app_title(),+ ) |
||
106 |
- filter = teal_slices(),+ }) |
||
107 |
- header = tags$p(),+ }, |
||
108 | -+ | 44x |
- footer = tags$p(),+ ui = function(id) { |
109 | -+ | ! |
- id = character(0)) {+ ns <- NS(id) |
110 | -15x | +! |
- logger::log_trace("init initializing teal app with: data ({ class(data)[1] }).")+ teal.widgets::standard_layout( |
111 | -15x | +! |
- if (is.list(data) && !inherits(data, "teal_data_module")) {+ output = verbatimTextOutput(ns("text")), |
112 | -10x | +! |
- checkmate::assert_list(data, names = "named")+ encoding = div( |
113 | -10x | +! |
- data <- do.call(teal.data::teal_data, data)+ selectInput(ns("dataname"), "Choose a dataset", choices = NULL), |
114 | -+ | ! |
- }+ teal.widgets::verbatim_popup_ui(ns("rcode"), "Show R code") |
115 | -15x | +
- if (inherits(data, "TealData")) {+ ) |
|
116 | -! | +
- lifecycle::deprecate_stop(+ ) |
|
117 | -! | +
- when = "0.99.0",+ }, |
|
118 | -! | +44x |
- what = "init(data)",+ datanames = datanames |
119 | -! | +
- paste(+ ) |
|
120 | -! | +
- "TealData is no longer supported. Use teal_data() instead.",+ } |
|
121 | -! | +
- "Please follow migration instructions https://github.com/insightsengineering/teal/discussions/988."+ |
|
122 |
- )+ |
||
123 |
- )+ #' Get example modules. |
||
124 |
- }+ #' |
||
125 |
-
+ #' Creates an example hierarchy of `teal_modules` from which a `teal` app can be created. |
||
126 | -15x | +
- checkmate::assert_multi_class(data, c("teal_data", "teal_data_module"))+ #' @param datanames (`character`)\cr |
|
127 | -15x | +
- checkmate::assert_multi_class(modules, c("teal_module", "list", "teal_modules"))+ #' names of the datasets to be used in the example modules. Possible choices are `ADSL`, `ADTTE`. |
|
128 | -15x | +
- checkmate::assert(+ #' @return `teal_modules` |
|
129 | -15x | +
- checkmate::check_class(filter, "teal_slices"),+ #' @keywords internal |
|
130 | -15x | +
- checkmate::check_list(filter, names = "named")+ example_modules <- function(datanames = c("ADSL", "ADTTE")) { |
|
131 | -+ | ! |
- )+ checkmate::assert_subset(datanames, c("ADSL", "ADTTE")) |
132 | -14x | +! |
- checkmate::assert_multi_class(title, c("shiny.tag", "character"))+ mods <- modules( |
133 | -14x | +! |
- checkmate::assert_multi_class(header, c("shiny.tag", "character"))+ label = "d1", |
134 | -14x | +! |
- checkmate::assert_multi_class(footer, c("shiny.tag", "character"))+ modules( |
135 | -14x | +! |
- checkmate::assert_character(id, max.len = 1, any.missing = FALSE)+ label = "d2", |
136 | -+ | ! |
-
+ modules( |
137 | -14x | +! |
- teal.logger::log_system_info()+ label = "d3", |
138 | -+ | ! |
-
+ example_module(label = "aaa1", datanames = datanames), |
139 | -14x | +! |
- if (is.character(title)) {+ example_module(label = "aaa2", datanames = datanames), |
140 | ! |
- title <- build_app_title(title)+ example_module(label = "aaa3", datanames = datanames) |
|
141 |
- } else {+ ), |
||
142 | -14x | +! |
- validate_app_title_tag(title)+ example_module(label = "bbb", datanames = datanames) |
143 |
- }+ ), |
||
144 | -+ | ! |
-
+ example_module(label = "ccc", datanames = datanames) |
145 | -14x | +
- if (inherits(modules, "teal_module")) {+ ) |
|
146 | -1x | +! |
- modules <- list(modules)+ return(mods) |
147 |
- }+ } |
||
148 | -14x | +
1 | +
- if (inherits(modules, "list")) {+ #' Generates library calls from current session info |
|||
149 | -4x | +|||
2 | +
- modules <- do.call(teal::modules, modules)+ #' |
|||
150 | +3 |
- }+ #' Function to create multiple library calls out of current session info to make reproducible code works. |
||
151 | +4 |
-
+ #' |
||
152 | -14x | +|||
5 | +
- landing <- extract_module(modules, "teal_module_landing")+ #' @return Character object contain code |
|||
153 | -! | +|||
6 | +
- if (length(landing) > 1L) stop("Only one `landing_popup_module` can be used.")+ #' @keywords internal+ |
+ |||
7 | ++ |
+ get_rcode_libraries <- function() { |
||
154 | -14x | +8 | +5x |
- modules <- drop_module(modules, "teal_module_landing")+ vapply( |
155 | -+ | |||
9 | +5x |
-
+ utils::sessionInfo()$otherPkgs, |
||
156 | -+ | |||
10 | +5x |
- # Calculate app id that will be used to stamp filter state snapshots.+ function(x) { |
||
157 | -+ | |||
11 | +80x |
- # App id is a hash of the app's data and modules.+ paste0("library(", x$Package, ")") |
||
158 | +12 |
- # See "transferring snapshots" section in ?snapshot.+ }, |
||
159 | -14x | +13 | +5x |
- hashables <- mget(c("data", "modules"))+ character(1) |
160 | -14x | +|||
14 | +
- hashables$data <- if (inherits(hashables$data, "teal_data")) {+ ) %>%+ |
+ |||
15 | ++ |
+ # put it into reverse order to correctly simulate executed code |
||
161 | -13x | +16 | +5x |
- as.list(hashables$data@env)+ rev() %>% |
162 | -14x | +17 | +5x |
- } else if (inherits(data, "teal_data_module")) {+ paste0(sep = "\n") %>% |
163 | -1x | +18 | +5x |
- body(data$server)+ paste0(collapse = "") |
164 | +19 |
- }+ } |
||
165 | +20 | |||
166 | -14x | +|||
21 | +
- attr(filter, "app_id") <- rlang::hash(hashables)+ |
|||
167 | +22 | |||
168 | +23 |
- # convert teal.slice::teal_slices to teal::teal_slices+ get_rcode_str_install <- function() { |
||
169 | -14x | +24 | +9x |
- filter <- as.teal_slices(as.list(filter))+ code_string <- getOption("teal.load_nest_code") |
170 | +25 | |||
171 | -14x | +26 | +9x |
- if (isTRUE(attr(filter, "module_specific"))) {+ if (!is.null(code_string) && is.character(code_string)) { |
172 | -! | +|||
27 | +2x |
- module_names <- unlist(c(module_labels(modules), "global_filters"))+ return(code_string) |
||
173 | -! | +|||
28 | +
- failed_mod_names <- setdiff(names(attr(filter, "mapping")), module_names)+ } |
|||
174 | -! | +|||
29 | +
- if (length(failed_mod_names)) {+ |
|||
175 | -! | +|||
30 | +7x |
- stop(+ return("# Add any code to install/load your NEST environment here\n") |
||
176 | -! | +|||
31 | +
- sprintf(+ } |
|||
177 | -! | +|||
32 | +
- "Some module names in the mapping arguments don't match module labels.\n %s not in %s",+ |
|||
178 | -! | +|||
33 | +
- toString(failed_mod_names),+ #' Get datasets code |
|||
179 | -! | +|||
34 | +
- toString(unique(module_names))+ #' |
|||
180 | +35 |
- )+ #' Get combined code from `FilteredData` and from `CodeClass` object. |
||
181 | +36 |
- )+ #' |
||
182 | +37 |
- }+ #' @param datanames (`character`) names of datasets to extract code from |
||
183 | +38 |
-
+ #' @param datasets (`FilteredData`) object |
||
184 | -! | +|||
39 | +
- if (anyDuplicated(module_names)) {+ #' @param hashes named (`list`) of hashes per dataset |
|||
185 | +40 |
- # In teal we are able to set nested modules with duplicated label.+ #' |
||
186 | +41 |
- # Because mapping argument bases on the relationship between module-label and filter-id,+ #' @return Character string concatenated from the following elements: |
||
187 | +42 |
- # it is possible that module-label in mapping might refer to multiple teal_module (identified by the same label)+ #' - data pre-processing code (from `data` argument in `init`) |
||
188 | -! | +|||
43 | +
- stop(+ #' - hash check of loaded objects |
|||
189 | -! | +|||
44 | +
- sprintf(+ #' - filter code (if any) |
|||
190 | -! | +|||
45 | +
- "Module labels should be unique when teal_slices(mapping = TRUE). Duplicated labels:\n%s ",+ #' |
|||
191 | -! | +|||
46 | +
- toString(module_names[duplicated(module_names)])+ #' @keywords internal |
|||
192 | +47 |
- )+ get_datasets_code <- function(datanames, datasets, hashes) { |
||
193 | +48 |
- )+ # preprocessing code+ |
+ ||
49 | +4x | +
+ str_prepro <-+ |
+ ||
50 | +4x | +
+ teal.data:::get_code_dependency(attr(datasets, "preprocessing_code"), names = datanames, check_names = FALSE)+ |
+ ||
51 | +4x | +
+ if (length(str_prepro) == 0) {+ |
+ ||
52 | +! | +
+ str_prepro <- "message('Preprocessing is empty')" |
||
194 | +53 |
- }+ } else {+ |
+ ||
54 | +4x | +
+ str_prepro <- paste(str_prepro, collapse = "\n") |
||
195 | +55 |
} |
||
196 | +56 | |||
57 | ++ |
+ # hash checks+ |
+ ||
197 | -14x | +58 | +4x |
- if (inherits(data, "teal_data")) {+ str_hash <- vapply(datanames, function(dataname) { |
198 | -13x | +59 | +6x |
- if (length(teal_data_datanames(data)) == 0) {+ sprintf( |
199 | -1x | +60 | +6x |
- stop("`data` object has no datanames and its environment is empty. Specify `datanames(data)` and try again.")+ "stopifnot(%s == %s)",+ |
+
61 | +6x | +
+ deparse1(bquote(rlang::hash(.(as.name(dataname))))),+ |
+ ||
62 | +6x | +
+ deparse1(hashes[[dataname]]) |
||
200 | +63 |
- }+ )+ |
+ ||
64 | +4x | +
+ }, character(1))+ |
+ ||
65 | +4x | +
+ str_hash <- paste(str_hash, collapse = "\n") |
||
201 | +66 |
- # in case of teal_data_module this check is postponed to the srv_teal_with_splash+ |
||
202 | -12x | +|||
67 | +
- is_modules_ok <- check_modules_datanames(modules, teal_data_datanames(data))+ # filter expressions |
|||
203 | -12x | +68 | +4x |
- if (!isTRUE(is_modules_ok)) {+ str_filter <- teal.slice::get_filter_expr(datasets, datanames) |
204 | -1x | +69 | +4x |
- logger::log_error(is_modules_ok)+ if (str_filter == "") { |
205 | -1x | +70 | +2x |
- checkmate::assert(is_modules_ok, .var.name = "modules")+ str_filter <- character(0) |
206 | +71 |
- }+ } |
||
207 | +72 | |||
208 | -11x | +|||
73 | +
- is_filter_ok <- check_filter_datanames(filter, teal_data_datanames(data))+ # concatenate all code |
|||
209 | -11x | +74 | +4x |
- if (!isTRUE(is_filter_ok)) {+ str_code <- paste(c(str_prepro, str_hash, str_filter), collapse = "\n\n") |
210 | -1x | +75 | +4x |
- logger::log_warn(is_filter_ok)+ sprintf("%s\n", str_code) |
211 | +76 |
- # we allow app to continue if applied filters are outside+ } |
212 | +1 |
- # of possible data range+ #' Send input validation messages to output. |
|
213 | +2 |
- }+ #' |
|
214 | +3 |
- }+ #' Captures messages from `InputValidator` objects and collates them |
|
215 | +4 |
-
+ #' into one message passed to `validate`. |
|
216 | +5 |
- # Note regarding case `id = character(0)`:+ #' |
|
217 | +6 |
- # rather than using `callModule` and creating a submodule of this module, we directly modify+ #' `shiny::validate` is used to withhold rendering of an output element until |
|
218 | +7 |
- # the `ui` and `server` with `id = character(0)` and calling the server function directly+ #' certain conditions are met and to print a validation message in place |
|
219 | +8 |
- # rather than through `callModule`+ #' of the output element. |
|
220 | -12x | +||
9 | +
- res <- list(+ #' `shinyvalidate::InputValidator` allows to validate input elements |
||
221 | -12x | +||
10 | +
- ui = ui_teal_with_splash(id = id, data = data, title = title, header = header, footer = footer),+ #' and to display specific messages in their respective input widgets. |
||
222 | -12x | +||
11 | +
- server = function(input, output, session) {+ #' `validate_inputs` provides a hybrid solution. |
||
223 | -! | +||
12 | +
- if (length(landing) == 1L) {+ #' Given an `InputValidator` object, messages corresponding to inputs that fail validation |
||
224 | -! | +||
13 | +
- landing_module <- landing[[1L]]+ #' are extracted and placed in one validation message that is passed to a `validate`/`need` call. |
||
225 | -! | +||
14 | +
- do.call(landing_module$server, c(list(id = "landing_module_shiny_id"), landing_module$server_args))+ #' This way the input `validator` messages are repeated in the output. |
||
226 | +15 |
- }+ #' |
|
227 | -! | +||
16 | +
- filter <- deep_copy_filter(filter)+ #' The `...` argument accepts any number of `InputValidator` objects |
||
228 | -! | +||
17 | +
- srv_teal_with_splash(id = id, data = data, modules = modules, filter = filter)+ #' or a nested list of such objects. |
||
229 | +18 |
- }+ #' If `validators` are passed directly, all their messages are printed together |
|
230 | +19 |
- )+ #' under one (optional) header message specified by `header`. If a list is passed, |
|
231 | -12x | +||
20 | +
- logger::log_trace("init teal app has been initialized.")+ #' messages are grouped by `validator`. The list's names are used as headers |
||
232 | -12x | +||
21 | +
- return(res)+ #' for their respective message groups. |
||
233 | +22 |
- }+ #' If neither of the nested list elements is named, a header message is taken from `header`. |
1 | +23 |
- #' Get dummy `CDISC` data+ #' |
|
2 | +24 |
- #'+ #' @param ... either any number of `InputValidator` objects |
|
3 | +25 |
- #' Get dummy `CDISC` data including `ADSL`, `ADAE` and `ADLB`.+ #' or an optionally named, possibly nested `list` of `InputValidator` |
|
4 | +26 |
- #' Some NAs are also introduced to stress test.+ #' objects, see `Details` |
|
5 | +27 |
- #'+ #' @param header `character(1)` generic validation message; set to NULL to omit |
|
6 | +28 |
- #' @return `cdisc_data`+ #' |
|
7 | +29 |
- #' @keywords internal+ #' @return |
|
8 | +30 |
- example_cdisc_data <- function() { # nolint+ #' Returns NULL if the final validation call passes and a `shiny.silent.error` if it fails. |
|
9 | -! | +||
31 | +
- ADSL <- data.frame( # nolint+ #' |
||
10 | -! | +||
32 | +
- STUDYID = "study",+ #' @seealso [`shinyvalidate::InputValidator`], [`shiny::validate`] |
||
11 | -! | +||
33 | +
- USUBJID = 1:10,+ #' |
||
12 | -! | +||
34 | +
- SEX = sample(c("F", "M"), 10, replace = TRUE),+ #' @examples |
||
13 | -! | +||
35 | +
- AGE = stats::rpois(10, 40)+ #' library(shiny) |
||
14 | +36 |
- )+ #' library(shinyvalidate) |
|
15 | -! | +||
37 | +
- ADTTE <- rbind(ADSL, ADSL, ADSL) # nolint+ #' |
||
16 | -! | +||
38 | +
- ADTTE$PARAMCD <- rep(c("OS", "EFS", "PFS"), each = 10) # nolint+ #' ui <- fluidPage( |
||
17 | -! | +||
39 | +
- ADTTE$AVAL <- c( # nolint+ #' selectInput("method", "validation method", c("sequential", "combined", "grouped")), |
||
18 | -! | +||
40 | +
- stats::rnorm(10, mean = 700, sd = 200), # dummy OS level+ #' sidebarLayout( |
||
19 | -! | +||
41 | +
- stats::rnorm(10, mean = 400, sd = 100), # dummy EFS level+ #' sidebarPanel( |
||
20 | -! | +||
42 | +
- stats::rnorm(10, mean = 450, sd = 200) # dummy PFS level+ #' selectInput("letter", "select a letter:", c(letters[1:3], LETTERS[4:6])), |
||
21 | +43 |
- )+ #' selectInput("number", "select a number:", 1:6), |
|
22 | +44 |
-
+ #' br(), |
|
23 | -! | +||
45 | +
- ADSL$logical_test <- sample(c(TRUE, FALSE, NA), size = nrow(ADSL), replace = TRUE) # nolint+ #' selectInput("color", "select a color:", |
||
24 | -! | +||
46 | +
- ADSL$SEX[c(2, 5)] <- NA # nolint+ #' c("black", "indianred2", "springgreen2", "cornflowerblue"), |
||
25 | +47 |
-
+ #' multiple = TRUE |
|
26 | -! | +||
48 | +
- res <- teal.data::cdisc_data(+ #' ), |
||
27 | -! | +||
49 | +
- ADSL = ADSL,+ #' sliderInput("size", "select point size:", |
||
28 | -! | +||
50 | +
- ADTTE = ADTTE,+ #' min = 0.1, max = 4, value = 0.25 |
||
29 | -! | +||
51 | ++ |
+ #' )+ |
+ |
52 | ++ |
+ #' ),+ |
+ |
53 | ++ |
+ #' mainPanel(plotOutput("plot"))+ |
+ |
54 | ++ |
+ #' )+ |
+ |
55 | ++ |
+ #' )+ |
+ |
56 | ++ |
+ #'+ |
+ |
57 | ++ |
+ #' server <- function(input, output) {+ |
+ |
58 | ++ |
+ #' # set up input validation+ |
+ |
59 | ++ |
+ #' iv <- InputValidator$new()+ |
+ |
60 | ++ |
+ #' iv$add_rule("letter", sv_in_set(LETTERS, "choose a capital letter"))+ |
+ |
61 | ++ |
+ #' iv$add_rule("number", ~ if (as.integer(.) %% 2L == 1L) "choose an even number")+ |
+ |
62 | ++ |
+ #' iv$enable()+ |
+ |
63 | ++ |
+ #' # more input validation+ |
+ |
64 | ++ |
+ #' iv_par <- InputValidator$new()+ |
+ |
65 | ++ |
+ #' iv_par$add_rule("color", sv_required(message = "choose a color"))+ |
+ |
66 | ++ |
+ #' iv_par$add_rule("color", ~ if (length(.) > 1L) "choose only one color")+ |
+ |
67 | ++ |
+ #' iv_par$add_rule(+ |
+ |
68 | ++ |
+ #' "size",+ |
+ |
69 | ++ |
+ #' sv_between(+ |
+ |
70 | ++ |
+ #' left = 0.5, right = 3,+ |
+ |
71 | ++ |
+ #' message_fmt = "choose a value between {left} and {right}"+ |
+ |
72 | ++ |
+ #' )+ |
+ |
73 | ++ |
+ #' )+ |
+ |
74 | ++ |
+ #' iv_par$enable()+ |
+ |
75 | ++ |
+ #'+ |
+ |
76 | ++ |
+ #' output$plot <- renderPlot({+ |
+ |
77 | ++ |
+ #' # validate output+ |
+ |
78 | ++ |
+ #' switch(input[["method"]],+ |
+ |
79 | ++ |
+ #' "sequential" = {+ |
+ |
80 | ++ |
+ #' validate_inputs(iv)+ |
+ |
81 | ++ |
+ #' validate_inputs(iv_par, header = "Set proper graphical parameters")+ |
+ |
82 | ++ |
+ #' },+ |
+ |
83 | ++ |
+ #' "combined" = validate_inputs(iv, iv_par),+ |
+ |
84 | ++ |
+ #' "grouped" = validate_inputs(list(+ |
+ |
85 | ++ |
+ #' "Some inputs require attention" = iv,+ |
+ |
86 | ++ |
+ #' "Set proper graphical parameters" = iv_par+ |
+ |
87 | ++ |
+ #' ))+ |
+ |
88 | ++ |
+ #' )+ |
+ |
89 | ++ |
+ #'+ |
+ |
90 | ++ |
+ #' plot(eruptions ~ waiting, faithful,+ |
+ |
91 | ++ |
+ #' las = 1, pch = 16,+ |
+ |
92 | ++ |
+ #' col = input[["color"]], cex = input[["size"]]+ |
+ |
93 | ++ |
+ #' )+ |
+ |
94 | ++ |
+ #' })+ |
+ |
95 | ++ |
+ #' }+ |
+ |
96 | ++ |
+ #'+ |
+ |
97 | ++ |
+ #' if (interactive()) {+ |
+ |
98 | ++ |
+ #' shinyApp(ui, server)+ |
+ |
99 | ++ |
+ #' }+ |
+ |
100 | ++ |
+ #'+ |
+ |
101 | ++ |
+ #' @export+ |
+ |
102 | ++ |
+ #'+ |
+ |
103 | ++ |
+ validate_inputs <- function(..., header = "Some inputs require attention") {+ |
+ |
104 | +36x | +
+ dots <- list(...)+ |
+ |
105 | +2x | +
+ if (!is_validators(dots)) stop("validate_inputs accepts validators or a list thereof")+ |
+ |
106 | ++ | + + | +|
107 | +34x | +
+ messages <- extract_validator(dots, header)+ |
+ |
108 | +34x | +
+ failings <- if (!any_names(dots)) {+ |
+ |
109 | +29x | +
+ add_header(messages, header)+ |
+ |
110 | ++ |
+ } else {+ |
+ |
111 | +5x | +
+ unlist(messages)+ |
+ |
112 | ++ |
+ }+ |
+ |
113 | ++ | + + | +|
114 | +34x | +
+ shiny::validate(shiny::need(is.null(failings), failings))+ |
+ |
115 | ++ |
+ }+ |
+ |
116 | ++ | + + | +|
117 | ++ |
+ ### internal functions+ |
+ |
118 | ++ | + + | +|
119 | ++ |
+ #' @keywords internal+ |
+ |
120 | ++ |
+ # recursive object type test+ |
+ |
121 | ++ |
+ # returns logical of length 1+ |
+ |
122 | ++ |
+ is_validators <- function(x) {+ |
+ |
123 | +118x | +
+ all(if (is.list(x)) unlist(lapply(x, is_validators)) else inherits(x, "InputValidator"))+ |
+ |
124 | ++ |
+ }+ |
+ |
125 | ++ | + + | +|
126 | ++ |
+ #' @keywords internal+ |
+ |
127 | ++ |
+ # test if an InputValidator object is enabled+ |
+ |
128 | ++ |
+ # returns logical of length 1+ |
+ |
129 | ++ |
+ # official method requested at https://github.com/rstudio/shinyvalidate/issues/64+ |
+ |
130 | ++ |
+ validator_enabled <- function(x) {+ |
+ |
131 | +49x | +
+ x$.__enclos_env__$private$enabled+ |
+ |
132 | ++ |
+ }+ |
+ |
133 | ++ | + + | +|
134 | ++ |
+ #' @keywords internal+ |
+ |
135 | ++ |
+ # recursively extract messages from validator list+ |
+ |
136 | ++ |
+ # returns character vector or a list of character vectors, possibly nested and named+ |
+ |
137 | ++ |
+ extract_validator <- function(iv, header) {+ |
+ |
138 | +113x | +
+ if (inherits(iv, "InputValidator")) {+ |
+ |
139 | +49x | +
+ add_header(gather_messages(iv), header)+ |
+ |
140 | ++ |
+ } else {+ |
+ |
141 | +58x | +
+ if (is.null(names(iv))) names(iv) <- rep("", length(iv))+ |
+ |
142 | +64x | +
+ mapply(extract_validator, iv = iv, header = names(iv), SIMPLIFY = FALSE)+ |
+ |
143 | ++ |
+ }+ |
+ |
144 | ++ |
+ }+ |
+ |
145 | ++ | + + | +|
146 | ++ |
+ #' @keywords internal+ |
+ |
147 | +
- code = '+ # collate failing messages from validator |
||
30 | -! | +||
148 | +
- ADSL <- data.frame(+ # returns list |
||
31 | -! | +||
149 | +
- STUDYID = "study",+ gather_messages <- function(iv) { |
||
32 | -! | +||
150 | +49x |
- USUBJID = 1:10,+ if (validator_enabled(iv)) { |
|
33 | -! | +||
151 | +46x |
- SEX = sample(c("F", "M"), 10, replace = TRUE),+ status <- iv$validate() |
|
34 | -! | +||
152 | +46x |
- AGE = rpois(10, 40)+ failing_inputs <- Filter(Negate(is.null), status)+ |
+ |
153 | +46x | +
+ unique(lapply(failing_inputs, function(x) x[["message"]])) |
|
35 | +154 |
- )+ } else { |
|
36 | -! | +||
155 | +3x |
- ADTTE <- rbind(ADSL, ADSL, ADSL)+ warning("Validator is disabled and will be omitted.") |
|
37 | -! | +||
156 | +3x |
- ADTTE$PARAMCD <- rep(c("OS", "EFS", "PFS"), each = 10)+ list() |
|
38 | -! | +||
157 | +
- ADTTE$AVAL <- c(+ } |
||
39 | -! | +||
158 | +
- rnorm(10, mean = 700, sd = 200),+ } |
||
40 | -! | +||
159 | +
- rnorm(10, mean = 400, sd = 100),+ |
||
41 | -! | +||
160 | +
- rnorm(10, mean = 450, sd = 200)+ #' @keywords internal |
||
42 | +161 |
- )+ # add optional header to failing messages |
|
43 | +162 | ++ |
+ add_header <- function(messages, header = "") {+ |
+
163 | +78x | +
+ ans <- unlist(messages)+ |
+ |
164 | +78x | +
+ if (length(ans) != 0L && isTRUE(nchar(header) > 0L)) {+ |
+ |
165 | +31x | +
+ ans <- c(paste0(header, "\n"), ans, "\n")+ |
+ |
166 | ++ |
+ }+ |
+ |
167 | +78x | +
+ ans+ |
+ |
168 | ++ |
+ }+ |
+ |
169 | |||
44 | -! | +||
170 | +
- ADSL$logical_test <- sample(c(TRUE, FALSE, NA), size = nrow(ADSL), replace = TRUE)+ #' @keywords internal |
||
45 | -! | +||
171 | ++ |
+ # recursively check if the object contains a named list+ |
+ |
172 | ++ |
+ any_names <- function(x) {+ |
+ |
173 | +103x | +
+ any(+ |
+ |
174 | +103x | +
+ if (is.list(x)) {+ |
+ |
175 | +58x | +
+ if (!is.null(names(x)) && any(names(x) != "")) TRUE else unlist(lapply(x, any_names))+ |
+ |
176 | ++ |
+ } else {+ |
+ |
177 | +40x | +
+ FALSE+ |
+ |
178 | ++ |
+ }+ |
+ |
179 | ++ |
+ )+ |
+ |
180 | ++ |
+ }+ |
+
1 | ++ |
+ #' Show R Code Modal+ |
+ |
2 | +
- ADSL$SEX[c(2, 5)] <- NA+ #' |
||
46 | +3 |
- '+ #' @export |
|
47 | +4 |
- )+ #' @description `r lifecycle::badge("stable")` |
|
48 | -! | +||
5 | +
- return(res)+ #' Use the [shiny::showModal()] function to show the R code inside. |
||
49 | +6 |
- }+ #' |
|
50 | +7 |
-
+ #' @param title (`character(1)`)\cr |
|
51 | +8 |
- #' Get datasets to go with example modules.+ #' Title of the modal, displayed in the first comment of the R-code. |
|
52 | +9 |
- #'+ #' @param rcode (`character`)\cr |
|
53 | +10 |
- #' Creates a nested list, the structure of which matches the module hierarchy created by `example_modules`.+ #' vector with R code to show inside the modal. |
|
54 | +11 |
- #' Each list leaf is the same `FilteredData` object.+ #' @param session (`ShinySession` optional)\cr |
|
55 | +12 |
- #'+ #' `shiny` Session object, if missing then [shiny::getDefaultReactiveDomain()] is used. |
|
56 | +13 |
- #' @return named list of `FilteredData` objects, each with `ADSL` set.+ #' |
|
57 | +14 |
- #' @keywords internal+ #' @references [shiny::showModal()] |
|
58 | +15 |
- example_datasets <- function() { # nolint+ show_rcode_modal <- function(title = NULL, rcode, session = getDefaultReactiveDomain()) { |
|
59 | +16 | ! |
- dummy_cdisc_data <- example_cdisc_data()+ rcode <- paste(rcode, collapse = "\n")+ |
+
17 | ++ | + | |
60 | +18 | ! |
- datasets <- teal_data_to_filtered_data(dummy_cdisc_data)+ ns <- session$ns |
61 | +19 | ! |
- list(+ showModal(modalDialog( |
62 | +20 | ! |
- "d2" = list(+ tagList( |
63 | +21 | ! |
- "d3" = list(+ tags$div( |
64 | +22 | ! |
- "aaa1" = datasets,+ actionButton(ns("copyRCode"), "Copy to Clipboard", `data-clipboard-target` = paste0("#", ns("r_code"))), |
65 | +23 | ! |
- "aaa2" = datasets,+ modalButton("Dismiss"), |
66 | +24 | ! |
- "aaa3" = datasets+ style = "mb-4" |
67 | +25 |
), |
|
68 | +26 | ! |
- "bbb" = datasets+ tags$div(tags$pre(id = ns("r_code"), rcode)), |
69 | +27 |
), |
|
70 | +28 | ! |
- "ccc" = datasets- |
-
71 | -- |
- )- |
- |
72 | -- |
- }- |
- |
73 | -- | - - | -|
74 | -- |
- #' An example `teal` module- |
- |
75 | -- |
- #'- |
- |
76 | -- |
- #' @description `r lifecycle::badge("experimental")`+ title = title, |
|
77 | -+ | ||
29 | +! |
- #' @inheritParams module+ footer = tagList( |
|
78 | -+ | ||
30 | +! |
- #' @return A `teal` module which can be included in the `modules` argument to [teal::init()].+ actionButton(ns("copyRCode"), "Copy to Clipboard", `data-clipboard-target` = paste0("#", ns("r_code"))), |
|
79 | -+ | ||
31 | +! |
- #' @examples+ modalButton("Dismiss") |
|
80 | +32 |
- #' app <- init(+ ), |
|
81 | -+ | ||
33 | +! |
- #' data = teal_data(IRIS = iris, MTCARS = mtcars),+ size = "l", |
|
82 | -+ | ||
34 | +! |
- #' modules = example_module()+ easyClose = TRUE |
|
83 | +35 |
- #' )+ )) |
|
84 | +36 |
- #' if (interactive()) {+ |
|
85 | -+ | ||
37 | +! |
- #' shinyApp(app$ui, app$server)+ return(NULL) |
|
86 | +38 |
- #' }+ } |
87 | +1 |
- #' @export+ #' Validate that dataset has a minimum number of observations |
|
88 | +2 |
- example_module <- function(label = "example teal module", datanames = "all") {- |
- |
89 | -44x | -
- checkmate::assert_string(label)- |
- |
90 | -44x | -
- module(- |
- |
91 | -44x | -
- label,+ #' |
- |
92 | -44x | +||
3 | +
- server = function(id, data) {+ #' @description `r lifecycle::badge("stable")` |
||
93 | -! | +||
4 | +
- checkmate::assert_class(data(), "teal_data")+ #' @param x a data.frame |
||
94 | -! | +||
5 | +
- moduleServer(id, function(input, output, session) {+ #' @param min_nrow minimum number of rows in \code{x} |
||
95 | -! | +||
6 | +
- ns <- session$ns+ #' @param complete \code{logical} default \code{FALSE} when set to \code{TRUE} then complete cases are checked. |
||
96 | -! | +||
7 | +
- updateSelectInput(session, "dataname", choices = isolate(teal.data::datanames(data())))+ #' @param allow_inf \code{logical} default \code{TRUE} when set to \code{FALSE} then error thrown if any values are |
||
97 | -! | +||
8 | +
- output$text <- renderPrint({+ #' infinite. |
||
98 | -! | +||
9 | +
- req(input$dataname)+ #' @param msg (`character(1)`) additional message to display alongside the default message. |
||
99 | -! | +||
10 | +
- data()[[input$dataname]]+ #' |
||
100 | +11 |
- })+ #' @details This function is a wrapper for `shiny::validate`. |
|
101 | -! | +||
12 | +
- teal.widgets::verbatim_popup_srv(+ #' |
||
102 | -! | +||
13 | +
- id = "rcode",+ #' @export |
||
103 | -! | +||
14 | +
- verbatim_content = reactive(teal.code::get_code(data())),+ #' |
||
104 | -! | +||
15 | +
- title = "Association Plot"+ #' @examples |
||
105 | +16 |
- )+ #' library(teal) |
|
106 | +17 |
- })+ #' ui <- fluidPage( |
|
107 | +18 |
- },+ #' sliderInput("len", "Max Length of Sepal", |
|
108 | -44x | +||
19 | +
- ui = function(id) {+ #' min = 4.3, max = 7.9, value = 5 |
||
109 | -! | +||
20 | +
- ns <- NS(id)+ #' ), |
||
110 | -! | +||
21 | +
- teal.widgets::standard_layout(+ #' plotOutput("plot") |
||
111 | -! | +||
22 | +
- output = verbatimTextOutput(ns("text")),+ #' ) |
||
112 | -! | +||
23 | +
- encoding = div(+ #' |
||
113 | -! | +||
24 | +
- selectInput(ns("dataname"), "Choose a dataset", choices = NULL),+ #' server <- function(input, output) { |
||
114 | -! | +||
25 | +
- teal.widgets::verbatim_popup_ui(ns("rcode"), "Show R code")+ #' output$plot <- renderPlot({ |
||
115 | +26 |
- )+ #' df <- iris[iris$Sepal.Length <= input$len, ] |
|
116 | +27 |
- )+ #' validate_has_data( |
|
117 | +28 |
- },+ #' iris_f, |
|
118 | -44x | +||
29 | +
- datanames = datanames+ #' min_nrow = 10, |
||
119 | +30 |
- )+ #' complete = FALSE, |
|
120 | +31 |
- }+ #' msg = "Please adjust Max Length of Sepal" |
|
121 | +32 |
-
+ #' ) |
|
122 | +33 |
-
+ #' |
|
123 | +34 |
- #' Get example modules.+ #' hist(iris_f$Sepal.Length, breaks = 5) |
|
124 | +35 |
- #'+ #' }) |
|
125 | +36 |
- #' Creates an example hierarchy of `teal_modules` from which a `teal` app can be created.+ #' } |
|
126 | +37 |
- #' @param datanames (`character`)\cr+ #' if (interactive()) { |
|
127 | +38 |
- #' names of the datasets to be used in the example modules. Possible choices are `ADSL`, `ADTTE`.+ #' shinyApp(ui, server) |
|
128 | +39 |
- #' @return `teal_modules`+ #' } |
|
129 | +40 |
- #' @keywords internal+ #' |
|
130 | +41 |
- example_modules <- function(datanames = c("ADSL", "ADTTE")) {+ validate_has_data <- function(x, |
|
131 | -! | +||
42 | +
- checkmate::assert_subset(datanames, c("ADSL", "ADTTE"))+ min_nrow = NULL, |
||
132 | -! | +||
43 | +
- mods <- modules(+ complete = FALSE, |
||
133 | -! | +||
44 | +
- label = "d1",+ allow_inf = TRUE, |
||
134 | -! | +||
45 | +
- modules(+ msg = NULL) { |
||
135 | -! | +||
46 | +17x |
- label = "d2",+ checkmate::assert_string(msg, null.ok = TRUE) |
|
136 | -! | +||
47 | +15x |
- modules(+ checkmate::assert_data_frame(x) |
|
137 | -! | +||
48 | +15x |
- label = "d3",+ if (!is.null(min_nrow)) { |
|
138 | -! | +||
49 | +15x |
- example_module(label = "aaa1", datanames = datanames),+ if (complete) { |
|
139 | -! | +||
50 | +5x |
- example_module(label = "aaa2", datanames = datanames),+ complete_index <- stats::complete.cases(x) |
|
140 | -! | +||
51 | +5x |
- example_module(label = "aaa3", datanames = datanames)+ validate(need( |
|
141 | -+ | ||
52 | +5x |
- ),+ sum(complete_index) > 0 && nrow(x[complete_index, , drop = FALSE]) >= min_nrow, |
|
142 | -! | +||
53 | +5x |
- example_module(label = "bbb", datanames = datanames)+ paste(c(paste("Number of complete cases is less than:", min_nrow), msg), collapse = "\n") |
|
143 | +54 |
- ),- |
- |
144 | -! | -
- example_module(label = "ccc", datanames = datanames)+ )) |
|
145 | +55 |
- )+ } else { |
|
146 | -! | +||
56 | +10x |
- return(mods)+ validate(need( |
|
147 | -+ | ||
57 | +10x |
- }+ nrow(x) >= min_nrow, |
1 | -+ | |||
58 | +10x |
- #' Generates library calls from current session info+ paste( |
||
2 | -+ | |||
59 | +10x |
- #'+ c(paste("Minimum number of records not met: >=", min_nrow, "records required."), msg), |
||
3 | -+ | |||
60 | +10x |
- #' Function to create multiple library calls out of current session info to make reproducible code works.+ collapse = "\n" |
||
4 | +61 |
- #'+ ) |
||
5 | +62 |
- #' @return Character object contain code+ )) |
||
6 | +63 |
- #' @keywords internal+ } |
||
7 | +64 |
- get_rcode_libraries <- function() {+ |
||
8 | -5x | +65 | +10x |
- vapply(+ if (!allow_inf) { |
9 | -5x | +66 | +6x |
- utils::sessionInfo()$otherPkgs,+ validate(need( |
10 | -5x | +67 | +6x |
- function(x) {+ all(vapply(x, function(col) !is.numeric(col) || !any(is.infinite(col)), logical(1))), |
11 | -80x | +68 | +6x |
- paste0("library(", x$Package, ")")+ "Dataframe contains Inf values which is not allowed." |
12 | +69 |
- },- |
- ||
13 | -5x | -
- character(1)+ )) |
||
14 | +70 |
- ) %>%+ } |
||
15 | +71 |
- # put it into reverse order to correctly simulate executed code- |
- ||
16 | -5x | -
- rev() %>%+ } |
||
17 | -5x | +|||
72 | +
- paste0(sep = "\n") %>%+ } |
|||
18 | -5x | +|||
73 | +
- paste0(collapse = "")+ |
|||
19 | +74 |
- }+ #' Validate that dataset has unique rows for key variables |
||
20 | +75 |
-
+ #' |
||
21 | +76 |
-
+ #' @description `r lifecycle::badge("stable")` |
||
22 | +77 |
-
+ #' @param x a data.frame |
||
23 | +78 |
- get_rcode_str_install <- function() {+ #' @param key a vector of ID variables from \code{x} that identify unique records |
||
24 | -9x | +|||
79 | +
- code_string <- getOption("teal.load_nest_code")+ #' |
|||
25 | +80 |
-
+ #' @details This function is a wrapper for `shiny::validate`. |
||
26 | -9x | +|||
81 | +
- if (!is.null(code_string) && is.character(code_string)) {+ #' |
|||
27 | -2x | +|||
82 | +
- return(code_string)+ #' @export |
|||
28 | +83 |
- }+ #' |
||
29 | +84 |
-
+ #' @examples |
||
30 | -7x | +|||
85 | +
- return("# Add any code to install/load your NEST environment here\n")+ #' iris$id <- rep(1:50, times = 3) |
|||
31 | +86 |
- }+ #' ui <- fluidPage( |
||
32 | +87 |
-
+ #' selectInput( |
||
33 | +88 |
- #' Get datasets code+ #' inputId = "species", |
||
34 | +89 |
- #'+ #' label = "Select species", |
||
35 | +90 |
- #' Get combined code from `FilteredData` and from `CodeClass` object.+ #' choices = c("setosa", "versicolor", "virginica"), |
||
36 | +91 |
- #'+ #' selected = "setosa", |
||
37 | +92 |
- #' @param datanames (`character`) names of datasets to extract code from+ #' multiple = TRUE |
||
38 | +93 |
- #' @param datasets (`FilteredData`) object+ #' ), |
||
39 | +94 |
- #' @param hashes named (`list`) of hashes per dataset+ #' plotOutput("plot") |
||
40 | +95 |
- #'+ #' ) |
||
41 | +96 |
- #' @return Character string concatenated from the following elements:+ #' server <- function(input, output) { |
||
42 | +97 |
- #' - data pre-processing code (from `data` argument in `init`)+ #' output$plot <- renderPlot({ |
||
43 | +98 |
- #' - hash check of loaded objects+ #' iris_f <- iris[iris$Species %in% input$species, ] |
||
44 | +99 |
- #' - filter code (if any)+ #' validate_one_row_per_id(iris_f, key = c("id")) |
||
45 | +100 |
#' |
||
46 | +101 |
- #' @keywords internal+ #' hist(iris_f$Sepal.Length, breaks = 5) |
||
47 | +102 |
- get_datasets_code <- function(datanames, datasets, hashes) {+ #' }) |
||
48 | +103 |
- # preprocessing code+ #' } |
||
49 | -4x | +|||
104 | +
- str_prepro <-+ #' if (interactive()) { |
|||
50 | -4x | +|||
105 | +
- teal.data:::get_code_dependency(attr(datasets, "preprocessing_code"), names = datanames, check_names = FALSE)+ #' shinyApp(ui, server) |
|||
51 | -4x | +|||
106 | +
- if (length(str_prepro) == 0) {+ #' } |
|||
52 | -! | +|||
107 | +
- str_prepro <- "message('Preprocessing is empty')"+ #' |
|||
53 | +108 |
- } else {+ validate_one_row_per_id <- function(x, key = c("USUBJID", "STUDYID")) { |
||
54 | -4x | +|||
109 | +! |
- str_prepro <- paste(str_prepro, collapse = "\n")+ validate(need(!any(duplicated(x[key])), paste("Found more than one row per id."))) |
||
55 | +110 |
- }+ } |
||
56 | +111 | |||
57 | +112 |
- # hash checks- |
- ||
58 | -4x | -
- str_hash <- vapply(datanames, function(dataname) {+ #' Validates that vector includes all expected values |
||
59 | -6x | +|||
113 | +
- sprintf(+ #' |
|||
60 | -6x | +|||
114 | +
- "stopifnot(%s == %s)",+ #' @description `r lifecycle::badge("stable")` |
|||
61 | -6x | +|||
115 | +
- deparse1(bquote(rlang::hash(.(as.name(dataname))))),+ #' @param x values to test. All must be in \code{choices} |
|||
62 | -6x | +|||
116 | +
- deparse1(hashes[[dataname]])+ #' @param choices a vector to test for values of \code{x} |
|||
63 | +117 |
- )+ #' @param msg warning message to display |
||
64 | -4x | +|||
118 | +
- }, character(1))+ #' |
|||
65 | -4x | +|||
119 | +
- str_hash <- paste(str_hash, collapse = "\n")+ #' @details This function is a wrapper for `shiny::validate`. |
|||
66 | +120 |
-
+ #' |
||
67 | +121 |
- # filter expressions+ #' @export |
||
68 | -4x | +|||
122 | +
- str_filter <- teal.slice::get_filter_expr(datasets, datanames)+ #' |
|||
69 | -4x | +|||
123 | +
- if (str_filter == "") {+ #' @examples |
|||
70 | -2x | +|||
124 | +
- str_filter <- character(0)+ #' ui <- fluidPage( |
|||
71 | +125 |
- }+ #' selectInput( |
||
72 | +126 |
-
+ #' "species", |
||
73 | +127 |
- # concatenate all code+ #' "Select species", |
||
74 | -4x | +|||
128 | +
- str_code <- paste(c(str_prepro, str_hash, str_filter), collapse = "\n\n")+ #' choices = c("setosa", "versicolor", "virginica", "unknown species"), |
|||
75 | -4x | +|||
129 | +
- sprintf("%s\n", str_code)+ #' selected = "setosa", |
|||
76 | +130 |
- }+ #' multiple = FALSE |
1 | +131 |
- # This file contains Shiny modules useful for debugging and developing teal.+ #' ), |
|
2 | +132 |
- # We do not export the functions in this file. They are for+ #' verbatimTextOutput("summary") |
|
3 | +133 |
- # developers only and can be accessed via `:::`.+ #' ) |
|
4 | +134 |
-
+ #' |
|
5 | +135 |
- #' Dummy module to show the filter calls generated by the right encoding panel+ #' server <- function(input, output) { |
|
6 | +136 |
- #'+ #' output$summary <- renderPrint({ |
|
7 | +137 |
- #'+ #' validate_in(input$species, iris$Species, "Species does not exist.") |
|
8 | +138 |
- #' Please do not remove, this is useful for debugging teal without+ #' nrow(iris[iris$Species == input$species, ]) |
|
9 | +139 |
- #' dependencies and simplifies `\link[devtools]{load_all}` which otherwise fails+ #' }) |
|
10 | +140 |
- #' and avoids session restarts!+ #' } |
|
11 | +141 |
- #'+ #' if (interactive()) { |
|
12 | +142 |
- #' @param label `character` label of module+ #' shinyApp(ui, server) |
|
13 | +143 |
- #' @keywords internal+ #' } |
|
14 | +144 |
#' |
|
15 | +145 |
- #' @examples+ validate_in <- function(x, choices, msg) {+ |
+ |
146 | +! | +
+ validate(need(length(x) > 0 && length(choices) > 0 && all(x %in% choices), msg)) |
|
16 | +147 |
- #' app <- init(+ } |
|
17 | +148 |
- #' data = teal_data(iris = iris, mtcars = mtcars),+ |
|
18 | +149 |
- #' modules = teal:::filter_calls_module(),+ #' Validates that vector has length greater than 0 |
|
19 | +150 |
- #' header = "Simple teal app"+ #' |
|
20 | +151 |
- #' )+ #' @description `r lifecycle::badge("stable")` |
|
21 | +152 |
- #' if (interactive()) {+ #' @param x vector |
|
22 | +153 |
- #' shinyApp(app$ui, app$server)+ #' @param msg message to display |
|
23 | +154 |
- #' }+ #' |
|
24 | +155 |
- filter_calls_module <- function(label = "Filter Calls Module") { # nolint+ #' @details This function is a wrapper for `shiny::validate`. |
|
25 | -! | +||
156 | +
- checkmate::assert_string(label)+ #' |
||
26 | +157 |
-
+ #' @export |
|
27 | -! | +||
158 | +
- module(+ #' |
||
28 | -! | +||
159 | +
- label = label,+ #' @examples |
||
29 | -! | +||
160 | +
- server = function(input, output, session, data) {+ #' data <- data.frame( |
||
30 | -! | +||
161 | +
- checkmate::assert_class(data, "reactive")+ #' id = c(1:10, 11:20, 1:10), |
||
31 | -! | +||
162 | +
- checkmate::assert_class(isolate(data()), "teal_data")+ #' strata = rep(c("A", "B"), each = 15) |
||
32 | +163 |
-
+ #' ) |
|
33 | -! | +||
164 | +
- output$filter_calls <- renderText({+ #' ui <- fluidPage( |
||
34 | -! | +||
165 | +
- teal.data::get_code(data())+ #' selectInput("ref1", "Select strata1 to compare", |
||
35 | +166 |
- })+ #' choices = c("A", "B", "C"), selected = "A" |
|
36 | +167 |
- },+ #' ), |
|
37 | -! | +||
168 | +
- ui = function(id, ...) {+ #' selectInput("ref2", "Select strata2 to compare", |
||
38 | -! | +||
169 | +
- ns <- NS(id)+ #' choices = c("A", "B", "C"), selected = "B" |
||
39 | -! | +||
170 | +
- div(+ #' ), |
||
40 | -! | +||
171 | +
- h2("The following filter calls are generated:"),+ #' verbatimTextOutput("arm_summary") |
||
41 | -! | +||
172 | +
- verbatimTextOutput(ns("filter_calls"))+ #' ) |
||
42 | +173 |
- )+ #' |
|
43 | +174 |
- },+ #' server <- function(input, output) { |
|
44 | -! | +||
175 | +
- datanames = "all"+ #' output$arm_summary <- renderText({ |
||
45 | +176 |
- )+ #' sample_1 <- data$id[data$strata == input$ref1] |
|
46 | +177 |
- }+ #' sample_2 <- data$id[data$strata == input$ref2] |
1 | +178 |
- setOldClass("teal_data_module")+ #' |
|
2 | +179 |
-
+ #' validate_has_elements(sample_1, "No subjects in strata1.") |
|
3 | +180 |
- #' Evaluate Code on `teal_data_module`+ #' validate_has_elements(sample_2, "No subjects in strata2.") |
|
4 | +181 |
#' |
|
5 | +182 |
- #' @details+ #' paste0( |
|
6 | +183 |
- #' `eval_code` evaluates given code in the environment of the `teal_data` object created by the `teal_data_module`.+ #' "Number of samples in: strata1=", length(sample_1), |
|
7 | +184 |
- #' The code is added to the `@code` slot of the `teal_data`.+ #' " comparions strata2=", length(sample_2) |
|
8 | +185 |
- #'+ #' ) |
|
9 | +186 |
- #' @param object (`teal_data_module`)+ #' }) |
|
10 | +187 |
- #' @inheritParams teal.code::eval_code+ #' } |
|
11 | +188 |
- #'+ #' if (interactive()) { |
|
12 | +189 |
- #' @return+ #' shinyApp(ui, server) |
|
13 | +190 |
- #' `eval_code` returns a `teal_data_module` object with a delayed evaluation of `code` when the module is run.+ #' } |
|
14 | +191 |
- #'+ validate_has_elements <- function(x, msg) { |
|
15 | -+ | ||
192 | +! |
- #' @examples+ validate(need(length(x) > 0, msg)) |
|
16 | +193 |
- #' tdm <- teal_data_module(+ } |
|
17 | +194 |
- #' ui = function(id) div(id = shiny::NS(id)("div_id")),+ |
|
18 | +195 |
- #' server = function(id) {+ #' Validates no intersection between two vectors |
|
19 | +196 |
- #' shiny::moduleServer(id, function(input, output, session) {+ #' |
|
20 | +197 |
- #' shiny::reactive(teal_data(IRIS = iris))+ #' @description `r lifecycle::badge("stable")` |
|
21 | +198 |
- #' })+ #' @param x vector |
|
22 | +199 |
- #' }+ #' @param y vector |
|
23 | +200 |
- #' )+ #' @param msg message to display if \code{x} and \code{y} intersect |
|
24 | +201 |
- #' eval_code(tdm, "IRIS <- subset(IRIS, Species == 'virginica')")+ #' |
|
25 | +202 |
- #'+ #' @details This function is a wrapper for `shiny::validate`. |
|
26 | +203 |
- #' @include teal_data_module.R+ #' |
|
27 | +204 |
- #' @name eval_code+ #' @export |
|
28 | +205 |
- #' @rdname teal_data_module+ #' |
|
29 | +206 |
- #' @aliases eval_code,teal_data_module,character-method+ #' @examples |
|
30 | +207 |
- #' @aliases eval_code,teal_data_module,language-method+ #' data <- data.frame( |
|
31 | +208 |
- #' @aliases eval_code,teal_data_module,expression-method+ #' id = c(1:10, 11:20, 1:10), |
|
32 | +209 |
- #'+ #' strata = rep(c("A", "B", "C"), each = 10) |
|
33 | +210 |
- #' @importFrom methods setMethod+ #' ) |
|
34 | +211 |
- #' @importMethodsFrom teal.code eval_code+ #' |
|
35 | +212 |
- #'+ #' ui <- fluidPage( |
|
36 | +213 |
- setMethod("eval_code", signature = c("teal_data_module", "character"), function(object, code) {+ #' selectInput("ref1", "Select strata1 to compare", |
|
37 | -13x | +||
214 | +
- teal_data_module(+ #' choices = c("A", "B", "C"), |
||
38 | -13x | +||
215 | +
- ui = function(id) {+ #' selected = "A" |
||
39 | -1x | +||
216 | +
- ns <- NS(id)+ #' ), |
||
40 | -1x | +||
217 | +
- object$ui(ns("mutate_inner"))+ #' selectInput("ref2", "Select strata2 to compare", |
||
41 | +218 |
- },+ #' choices = c("A", "B", "C"), |
|
42 | -13x | +||
219 | +
- server = function(id) {+ #' selected = "B" |
||
43 | -11x | +||
220 | +
- moduleServer(id, function(input, output, session) {+ #' ), |
||
44 | -11x | +||
221 | +
- teal_data_rv <- object$server("mutate_inner")+ #' verbatimTextOutput("summary") |
||
45 | +222 |
-
+ #' ) |
|
46 | -11x | +||
223 | +
- if (!is.reactive(teal_data_rv)) {+ #' |
||
47 | -1x | +||
224 | +
- stop("The `teal_data_module` must return a reactive expression.", call. = FALSE)+ #' server <- function(input, output) { |
||
48 | +225 |
- }+ #' output$summary <- renderText({ |
|
49 | +226 |
-
+ #' sample_1 <- data$id[data$strata == input$ref1] |
|
50 | -10x | +||
227 | +
- td <- eventReactive(teal_data_rv(),+ #' sample_2 <- data$id[data$strata == input$ref2] |
||
51 | +228 |
- {+ #' |
|
52 | -10x | +||
229 | +
- if (inherits(teal_data_rv(), c("teal_data", "qenv.error"))) {+ #' validate_no_intersection( |
||
53 | -6x | +||
230 | +
- eval_code(teal_data_rv(), code)+ #' sample_1, sample_2, |
||
54 | +231 |
- } else {+ #' "subjects within strata1 and strata2 cannot overlap" |
|
55 | -4x | +||
232 | +
- teal_data_rv()+ #' ) |
||
56 | +233 |
- }+ #' paste0( |
|
57 | +234 |
- },+ #' "Number of subject in: reference treatment=", length(sample_1), |
|
58 | -10x | +||
235 | +
- ignoreNULL = FALSE+ #' " comparions treatment=", length(sample_2) |
||
59 | +236 |
- )+ #' ) |
|
60 | -10x | +||
237 | +
- td+ #' }) |
||
61 | +238 |
- })+ #' } |
|
62 | +239 |
- }+ #' if (interactive()) { |
|
63 | +240 |
- )+ #' shinyApp(ui, server) |
|
64 | +241 |
- })+ #' } |
|
65 | +242 |
-
+ #' |
|
66 | +243 |
- setMethod("eval_code", signature = c("teal_data_module", "language"), function(object, code) {+ validate_no_intersection <- function(x, y, msg) { |
|
67 | -1x | +||
244 | +! |
- eval_code(object, code = paste(lang2calls(code), collapse = "\n"))+ validate(need(length(intersect(x, y)) == 0, msg)) |
|
68 | +245 |
- })+ } |
|
69 | +246 | ||
70 | +247 |
- setMethod("eval_code", signature = c("teal_data_module", "expression"), function(object, code) {- |
- |
71 | -6x | -
- eval_code(object, code = paste(lang2calls(code), collapse = "\n"))+ |
|
72 | +248 |
- })+ #' Validates that dataset contains specific variable |
1 | +249 |
- #' Send input validation messages to output.+ #' |
|
2 | +250 |
- #'+ #' @description `r lifecycle::badge("stable")` |
|
3 | +251 |
- #' Captures messages from `InputValidator` objects and collates them+ #' @param data a data.frame |
|
4 | +252 |
- #' into one message passed to `validate`.+ #' @param varname name of variable in \code{data} |
|
5 | +253 |
- #'+ #' @param msg message to display if \code{data} does not include \code{varname} |
|
6 | +254 |
- #' `shiny::validate` is used to withhold rendering of an output element until+ #' |
|
7 | +255 |
- #' certain conditions are met and to print a validation message in place+ #' @details This function is a wrapper for `shiny::validate`. |
|
8 | +256 |
- #' of the output element.+ #' |
|
9 | +257 |
- #' `shinyvalidate::InputValidator` allows to validate input elements+ #' @export |
|
10 | +258 |
- #' and to display specific messages in their respective input widgets.+ #' |
|
11 | +259 |
- #' `validate_inputs` provides a hybrid solution.+ #' @examples |
|
12 | +260 |
- #' Given an `InputValidator` object, messages corresponding to inputs that fail validation+ #' data <- data.frame( |
|
13 | +261 |
- #' are extracted and placed in one validation message that is passed to a `validate`/`need` call.+ #' one = rep("a", length.out = 20), |
|
14 | +262 |
- #' This way the input `validator` messages are repeated in the output.+ #' two = rep(c("a", "b"), length.out = 20) |
|
15 | +263 |
- #'+ #' ) |
|
16 | +264 |
- #' The `...` argument accepts any number of `InputValidator` objects+ #' ui <- fluidPage( |
|
17 | +265 |
- #' or a nested list of such objects.+ #' selectInput( |
|
18 | +266 |
- #' If `validators` are passed directly, all their messages are printed together+ #' "var", |
|
19 | +267 |
- #' under one (optional) header message specified by `header`. If a list is passed,+ #' "Select variable", |
|
20 | +268 |
- #' messages are grouped by `validator`. The list's names are used as headers+ #' choices = c("one", "two", "three", "four"), |
|
21 | +269 |
- #' for their respective message groups.+ #' selected = "one" |
|
22 | +270 |
- #' If neither of the nested list elements is named, a header message is taken from `header`.+ #' ), |
|
23 | +271 |
- #'+ #' verbatimTextOutput("summary") |
|
24 | +272 |
- #' @param ... either any number of `InputValidator` objects+ #' ) |
|
25 | +273 |
- #' or an optionally named, possibly nested `list` of `InputValidator`+ #' |
|
26 | +274 |
- #' objects, see `Details`+ #' server <- function(input, output) { |
|
27 | +275 |
- #' @param header `character(1)` generic validation message; set to NULL to omit+ #' output$summary <- renderText({ |
|
28 | +276 |
- #'+ #' validate_has_variable(data, input$var) |
|
29 | +277 |
- #' @return+ #' paste0("Selected treatment variables: ", paste(input$var, collapse = ", ")) |
|
30 | +278 |
- #' Returns NULL if the final validation call passes and a `shiny.silent.error` if it fails.+ #' }) |
|
31 | +279 |
- #'+ #' } |
|
32 | +280 |
- #' @seealso [`shinyvalidate::InputValidator`], [`shiny::validate`]+ #' if (interactive()) { |
|
33 | +281 |
- #'+ #' shinyApp(ui, server) |
|
34 | +282 |
- #' @examples+ #' } |
|
35 | +283 |
- #' library(shiny)+ validate_has_variable <- function(data, varname, msg) { |
|
36 | -+ | ||
284 | +! |
- #' library(shinyvalidate)+ if (length(varname) != 0) { |
|
37 | -+ | ||
285 | +! |
- #'+ has_vars <- varname %in% names(data) |
|
38 | +286 |
- #' ui <- fluidPage(+ |
|
39 | -+ | ||
287 | +! |
- #' selectInput("method", "validation method", c("sequential", "combined", "grouped")),+ if (!all(has_vars)) { |
|
40 | -+ | ||
288 | +! |
- #' sidebarLayout(+ if (missing(msg)) { |
|
41 | -+ | ||
289 | +! |
- #' sidebarPanel(+ msg <- sprintf( |
|
42 | -+ | ||
290 | +! |
- #' selectInput("letter", "select a letter:", c(letters[1:3], LETTERS[4:6])),+ "%s does not have the required variables: %s.", |
|
43 | -+ | ||
291 | +! |
- #' selectInput("number", "select a number:", 1:6),+ deparse(substitute(data)), |
|
44 | -+ | ||
292 | +! |
- #' br(),+ toString(varname[!has_vars]) |
|
45 | +293 |
- #' selectInput("color", "select a color:",+ ) |
|
46 | +294 |
- #' c("black", "indianred2", "springgreen2", "cornflowerblue"),+ } |
|
47 | -+ | ||
295 | +! |
- #' multiple = TRUE+ validate(need(FALSE, msg)) |
|
48 | +296 |
- #' ),+ } |
|
49 | +297 |
- #' sliderInput("size", "select point size:",+ } |
|
50 | +298 |
- #' min = 0.1, max = 4, value = 0.25+ } |
|
51 | +299 |
- #' )+ |
|
52 | +300 |
- #' ),+ #' Validate that variables has expected number of levels |
|
53 | +301 |
- #' mainPanel(plotOutput("plot"))+ #' |
|
54 | +302 |
- #' )+ #' @description `r lifecycle::badge("stable")` |
|
55 | +303 |
- #' )+ #' @param x variable name. If \code{x} is not a factor, the unique values |
|
56 | +304 |
- #'+ #' are treated as levels. |
|
57 | +305 |
- #' server <- function(input, output) {+ #' @param min_levels cutoff for minimum number of levels of \code{x} |
|
58 | +306 |
- #' # set up input validation+ #' @param max_levels cutoff for maximum number of levels of \code{x} |
|
59 | +307 |
- #' iv <- InputValidator$new()+ #' @param var_name name of variable being validated for use in |
|
60 | +308 |
- #' iv$add_rule("letter", sv_in_set(LETTERS, "choose a capital letter"))+ #' validation message |
|
61 | +309 |
- #' iv$add_rule("number", ~ if (as.integer(.) %% 2L == 1L) "choose an even number")+ #' |
|
62 | +310 |
- #' iv$enable()+ #' @details If the number of levels of \code{x} is less than \code{min_levels} |
|
63 | +311 |
- #' # more input validation+ #' or greater than \code{max_levels} the validation will fail. |
|
64 | +312 |
- #' iv_par <- InputValidator$new()+ #' This function is a wrapper for `shiny::validate`. |
|
65 | +313 |
- #' iv_par$add_rule("color", sv_required(message = "choose a color"))+ #' |
|
66 | +314 |
- #' iv_par$add_rule("color", ~ if (length(.) > 1L) "choose only one color")+ #' @export |
|
67 | +315 |
- #' iv_par$add_rule(+ #' @examples |
|
68 | +316 |
- #' "size",+ #' data <- data.frame( |
|
69 | +317 |
- #' sv_between(+ #' one = rep("a", length.out = 20), |
|
70 | +318 |
- #' left = 0.5, right = 3,+ #' two = rep(c("a", "b"), length.out = 20), |
|
71 | +319 |
- #' message_fmt = "choose a value between {left} and {right}"+ #' three = rep(c("a", "b", "c"), length.out = 20), |
|
72 | +320 |
- #' )+ #' four = rep(c("a", "b", "c", "d"), length.out = 20), |
|
73 | +321 |
- #' )+ #' stringsAsFactors = TRUE |
|
74 | +322 |
- #' iv_par$enable()+ #' ) |
|
75 | +323 |
- #'+ #' ui <- fluidPage( |
|
76 | +324 |
- #' output$plot <- renderPlot({+ #' selectInput( |
|
77 | +325 |
- #' # validate output+ #' "var", |
|
78 | +326 |
- #' switch(input[["method"]],+ #' "Select variable", |
|
79 | +327 |
- #' "sequential" = {+ #' choices = c("one", "two", "three", "four"), |
|
80 | +328 |
- #' validate_inputs(iv)+ #' selected = "one" |
|
81 | +329 |
- #' validate_inputs(iv_par, header = "Set proper graphical parameters")+ #' ), |
|
82 | +330 |
- #' },+ #' verbatimTextOutput("summary") |
|
83 | +331 |
- #' "combined" = validate_inputs(iv, iv_par),+ #' ) |
|
84 | +332 |
- #' "grouped" = validate_inputs(list(+ #' |
|
85 | +333 |
- #' "Some inputs require attention" = iv,+ #' server <- function(input, output) { |
|
86 | +334 |
- #' "Set proper graphical parameters" = iv_par+ #' output$summary <- renderText({ |
|
87 | +335 |
- #' ))+ #' validate_n_levels(data[[input$var]], min_levels = 2, max_levels = 15, var_name = input$var) |
|
88 | +336 |
- #' )+ #' paste0( |
|
89 | +337 |
- #'+ #' "Levels of selected treatment variable: ", |
|
90 | +338 |
- #' plot(eruptions ~ waiting, faithful,+ #' paste(levels(data[[input$var]]), |
|
91 | +339 |
- #' las = 1, pch = 16,+ #' collapse = ", " |
|
92 | +340 |
- #' col = input[["color"]], cex = input[["size"]]+ #' ) |
|
93 | +341 |
#' ) |
|
94 | +342 |
#' }) |
|
95 | +343 |
#' } |
|
96 | -- |
- #'- |
- |
97 | +344 |
#' if (interactive()) { |
|
98 | +345 |
#' shinyApp(ui, server) |
|
99 | +346 |
#' } |
|
100 | +347 |
- #'+ validate_n_levels <- function(x, min_levels = 1, max_levels = 12, var_name) { |
|
101 | -+ | ||
348 | +! |
- #' @export+ x_levels <- if (is.factor(x)) { |
|
102 | -+ | ||
349 | +! |
- #'+ levels(x) |
|
103 | +350 |
- validate_inputs <- function(..., header = "Some inputs require attention") {+ } else { |
|
104 | -36x | +||
351 | +! |
- dots <- list(...)+ unique(x) |
|
105 | -2x | +||
352 | +
- if (!is_validators(dots)) stop("validate_inputs accepts validators or a list thereof")+ } |
||
106 | +353 | ||
107 | -34x | +||
354 | +! |
- messages <- extract_validator(dots, header)+ if (!is.null(min_levels) && !(is.null(max_levels))) { |
|
108 | -34x | +||
355 | +! |
- failings <- if (!any_names(dots)) {+ validate(need( |
|
109 | -29x | +||
356 | +! |
- add_header(messages, header)+ length(x_levels) >= min_levels && length(x_levels) <= max_levels, |
|
110 | -+ | ||
357 | +! |
- } else {+ sprintf( |
|
111 | -5x | +||
358 | +! |
- unlist(messages)+ "%s variable needs minimum %s level(s) and maximum %s level(s).", |
|
112 | -+ | ||
359 | +! |
- }+ var_name, min_levels, max_levels |
|
113 | +360 | - - | -|
114 | -34x | -
- shiny::validate(shiny::need(is.null(failings), failings))+ ) |
|
115 | +361 |
- }+ )) |
|
116 | -+ | ||
362 | +! |
-
+ } else if (!is.null(min_levels)) { |
|
117 | -+ | ||
363 | +! |
- ### internal functions+ validate(need( |
|
118 | -+ | ||
364 | +! |
-
+ length(x_levels) >= min_levels, |
|
119 | -+ | ||
365 | +! |
- #' @keywords internal+ sprintf("%s variable needs minimum %s levels(s)", var_name, min_levels) |
|
120 | +366 |
- # recursive object type test+ )) |
|
121 | -+ | ||
367 | +! |
- # returns logical of length 1+ } else if (!is.null(max_levels)) { |
|
122 | -+ | ||
368 | +! |
- is_validators <- function(x) {+ validate(need( |
|
123 | -118x | +||
369 | +! |
- all(if (is.list(x)) unlist(lapply(x, is_validators)) else inherits(x, "InputValidator"))+ length(x_levels) <= max_levels, |
|
124 | -+ | ||
370 | +! |
- }+ sprintf("%s variable needs maximum %s level(s)", var_name, max_levels) |
|
125 | +371 |
-
+ )) |
|
126 | +372 |
- #' @keywords internal+ } |
|
127 | +373 |
- # test if an InputValidator object is enabled+ } |
128 | +1 |
- # returns logical of length 1+ setOldClass("teal_data_module") |
||
129 | +2 |
- # official method requested at https://github.com/rstudio/shinyvalidate/issues/64+ |
||
130 | +3 |
- validator_enabled <- function(x) {+ #' Evaluate Code on `teal_data_module` |
||
131 | -49x | +|||
4 | +
- x$.__enclos_env__$private$enabled+ #' |
|||
132 | +5 |
- }+ #' @details |
||
133 | +6 |
-
+ #' `eval_code` evaluates given code in the environment of the `teal_data` object created by the `teal_data_module`. |
||
134 | +7 |
- #' @keywords internal+ #' The code is added to the `@code` slot of the `teal_data`. |
||
135 | +8 |
- # recursively extract messages from validator list+ #' |
||
136 | +9 |
- # returns character vector or a list of character vectors, possibly nested and named+ #' @param object (`teal_data_module`) |
||
137 | +10 |
- extract_validator <- function(iv, header) {+ #' @inheritParams teal.code::eval_code |
||
138 | -113x | +|||
11 | +
- if (inherits(iv, "InputValidator")) {+ #' |
|||
139 | -49x | +|||
12 | +
- add_header(gather_messages(iv), header)+ #' @return |
|||
140 | +13 |
- } else {+ #' `eval_code` returns a `teal_data_module` object with a delayed evaluation of `code` when the module is run. |
||
141 | -58x | +|||
14 | +
- if (is.null(names(iv))) names(iv) <- rep("", length(iv))+ #' |
|||
142 | -64x | +|||
15 | +
- mapply(extract_validator, iv = iv, header = names(iv), SIMPLIFY = FALSE)+ #' @examples |
|||
143 | +16 |
- }+ #' tdm <- teal_data_module( |
||
144 | +17 |
- }+ #' ui = function(id) div(id = shiny::NS(id)("div_id")), |
||
145 | +18 |
-
+ #' server = function(id) { |
||
146 | +19 |
- #' @keywords internal+ #' shiny::moduleServer(id, function(input, output, session) { |
||
147 | +20 |
- # collate failing messages from validator+ #' shiny::reactive(teal_data(IRIS = iris)) |
||
148 | +21 |
- # returns list+ #' }) |
||
149 | +22 |
- gather_messages <- function(iv) {+ #' } |
||
150 | -49x | +|||
23 | +
- if (validator_enabled(iv)) {+ #' ) |
|||
151 | -46x | +|||
24 | +
- status <- iv$validate()+ #' eval_code(tdm, "IRIS <- subset(IRIS, Species == 'virginica')") |
|||
152 | -46x | +|||
25 | +
- failing_inputs <- Filter(Negate(is.null), status)+ #' |
|||
153 | -46x | +|||
26 | +
- unique(lapply(failing_inputs, function(x) x[["message"]]))+ #' @include teal_data_module.R |
|||
154 | +27 |
- } else {+ #' @name eval_code |
||
155 | -3x | +|||
28 | +
- logger::log_warn("Validator is disabled and will be omitted.")+ #' @rdname teal_data_module |
|||
156 | -3x | +|||
29 | +
- list()+ #' @aliases eval_code,teal_data_module,character-method |
|||
157 | +30 |
- }+ #' @aliases eval_code,teal_data_module,language-method |
||
158 | +31 |
- }+ #' @aliases eval_code,teal_data_module,expression-method |
||
159 | +32 |
-
+ #' |
||
160 | +33 |
- #' @keywords internal+ #' @importFrom methods setMethod |
||
161 | +34 |
- # add optional header to failing messages+ #' @importMethodsFrom teal.code eval_code |
||
162 | +35 |
- add_header <- function(messages, header = "") {+ #' |
||
163 | -78x | +|||
36 | +
- ans <- unlist(messages)+ setMethod("eval_code", signature = c("teal_data_module", "character"), function(object, code) { |
|||
164 | -78x | +37 | +13x |
- if (length(ans) != 0L && isTRUE(nchar(header) > 0L)) {+ teal_data_module( |
165 | -31x | +38 | +13x |
- ans <- c(paste0(header, "\n"), ans, "\n")+ ui = function(id) { |
166 | -+ | |||
39 | +1x |
- }+ ns <- NS(id) |
||
167 | -78x | +40 | +1x |
- ans+ object$ui(ns("mutate_inner")) |
168 | +41 |
- }+ }, |
||
169 | -+ | |||
42 | +13x |
-
+ server = function(id) { |
||
170 | -+ | |||
43 | +11x |
- #' @keywords internal+ moduleServer(id, function(input, output, session) { |
||
171 | -+ | |||
44 | +11x |
- # recursively check if the object contains a named list+ teal_data_rv <- object$server("mutate_inner") |
||
172 | +45 |
- any_names <- function(x) {+ |
||
173 | -103x | +46 | +11x |
- any(+ if (!is.reactive(teal_data_rv)) { |
174 | -103x | +47 | +1x |
- if (is.list(x)) {+ stop("The `teal_data_module` must return a reactive expression.", call. = FALSE) |
175 | -58x | +|||
48 | +
- if (!is.null(names(x)) && any(names(x) != "")) TRUE else unlist(lapply(x, any_names))+ } |
|||
176 | +49 |
- } else {+ |
||
177 | -40x | +50 | +10x |
- FALSE+ td <- eventReactive(teal_data_rv(), |
178 | +51 |
- }+ {+ |
+ ||
52 | +10x | +
+ if (inherits(teal_data_rv(), c("teal_data", "qenv.error"))) { |
||
179 | -+ | |||
53 | +6x |
- )+ eval_code(teal_data_rv(), code) |
||
180 | +54 |
- }+ } else { |
1 | -+ | |||
55 | +4x |
- .onLoad <- function(libname, pkgname) { # nolint+ teal_data_rv() |
||
2 | +56 |
- # adapted from https://github.com/r-lib/devtools/blob/master/R/zzz.R- |
- ||
3 | -! | -
- teal_default_options <- list(teal.show_js_log = FALSE)+ } |
||
4 | +57 |
-
+ }, |
||
5 | -! | +|||
58 | +10x |
- op <- options()+ ignoreNULL = FALSE |
||
6 | -! | +|||
59 | +
- toset <- !(names(teal_default_options) %in% names(op))+ ) |
|||
7 | -! | +|||
60 | +10x |
- if (any(toset)) options(teal_default_options[toset])+ td |
||
8 | +61 |
-
+ }) |
||
9 | -! | +|||
62 | +
- options("shiny.sanitize.errors" = FALSE)+ } |
|||
10 | +63 |
-
+ ) |
||
11 | +64 |
- # Set up the teal logger instance+ }) |
||
12 | -! | +|||
65 | +
- teal.logger::register_logger("teal")+ |
|||
13 | +66 |
-
+ setMethod("eval_code", signature = c("teal_data_module", "language"), function(object, code) { |
||
14 | -! | +|||
67 | +1x |
- invisible()+ eval_code(object, code = paste(lang2calls(code), collapse = "\n")) |
||
15 | +68 |
- }+ }) |
||
16 | +69 | |||
17 | +70 |
- .onAttach <- function(libname, pkgname) { # nolint- |
- ||
18 | -2x | -
- packageStartupMessage(+ setMethod("eval_code", signature = c("teal_data_module", "expression"), function(object, code) { |
||
19 | -2x | +71 | +6x |
- "\nYou are using teal version ",+ eval_code(object, code = paste(lang2calls(code), collapse = "\n")) |
20 | +72 |
- # `system.file` uses the `shim` of `system.file` by `teal`+ }) |
21 | +1 |
- # we avoid `desc` dependency here to get the version- |
- |
22 | -2x | -
- read.dcf(system.file("DESCRIPTION", package = "teal"))[, "Version"]+ #' Evaluate Expression on `teal_data_module` |
|
23 | +2 |
- )+ #' |
|
24 | +3 |
- }+ #' @details |
|
25 | +4 |
-
+ #' `within` is a convenience function for evaluating inline code inside the environment of a `teal_data_module`. |
|
26 | +5 |
- # This one is here because setdiff_teal_slice should not be exported from teal.slice.+ #' |
|
27 | +6 |
- setdiff_teal_slices <- getFromNamespace("setdiff_teal_slices", "teal.slice")+ #' @param data (`teal_data_module`) object |
|
28 | +7 |
- # This one is here because it is needed by c.teal_slices but we don't want it exported from teal.slice.+ #' @param expr (`expression`) to evaluate. Must be inline code. See |
|
29 | +8 |
- coalesce_r <- getFromNamespace("coalesce_r", "teal.slice")+ #' @param ... See `Details`. |
|
30 | +9 |
- # all *Block objects are private in teal.reporter+ #' |
|
31 | +10 |
- RcodeBlock <- getFromNamespace("RcodeBlock", "teal.reporter") # nolint+ #' @return |
|
32 | +11 |
-
+ #' `within` returns a `teal_data_module` object with a delayed evaluation of `expr` when the module is run. |
|
33 | +12 |
- # Use non-exported function(s) from teal.code+ #' |
|
34 | +13 |
- # This one is here because lang2calls should not be exported from teal.code+ #' @examples |
|
35 | +14 |
- lang2calls <- getFromNamespace("lang2calls", "teal.code")+ #' tdm <- teal_data_module( |
1 | +15 |
- #' Landing Popup Module+ #' ui = function(id) div(id = shiny::NS(id)("div_id")), |
|
2 | +16 |
- #'+ #' server = function(id) { |
|
3 | +17 |
- #' @description Creates a landing welcome popup for `teal` applications.+ #' shiny::moduleServer(id, function(input, output, session) { |
|
4 | +18 |
- #'+ #' shiny::reactive(teal_data(IRIS = iris)) |
|
5 | +19 |
- #' This module is used to display a popup dialog when the application starts.+ #' }) |
|
6 | +20 |
- #' The dialog blocks the access to the application and must be closed with a button before the application is viewed.+ #' } |
|
7 | +21 |
- #'+ #' ) |
|
8 | +22 |
- #' @param label `character(1)` the label of the module.+ #' within(tdm, IRIS <- subset(IRIS, Species == "virginica")) |
|
9 | +23 |
- #' @param title `character(1)` the text to be displayed as a title of the popup.+ #' |
|
10 | +24 |
- #' @param content The content of the popup. Passed to `...` of `shiny::modalDialog`. Can be a `character`+ #' @include teal_data_module.R |
|
11 | +25 |
- #' or a list of `shiny.tag`s. See examples.+ #' @name within |
|
12 | +26 |
- #' @param buttons `shiny.tag` or a list of tags (`tagList`). Typically a `modalButton` or `actionButton`. See examples.+ #' @rdname teal_data_module |
|
13 | +27 |
#' |
|
14 | +28 |
- #' @return A `teal_module` (extended with `teal_landing_module` class) to be used in `teal` applications.+ #' @export |
|
15 | +29 |
#' |
|
16 | +30 |
- #' @examples+ within.teal_data_module <- function(data, expr, ...) { |
|
17 | -+ | ||
31 | +6x |
- #' app1 <- teal::init(+ expr <- substitute(expr) |
|
18 | -+ | ||
32 | +6x |
- #' data = teal_data(iris = iris),+ extras <- list(...) |
|
19 | +33 |
- #' modules = teal::modules(+ |
|
20 | +34 |
- #' teal::landing_popup_module(+ # Add braces for consistency. |
|
21 | -+ | ||
35 | +6x |
- #' content = "A place for the welcome message or a disclaimer statement.",+ if (!identical(as.list(expr)[[1L]], as.symbol("{"))) { |
|
22 | -+ | ||
36 | +6x |
- #' buttons = modalButton("Proceed")+ expr <- call("{", expr) |
|
23 | +37 |
- #' ),+ } |
|
24 | +38 |
- #' example_module()+ |
|
25 | -+ | ||
39 | +6x |
- #' )+ calls <- as.list(expr)[-1] |
|
26 | +40 |
- #' )+ |
|
27 | +41 |
- #' if (interactive()) {+ # Inject extra values into expressions.+ |
+ |
42 | +6x | +
+ calls <- lapply(calls, function(x) do.call(substitute, list(x, env = extras))) |
|
28 | +43 |
- #' shinyApp(app1$ui, app1$server)+ + |
+ |
44 | +6x | +
+ eval_code(object = data, code = as.expression(calls)) |
|
29 | +45 |
- #' }+ } |
30 | +1 |
- #'+ #' Data Module for `teal` Applications |
|
31 | +2 |
- #' app2 <- teal::init(+ #' |
|
32 | +3 |
- #' data = teal_data(iris = iris),+ #' @description |
|
33 | +4 |
- #' modules = teal::modules(+ #' `r lifecycle::badge("experimental")` |
|
34 | +5 |
- #' teal::landing_popup_module(+ #' |
|
35 | +6 |
- #' title = "Welcome",+ #' Create a `teal_data_module` object and evaluate code on it with history tracking. |
|
36 | +7 |
- #' content = tags$b(+ #' |
|
37 | +8 |
- #' "A place for the welcome message or a disclaimer statement.",+ #' @details |
|
38 | +9 |
- #' style = "color: red;"+ #' `teal_data_module` creates a `shiny` module to supply or modify data in a `teal` application. |
|
39 | +10 |
- #' ),+ #' The module allows for running data pre-processing code (creation _and_ some modification) after the app starts. |
|
40 | +11 |
- #' buttons = tagList(+ #' The body of the server function will be run in the app rather than in the global environment. |
|
41 | +12 |
- #' modalButton("Proceed"),+ #' This means it will be run every time the app starts, so use sparingly.\cr |
|
42 | +13 |
- #' actionButton("read", "Read more",+ #' Pass this module instead of a `teal_data` object in a call to [init()]. |
|
43 | +14 |
- #' onclick = "window.open('http://google.com', '_blank')"+ #' Note that the server function must always return a `teal_data` object wrapped in a reactive expression.\cr |
|
44 | +15 |
- #' ),+ #' See vignette `vignette("data-as-shiny-module", package = "teal")` for more details. |
|
45 | +16 |
- #' actionButton("close", "Reject", onclick = "window.close()")+ #' |
|
46 | +17 |
- #' )+ #' @param ui (`function(id)`)\cr |
|
47 | +18 |
- #' ),+ #' `shiny` module `ui` function; must only take `id` argument |
|
48 | +19 |
- #' example_module()+ #' @param server (`function(id)`)\cr |
|
49 | +20 |
- #' )+ #' `shiny` module `ui` function; must only take `id` argument; |
|
50 | +21 |
- #' )+ #' must return reactive expression containing `teal_data` object |
|
51 | +22 |
#' |
|
52 | +23 |
- #' if (interactive()) {+ #' @return |
|
53 | +24 |
- #' shinyApp(app2$ui, app2$server)+ #' `teal_data_module` returns an object of class `teal_data_module`. |
|
54 | +25 |
- #' }+ #' |
|
55 | +26 |
- #'+ #' @examples |
|
56 | +27 |
- #' @export+ #' data <- teal_data_module( |
|
57 | +28 |
- landing_popup_module <- function(label = "Landing Popup",+ #' ui = function(id) { |
|
58 | +29 |
- title = NULL,+ #' ns <- NS(id) |
|
59 | +30 |
- content = NULL,+ #' actionButton(ns("submit"), label = "Load data") |
|
60 | +31 |
- buttons = modalButton("Accept")) {+ #' }, |
|
61 | -! | +||
32 | +
- checkmate::assert_string(label)+ #' server = function(id) { |
||
62 | -! | +||
33 | +
- checkmate::assert_string(title, null.ok = TRUE)+ #' moduleServer(id, function(input, output, session) { |
||
63 | -! | +||
34 | +
- checkmate::assert_multi_class(+ #' eventReactive(input$submit, { |
||
64 | -! | +||
35 | +
- content,+ #' data <- within( |
||
65 | -! | +||
36 | +
- classes = c("character", "shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE+ #' teal_data(), |
||
66 | +37 |
- )+ #' { |
|
67 | -! | +||
38 | +
- checkmate::assert_multi_class(buttons, classes = c("shiny.tag", "shiny.tag.list"))+ #' dataset1 <- iris |
||
68 | +39 |
-
+ #' dataset2 <- mtcars |
|
69 | -! | +||
40 | +
- logger::log_info("Initializing landing_popup_module")+ #' } |
||
70 | +41 |
-
+ #' ) |
|
71 | -! | +||
42 | +
- module <- module(+ #' datanames(data) <- c("dataset1", "dataset2") |
||
72 | -! | +||
43 | +
- label = label,+ #' |
||
73 | -! | +||
44 | +
- server = function(id) {+ #' data |
||
74 | -! | +||
45 | +
- moduleServer(id, function(input, output, session) {+ #' }) |
||
75 | -! | +||
46 | +
- showModal(+ #' }) |
||
76 | -! | +||
47 | +
- modalDialog(+ #' } |
||
77 | -! | +||
48 | +
- id = "landingpopup",+ #' ) |
||
78 | -! | +||
49 | +
- title = title,+ #' |
||
79 | -! | +||
50 | +
- content,+ #' @name teal_data_module |
||
80 | -! | +||
51 | +
- footer = buttons+ #' @rdname teal_data_module |
||
81 | +52 |
- )+ #' |
|
82 | +53 |
- )+ #' @seealso [`teal_data-class`], [`base::within()`], [`teal.code::within.qenv()`] |
|
83 | +54 |
- })+ #' |
|
84 | +55 |
- }+ #' @export |
|
85 | +56 |
- )+ teal_data_module <- function(ui, server) { |
|
86 | -! | +||
57 | +35x |
- class(module) <- c("teal_module_landing", class(module))+ checkmate::assert_function(ui, args = "id", nargs = 1) |
|
87 | -! | +||
58 | +34x |
- module+ checkmate::assert_function(server, args = "id", nargs = 1)+ |
+ |
59 | +33x | +
+ structure(+ |
+ |
60 | +33x | +
+ list(ui = ui, server = server),+ |
+ |
61 | +33x | +
+ class = "teal_data_module" |
|
88 | +62 | ++ |
+ )+ |
+
63 |
}@@ -30285,14 +30844,14 @@ teal coverage - 63.82% |
1 |
- #' Evaluate Expression on `teal_data_module`+ #' Landing Popup Module |
||
3 |
- #' @details+ #' @description Creates a landing welcome popup for `teal` applications. |
||
4 |
- #' `within` is a convenience function for evaluating inline code inside the environment of a `teal_data_module`.+ #' |
||
5 |
- #'+ #' This module is used to display a popup dialog when the application starts. |
||
6 |
- #' @param data (`teal_data_module`) object+ #' The dialog blocks the access to the application and must be closed with a button before the application is viewed. |
||
7 |
- #' @param expr (`expression`) to evaluate. Must be inline code. See+ #' |
||
8 |
- #' @param ... See `Details`.+ #' @param label `character(1)` the label of the module. |
||
9 |
- #'+ #' @param title `character(1)` the text to be displayed as a title of the popup. |
||
10 |
- #' @return+ #' @param content The content of the popup. Passed to `...` of `shiny::modalDialog`. Can be a `character` |
||
11 |
- #' `within` returns a `teal_data_module` object with a delayed evaluation of `expr` when the module is run.+ #' or a list of `shiny.tag`s. See examples. |
||
12 |
- #'+ #' @param buttons `shiny.tag` or a list of tags (`tagList`). Typically a `modalButton` or `actionButton`. See examples. |
||
13 |
- #' @examples+ #' |
||
14 |
- #' tdm <- teal_data_module(+ #' @return A `teal_module` (extended with `teal_landing_module` class) to be used in `teal` applications. |
||
15 |
- #' ui = function(id) div(id = shiny::NS(id)("div_id")),+ #' |
||
16 |
- #' server = function(id) {+ #' @examples |
||
17 |
- #' shiny::moduleServer(id, function(input, output, session) {+ #' app1 <- teal::init( |
||
18 |
- #' shiny::reactive(teal_data(IRIS = iris))+ #' data = teal_data(iris = iris), |
||
19 |
- #' })+ #' modules = teal::modules( |
||
20 |
- #' }+ #' teal::landing_popup_module( |
||
21 |
- #' )+ #' content = "A place for the welcome message or a disclaimer statement.", |
||
22 |
- #' within(tdm, IRIS <- subset(IRIS, Species == "virginica"))+ #' buttons = modalButton("Proceed") |
||
23 |
- #'+ #' ), |
||
24 |
- #' @include teal_data_module.R+ #' example_module() |
||
25 |
- #' @name within+ #' ) |
||
26 |
- #' @rdname teal_data_module+ #' ) |
||
27 |
- #'+ #' if (interactive()) { |
||
28 |
- #' @export+ #' shinyApp(app1$ui, app1$server) |
||
29 |
- #'+ #' } |
||
30 |
- within.teal_data_module <- function(data, expr, ...) {+ #' |
||
31 | -6x | +
- expr <- substitute(expr)+ #' app2 <- teal::init( |
|
32 | -6x | +
- extras <- list(...)+ #' data = teal_data(iris = iris), |
|
33 |
-
+ #' modules = teal::modules( |
||
34 |
- # Add braces for consistency.+ #' teal::landing_popup_module( |
||
35 | -6x | +
- if (!identical(as.list(expr)[[1L]], as.symbol("{"))) {+ #' title = "Welcome", |
|
36 | -6x | +
- expr <- call("{", expr)+ #' content = tags$b( |
|
37 |
- }+ #' "A place for the welcome message or a disclaimer statement.", |
||
38 |
-
+ #' style = "color: red;" |
||
39 | -6x | +
- calls <- as.list(expr)[-1]+ #' ), |
|
40 |
-
+ #' buttons = tagList( |
||
41 |
- # Inject extra values into expressions.+ #' modalButton("Proceed"), |
||
42 | -6x | +
- calls <- lapply(calls, function(x) do.call(substitute, list(x, env = extras)))+ #' actionButton("read", "Read more", |
|
43 |
-
+ #' onclick = "window.open('http://google.com', '_blank')" |
||
44 | -6x | +
- eval_code(object = data, code = as.expression(calls))+ #' ), |
|
45 |
- }+ #' actionButton("close", "Reject", onclick = "window.close()") |
1 | +46 |
- #' Show R Code Modal+ #' ) |
|
2 | +47 |
- #'+ #' ), |
|
3 | +48 |
- #' @export+ #' example_module() |
|
4 | +49 |
- #' @description `r lifecycle::badge("stable")`+ #' ) |
|
5 | +50 |
- #' Use the [shiny::showModal()] function to show the R code inside.+ #' ) |
|
6 | +51 |
#' |
|
7 | +52 |
- #' @param title (`character(1)`)\cr+ #' if (interactive()) { |
|
8 | +53 |
- #' Title of the modal, displayed in the first comment of the R-code.+ #' shinyApp(app2$ui, app2$server) |
|
9 | +54 |
- #' @param rcode (`character`)\cr+ #' } |
|
10 | +55 |
- #' vector with R code to show inside the modal.+ #' |
|
11 | +56 |
- #' @param session (`ShinySession` optional)\cr+ #' @export |
|
12 | +57 |
- #' `shiny` Session object, if missing then [shiny::getDefaultReactiveDomain()] is used.+ landing_popup_module <- function(label = "Landing Popup", |
|
13 | +58 |
- #'+ title = NULL, |
|
14 | +59 |
- #' @references [shiny::showModal()]+ content = NULL, |
|
15 | +60 |
- show_rcode_modal <- function(title = NULL, rcode, session = getDefaultReactiveDomain()) {+ buttons = modalButton("Accept")) { |
|
16 | +61 | ! |
- rcode <- paste(rcode, collapse = "\n")+ checkmate::assert_string(label) |
17 | -+ | ||
62 | +! |
-
+ checkmate::assert_string(title, null.ok = TRUE) |
|
18 | +63 | ! |
- ns <- session$ns+ checkmate::assert_multi_class( |
19 | +64 | ! |
- showModal(modalDialog(+ content, |
20 | +65 | ! |
- tagList(+ classes = c("character", "shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE |
21 | -! | +||
66 | +
- tags$div(+ ) |
||
22 | +67 | ! |
- actionButton(ns("copyRCode"), "Copy to Clipboard", `data-clipboard-target` = paste0("#", ns("r_code"))),+ checkmate::assert_multi_class(buttons, classes = c("shiny.tag", "shiny.tag.list")) |
23 | -! | +||
68 | +
- modalButton("Dismiss"),+ |
||
24 | +69 | ! |
- style = "mb-4"+ logger::log_info("Initializing landing_popup_module") |
25 | +70 |
- ),+ |
|
26 | +71 | ! |
- tags$div(tags$pre(id = ns("r_code"), rcode)),+ module <- module( |
27 | -+ | ||
72 | +! |
- ),+ label = label, |
|
28 | +73 | ! |
- title = title,+ server = function(id) { |
29 | +74 | ! |
- footer = tagList(+ moduleServer(id, function(input, output, session) { |
30 | +75 | ! |
- actionButton(ns("copyRCode"), "Copy to Clipboard", `data-clipboard-target` = paste0("#", ns("r_code"))),+ showModal( |
31 | +76 | ! |
- modalButton("Dismiss")+ modalDialog( |
32 | -+ | ||
77 | +! |
- ),+ id = "landingpopup", |
|
33 | +78 | ! |
- size = "l",+ title = title, |
34 | +79 | ! |
- easyClose = TRUE+ content,+ |
+
80 | +! | +
+ footer = buttons |
|
35 | +81 |
- ))+ ) |
|
36 | +82 |
-
+ )+ |
+ |
83 | ++ |
+ })+ |
+ |
84 | ++ |
+ }+ |
+ |
85 | ++ |
+ ) |
|
37 | +86 | ! |
- return(NULL)+ class(module) <- c("teal_module_landing", class(module))+ |
+
87 | +! | +
+ module |
|
38 | +88 |
} |