diff --git a/main/coverage-report/index.html b/main/coverage-report/index.html index f9c7e418a0..dd0a158cf3 100644 --- a/main/coverage-report/index.html +++ b/main/coverage-report/index.html @@ -106,19 +106,19 @@
1 |
- #' Filter state snapshot management.+ #' Validate that dataset has a minimum number of observations |
||
3 |
- #' Capture and restore snapshots of the global (app) filter state.+ #' `r lifecycle::badge("stable")` |
||
5 |
- #' This module introduces snapshots: stored descriptions of the filter state of the entire application.+ #' This function is a wrapper for `shiny::validate`. |
||
6 |
- #' Snapshots allow the user to save the current filter state of the application for later use in the session,+ #' |
||
7 |
- #' as well as to save it to file in order to share it with an app developer or other users,+ #' @param x (`data.frame`) |
||
8 |
- #' who in turn can upload it to their own session.+ #' @param min_nrow (`numeric(1)`) Minimum allowed number of rows in `x`. |
||
9 |
- #'+ #' @param complete (`logical(1)`) Flag specifying whether to check only complete cases. Defaults to `FALSE`. |
||
10 |
- #' The snapshot manager is accessed through the filter manager, with the cog icon in the top right corner.+ #' @param allow_inf (`logical(1)`) Flag specifying whether to allow infinite values. Defaults to `TRUE`. |
||
11 |
- #' At the beginning of a session it presents three icons: a camera, an upload, and an circular arrow.+ #' @param msg (`character(1)`) Additional message to display alongside the default message. |
||
12 |
- #' Clicking the camera captures a snapshot, clicking the upload adds a snapshot from a file+ #' |
||
13 |
- #' and applies the filter states therein, and clicking the arrow resets initial application state.+ #' @export |
||
14 |
- #' As snapshots are added, they will show up as rows in a table and each will have a select button and a save button.+ #' |
||
15 |
- #'+ #' @examples |
||
16 |
- #' @section Server logic:+ #' library(teal) |
||
17 |
- #' Snapshots are basically `teal_slices` objects, however, since each module is served by a separate instance+ #' ui <- fluidPage( |
||
18 |
- #' of `FilteredData` and these objects require shared state, `teal_slice` is a `reactiveVal` so `teal_slices`+ #' sliderInput("len", "Max Length of Sepal", |
||
19 |
- #' cannot be stored as is. Therefore, `teal_slices` are reversibly converted to a list of lists representation+ #' min = 4.3, max = 7.9, value = 5 |
||
20 |
- #' (attributes are maintained).+ #' ), |
||
21 |
- #'+ #' plotOutput("plot") |
||
22 |
- #' Snapshots are stored in a `reactiveVal` as a named list.+ #' ) |
||
23 |
- #' The first snapshot is the initial state of the application and the user can add a snapshot whenever they see fit.+ #' |
||
24 |
- #'+ #' server <- function(input, output) { |
||
25 |
- #' For every snapshot except the initial one, a piece of UI is generated that contains+ #' output$plot <- renderPlot({ |
||
26 |
- #' the snapshot name, a select button to restore that snapshot, and a save button to save it to a file.+ #' iris_df <- iris[iris$Sepal.Length <= input$len, ] |
||
27 |
- #' The initial snapshot is restored by a separate "reset" button.+ #' validate_has_data( |
||
28 |
- #' It cannot be saved directly but a user is welcome to capture the initial state as a snapshot and save that.+ #' iris_df, |
||
29 |
- #'+ #' min_nrow = 10, |
||
30 |
- #' @section Snapshot mechanics:+ #' complete = FALSE, |
||
31 |
- #' When a snapshot is captured, the user is prompted to name it.+ #' msg = "Please adjust Max Length of Sepal" |
||
32 |
- #' Names are displayed as is but since they are used to create button ids,+ #' ) |
||
33 |
- #' under the hood they are converted to syntactically valid strings.+ #' |
||
34 |
- #' New snapshot names are validated so that their valid versions are unique.+ #' hist(iris_df$Sepal.Length, breaks = 5) |
||
35 |
- #' Leading and trailing white space is trimmed.+ #' }) |
||
36 |
- #'+ #' } |
||
37 |
- #' The module can read the global state of the application from `slices_global` and `mapping_matrix`.+ #' if (interactive()) { |
||
38 |
- #' The former provides a list of all existing `teal_slice`s and the latter says which slice is active in which module.+ #' shinyApp(ui, server) |
||
39 |
- #' Once a name has been accepted, `slices_global` is converted to a list of lists - a snapshot.+ #' } |
||
40 |
- #' The snapshot contains the `mapping` attribute of the initial application state+ #' |
||
41 |
- #' (or one that has been restored), which may not reflect the current one,+ validate_has_data <- function(x, |
||
42 |
- #' so `mapping_matrix` is transformed to obtain the current mapping, i.e. a list that,+ min_nrow = NULL, |
||
43 |
- #' when passed to the `mapping` argument of [teal_slices()], would result in the current mapping.+ complete = FALSE, |
||
44 |
- #' This is substituted as the snapshot's `mapping` attribute and the snapshot is added to the snapshot list.+ allow_inf = TRUE, |
||
45 |
- #'+ msg = NULL) { |
||
46 | -+ | 17x |
- #' To restore app state, a snapshot is retrieved from storage and rebuilt into a `teal_slices` object.+ checkmate::assert_string(msg, null.ok = TRUE) |
47 | -+ | 15x |
- #' Then state of all `FilteredData` objects (provided in `filtered_data_list`) is cleared+ checkmate::assert_data_frame(x) |
48 | -+ | 15x |
- #' and set anew according to the `mapping` attribute of the snapshot.+ if (!is.null(min_nrow)) { |
49 | -+ | 15x |
- #' The snapshot is then set as the current content of `slices_global`.+ if (complete) { |
50 | -+ | 5x |
- #'+ complete_index <- stats::complete.cases(x) |
51 | -+ | 5x |
- #' To save a snapshot, the snapshot is retrieved and reassembled just like for restoring,+ validate(need( |
52 | -+ | 5x |
- #' and then saved to file with [slices_store()].+ sum(complete_index) > 0 && nrow(x[complete_index, , drop = FALSE]) >= min_nrow, |
53 | -+ | 5x |
- #'+ paste(c(paste("Number of complete cases is less than:", min_nrow), msg), collapse = "\n") |
54 |
- #' When a snapshot is uploaded, it will first be added to storage just like a newly created one,+ )) |
||
55 |
- #' and then used to restore app state much like a snapshot taken from storage.+ } else { |
||
56 | -+ | 10x |
- #' Upon clicking the upload icon the user will be prompted for a file to upload+ validate(need( |
57 | -+ | 10x |
- #' and may choose to name the new snapshot. The name defaults to the name of the file (the extension is dropped)+ nrow(x) >= min_nrow, |
58 | -+ | 10x |
- #' and normal naming rules apply. Loading the file yields a `teal_slices` object,+ paste( |
59 | -+ | 10x |
- #' which is disassembled for storage and used directly for restoring app state.+ c(paste("Minimum number of records not met: >=", min_nrow, "records required."), msg), |
60 | -+ | 10x |
- #'+ collapse = "\n" |
61 |
- #' @section Transferring snapshots:+ ) |
||
62 |
- #' Snapshots uploaded from disk should only be used in the same application they come from,+ )) |
||
63 |
- #' _i.e._ an application that uses the same data and the same modules.+ } |
||
64 |
- #' To ensure this is the case, `init` stamps `teal_slices` with an app id that is stored in the `app_id` attribute of+ |
||
65 | -+ | 10x |
- #' a `teal_slices` object. When a snapshot is restored from file, its `app_id` is compared to that+ if (!allow_inf) { |
66 | -+ | 6x |
- #' of the current app state and only if the match is the snapshot admitted to the session.+ validate(need( |
67 | -+ | 6x |
- #'+ all(vapply(x, function(col) !is.numeric(col) || !any(is.infinite(col)), logical(1))), |
68 | -+ | 6x |
- #' @param id (`character(1)`) `shiny` module id+ "Dataframe contains Inf values which is not allowed." |
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 |
- #' @param mapping_matrix (`reactive`) that contains a `data.frame` representation+ } |
||
72 |
- #' of the mapping of filter state ids (rows) to modules labels (columns);+ } |
||
73 |
- #' all columns are `logical` vectors+ |
||
74 |
- #' @param filtered_data_list non-nested (named `list`) that contains `FilteredData` objects+ #' Validate that dataset has unique rows for key variables |
||
76 |
- #' @return Nothing is returned.+ #' `r lifecycle::badge("stable")` |
||
78 |
- #' @name snapshot_manager_module+ #' This function is a wrapper for `shiny::validate`. |
||
79 |
- #' @aliases snapshot snapshot_manager+ #' |
||
80 |
- #'+ #' @param x (`data.frame`) |
||
81 |
- #' @author Aleksander Chlebowski+ #' @param key (`character`) Vector of ID variables from `x` that identify unique records. |
||
83 |
- #' @rdname snapshot_manager_module+ #' @export |
||
84 |
- #' @keywords internal+ #' |
||
85 |
- #'+ #' @examples |
||
86 |
- snapshot_manager_ui <- function(id) {+ #' iris$id <- rep(1:50, times = 3) |
||
87 | -! | +
- ns <- NS(id)+ #' ui <- fluidPage( |
|
88 | -! | +
- div(+ #' selectInput( |
|
89 | -! | +
- class = "snapshot_manager_content",+ #' inputId = "species", |
|
90 | -! | +
- div(+ #' label = "Select species", |
|
91 | -! | +
- class = "snapshot_table_row",+ #' choices = c("setosa", "versicolor", "virginica"), |
|
92 | -! | +
- span(tags$b("Snapshot manager")),+ #' selected = "setosa", |
|
93 | -! | +
- actionLink(ns("snapshot_add"), label = NULL, icon = icon("camera"), title = "add snapshot"),+ #' multiple = TRUE |
|
94 | -! | +
- actionLink(ns("snapshot_load"), label = NULL, icon = icon("upload"), title = "upload snapshot"),+ #' ), |
|
95 | -! | +
- actionLink(ns("snapshot_reset"), label = NULL, icon = icon("undo"), title = "reset initial state"),+ #' plotOutput("plot") |
|
96 | -! | +
- NULL+ #' ) |
|
97 |
- ),+ #' server <- function(input, output) { |
||
98 | -! | +
- uiOutput(ns("snapshot_list"))+ #' output$plot <- renderPlot({ |
|
99 |
- )+ #' iris_f <- iris[iris$Species %in% input$species, ] |
||
100 |
- }+ #' validate_one_row_per_id(iris_f, key = c("id")) |
||
101 |
-
+ #' |
||
102 |
- #' @rdname snapshot_manager_module+ #' hist(iris_f$Sepal.Length, breaks = 5) |
||
103 |
- #' @keywords internal+ #' }) |
||
104 |
- #'+ #' } |
||
105 |
- snapshot_manager_srv <- function(id, slices_global, mapping_matrix, filtered_data_list) {+ #' if (interactive()) { |
||
106 | -6x | +
- checkmate::assert_character(id)+ #' shinyApp(ui, server) |
|
107 | -6x | +
- checkmate::assert_true(is.reactive(slices_global))+ #' } |
|
108 | -6x | +
- checkmate::assert_class(isolate(slices_global()), "teal_slices")+ #' |
|
109 | -6x | +
- checkmate::assert_true(is.reactive(mapping_matrix))+ validate_one_row_per_id <- function(x, key = c("USUBJID", "STUDYID")) { |
|
110 | -6x | +! |
- checkmate::assert_data_frame(isolate(mapping_matrix()), null.ok = TRUE)+ validate(need(!any(duplicated(x[key])), paste("Found more than one row per id."))) |
111 | -6x | +
- checkmate::assert_list(filtered_data_list, types = "FilteredData", any.missing = FALSE, names = "named")+ } |
|
113 | -6x | +
- moduleServer(id, function(input, output, session) {+ #' Validates that vector includes all expected values |
|
114 | -6x | +
- ns <- session$ns+ #' |
|
115 |
-
+ #' `r lifecycle::badge("stable")` |
||
116 |
- # Store global filter states ----+ #' |
||
117 | -6x | +
- filter <- isolate(slices_global())+ #' This function is a wrapper for `shiny::validate`. |
|
118 | -6x | +
- snapshot_history <- reactiveVal({+ #' |
|
119 | -6x | +
- list(+ #' @param x Vector of values to test. |
|
120 | -6x | +
- "Initial application state" = as.list(filter, recursive = TRUE)+ #' @param choices Vector to test against. |
|
121 |
- )+ #' @param msg (`character(1)`) Error message to display if some elements of `x` are not elements of `choices`. |
||
122 |
- })+ #' |
||
123 |
-
+ #' @export |
||
124 |
- # Snapshot current application state ----+ #' |
||
125 |
- # Name snaphsot.+ #' @examples |
||
126 | -6x | +
- observeEvent(input$snapshot_add, {+ #' ui <- fluidPage( |
|
127 | -! | +
- showModal(+ #' selectInput( |
|
128 | -! | +
- modalDialog(+ #' "species", |
|
129 | -! | +
- textInput(ns("snapshot_name"), "Name the snapshot", width = "100%", placeholder = "Meaningful, unique name"),+ #' "Select species", |
|
130 | -! | +
- footer = tagList(+ #' choices = c("setosa", "versicolor", "virginica", "unknown species"), |
|
131 | -! | +
- actionButton(ns("snapshot_name_accept"), "Accept", icon = icon("thumbs-up")),+ #' selected = "setosa", |
|
132 | -! | +
- modalButton(label = "Cancel", icon = icon("thumbs-down"))+ #' multiple = FALSE |
|
133 |
- ),+ #' ), |
||
134 | -! | +
- size = "s"+ #' verbatimTextOutput("summary") |
|
135 |
- )+ #' ) |
||
136 |
- )+ #' |
||
137 |
- })+ #' server <- function(input, output) { |
||
138 |
- # Store snaphsot.+ #' output$summary <- renderPrint({ |
||
139 | -6x | +
- observeEvent(input$snapshot_name_accept, {+ #' validate_in(input$species, iris$Species, "Species does not exist.") |
|
140 | -! | +
- snapshot_name <- trimws(input$snapshot_name)+ #' nrow(iris[iris$Species == input$species, ]) |
|
141 | -! | +
- if (identical(snapshot_name, "")) {+ #' }) |
|
142 | -! | +
- showNotification(+ #' } |
|
143 | -! | +
- "Please name the snapshot.",+ #' if (interactive()) { |
|
144 | -! | +
- type = "message"+ #' shinyApp(ui, server) |
|
145 |
- )+ #' } |
||
146 | -! | +
- updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name")+ #' |
|
147 | -! | +
- } else if (is.element(make.names(snapshot_name), make.names(names(snapshot_history())))) {+ validate_in <- function(x, choices, msg) { |
|
148 | ! |
- showNotification(+ validate(need(length(x) > 0 && length(choices) > 0 && all(x %in% choices), msg)) |
|
149 | -! | +
- "This name is in conflict with other snapshot names. Please choose a different one.",+ } |
|
150 | -! | +
- type = "message"+ |
|
151 |
- )+ #' Validates that vector has length greater than 0 |
||
152 | -! | +
- updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name")+ #' |
|
153 |
- } else {+ #' `r lifecycle::badge("stable")` |
||
154 | -! | +
- snapshot <- as.list(slices_global(), recursive = TRUE)+ #' |
|
155 | -! | +
- attr(snapshot, "mapping") <- matrix_to_mapping(mapping_matrix())+ #' This function is a wrapper for `shiny::validate`. |
|
156 | -! | +
- snapshot_update <- c(snapshot_history(), list(snapshot))+ #' |
|
157 | -! | +
- names(snapshot_update)[length(snapshot_update)] <- snapshot_name+ #' @param x vector |
|
158 | -! | +
- snapshot_history(snapshot_update)+ #' @param msg message to display |
|
159 | -! | +
- removeModal()+ #' |
|
160 |
- # Reopen filter manager modal by clicking button in the main application.+ #' @export |
||
161 | -! | +
- shinyjs::click(id = "teal-main_ui-filter_manager-show", asis = TRUE)+ #' |
|
162 |
- }+ #' @examples |
||
163 |
- })+ #' data <- data.frame( |
||
164 |
-
+ #' id = c(1:10, 11:20, 1:10), |
||
165 |
- # Upload a snapshot file ----+ #' strata = rep(c("A", "B"), each = 15) |
||
166 |
- # Select file.+ #' ) |
||
167 | -6x | +
- observeEvent(input$snapshot_load, {+ #' ui <- fluidPage( |
|
168 | -! | +
- showModal(+ #' selectInput("ref1", "Select strata1 to compare", |
|
169 | -! | +
- modalDialog(+ #' choices = c("A", "B", "C"), selected = "A" |
|
170 | -! | +
- fileInput(ns("snapshot_file"), "Choose snapshot file", accept = ".json", width = "100%"),+ #' ), |
|
171 | -! | +
- textInput(+ #' selectInput("ref2", "Select strata2 to compare", |
|
172 | -! | +
- ns("snapshot_name"),+ #' choices = c("A", "B", "C"), selected = "B" |
|
173 | -! | +
- "Name the snapshot (optional)",+ #' ), |
|
174 | -! | +
- width = "100%",+ #' verbatimTextOutput("arm_summary") |
|
175 | -! | +
- placeholder = "Meaningful, unique name"+ #' ) |
|
176 |
- ),+ #' |
||
177 | -! | +
- footer = tagList(+ #' server <- function(input, output) { |
|
178 | -! | +
- actionButton(ns("snaphot_file_accept"), "Accept", icon = icon("thumbs-up")),+ #' output$arm_summary <- renderText({ |
|
179 | -! | +
- modalButton(label = "Cancel", icon = icon("thumbs-down"))+ #' sample_1 <- data$id[data$strata == input$ref1] |
|
180 |
- )+ #' sample_2 <- data$id[data$strata == input$ref2] |
||
181 |
- )+ #' |
||
182 |
- )+ #' validate_has_elements(sample_1, "No subjects in strata1.") |
||
183 |
- })+ #' validate_has_elements(sample_2, "No subjects in strata2.") |
||
184 |
- # Store new snapshot to list and restore filter states.+ #' |
||
185 | -6x | +
- observeEvent(input$snaphot_file_accept, {+ #' paste0( |
|
186 | -! | +
- snapshot_name <- trimws(input$snapshot_name)+ #' "Number of samples in: strata1=", length(sample_1), |
|
187 | -! | +
- if (identical(snapshot_name, "")) {+ #' " comparions strata2=", length(sample_2) |
|
188 | -! | +
- snapshot_name <- tools::file_path_sans_ext(input$snapshot_file$name)+ #' ) |
|
189 |
- }+ #' }) |
||
190 | -! | +
- if (is.element(make.names(snapshot_name), make.names(names(snapshot_history())))) {+ #' } |
|
191 | -! | +
- showNotification(+ #' if (interactive()) { |
|
192 | -! | +
- "This name is in conflict with other snapshot names. Please choose a different one.",+ #' shinyApp(ui, server) |
|
193 | -! | +
- type = "message"+ #' } |
|
194 |
- )+ validate_has_elements <- function(x, msg) { |
||
195 | ! |
- updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name")+ validate(need(length(x) > 0, msg)) |
|
196 |
- } else {+ } |
||
197 |
- # Restore snapshot and verify app compatibility.+ |
||
198 | -! | +
- snapshot_state <- try(slices_restore(input$snapshot_file$datapath))+ #' Validates no intersection between two vectors |
|
199 | -! | +
- if (!inherits(snapshot_state, "modules_teal_slices")) {+ #' |
|
200 | -! | +
- showNotification(+ #' `r lifecycle::badge("stable")` |
|
201 | -! | +
- "File appears to be corrupt.",+ #' |
|
202 | -! | +
- type = "error"+ #' This function is a wrapper for `shiny::validate`. |
|
203 |
- )+ #' |
||
204 | -! | +
- } else if (!identical(attr(snapshot_state, "app_id"), attr(slices_global(), "app_id"))) {+ #' @param x vector |
|
205 | -! | +
- showNotification(+ #' @param y vector |
|
206 | -! | +
- "This snapshot file is not compatible with the app and cannot be loaded.",+ #' @param msg (`character(1)`) message to display if `x` and `y` intersect |
|
207 | -! | +
- type = "warning"+ #' |
|
208 |
- )+ #' @export |
||
209 |
- } else {+ #' |
||
210 |
- # Add to snapshot history.+ #' @examples |
||
211 | -! | +
- snapshot <- as.list(snapshot_state, recursive = TRUE)+ #' data <- data.frame( |
|
212 | -! | +
- snapshot_update <- c(snapshot_history(), list(snapshot))+ #' id = c(1:10, 11:20, 1:10), |
|
213 | -! | +
- names(snapshot_update)[length(snapshot_update)] <- snapshot_name+ #' strata = rep(c("A", "B", "C"), each = 10) |
|
214 | -! | +
- snapshot_history(snapshot_update)+ #' ) |
|
215 |
- ### Begin simplified restore procedure. ###+ #' |
||
216 | -! | +
- mapping_unfolded <- unfold_mapping(attr(snapshot_state, "mapping"), names(filtered_data_list))+ #' ui <- fluidPage( |
|
217 | -! | +
- mapply(+ #' selectInput("ref1", "Select strata1 to compare", |
|
218 | -! | +
- function(filtered_data, filter_ids) {+ #' choices = c("A", "B", "C"), |
|
219 | -! | +
- filtered_data$clear_filter_states(force = TRUE)+ #' selected = "A" |
|
220 | -! | +
- slices <- Filter(function(x) x$id %in% filter_ids, snapshot_state)+ #' ), |
|
221 | -! | +
- filtered_data$set_filter_state(slices)+ #' selectInput("ref2", "Select strata2 to compare", |
|
222 |
- },+ #' choices = c("A", "B", "C"), |
||
223 | -! | +
- filtered_data = filtered_data_list,+ #' selected = "B" |
|
224 | -! | +
- filter_ids = mapping_unfolded+ #' ), |
|
225 |
- )+ #' verbatimTextOutput("summary") |
||
226 | -! | +
- slices_global(snapshot_state)+ #' ) |
|
227 | -! | +
- removeModal()+ #' |
|
228 |
- ### End simplified restore procedure. ###+ #' server <- function(input, output) { |
||
229 |
- }+ #' output$summary <- renderText({ |
||
230 |
- }+ #' sample_1 <- data$id[data$strata == input$ref1] |
||
231 |
- })+ #' sample_2 <- data$id[data$strata == input$ref2] |
||
232 |
- # Apply newly added snapshot.+ #' |
||
233 |
-
+ #' validate_no_intersection( |
||
234 |
- # Restore initial state ----+ #' sample_1, sample_2, |
||
235 | -6x | +
- observeEvent(input$snapshot_reset, {+ #' "subjects within strata1 and strata2 cannot overlap" |
|
236 | -! | +
- s <- "Initial application state"+ #' ) |
|
237 |
- ### Begin restore procedure. ###+ #' paste0( |
||
238 | -! | +
- snapshot <- snapshot_history()[[s]]+ #' "Number of subject in: reference treatment=", length(sample_1), |
|
239 | -! | +
- snapshot_state <- as.teal_slices(snapshot)+ #' " comparions treatment=", length(sample_2) |
|
240 | -! | +
- mapping_unfolded <- unfold_mapping(attr(snapshot_state, "mapping"), names(filtered_data_list))+ #' ) |
|
241 | -! | +
- mapply(+ #' }) |
|
242 | -! | +
- function(filtered_data, filter_ids) {+ #' } |
|
243 | -! | +
- filtered_data$clear_filter_states(force = TRUE)+ #' if (interactive()) { |
|
244 | -! | +
- slices <- Filter(function(x) x$id %in% filter_ids, snapshot_state)+ #' shinyApp(ui, server) |
|
245 | -! | +
- filtered_data$set_filter_state(slices)+ #' } |
|
246 |
- },+ #' |
||
247 | -! | +
- filtered_data = filtered_data_list,+ validate_no_intersection <- function(x, y, msg) { |
|
248 | ! |
- filter_ids = mapping_unfolded+ validate(need(length(intersect(x, y)) == 0, msg)) |
|
249 |
- )+ } |
||
250 | -! | +
- slices_global(snapshot_state)+ |
|
251 | -! | +
- removeModal()+ |
|
252 |
- ### End restore procedure. ###+ #' Validates that dataset contains specific variable |
||
253 |
- })+ #' |
||
254 |
-
+ #' `r lifecycle::badge("stable")` |
||
255 |
- # Build snapshot table ----+ #' |
||
256 |
- # Create UI elements and server logic for the snapshot table.+ #' This function is a wrapper for `shiny::validate`. |
||
257 |
- # Observers must be tracked to avoid duplication and excess reactivity.+ #' |
||
258 |
- # Remaining elements are tracked likewise for consistency and a slight speed margin.+ #' @param data (`data.frame`) |
||
259 | -6x | +
- observers <- reactiveValues()+ #' @param varname (`character(1)`) name of variable to check for in `data` |
|
260 | -6x | +
- handlers <- reactiveValues()+ #' @param msg (`character(1)`) message to display if `data` does not include `varname` |
|
261 | -6x | +
- divs <- reactiveValues()+ #' |
|
262 |
-
+ #' @export |
||
263 | -6x | +
- observeEvent(snapshot_history(), {+ #' |
|
264 | -2x | +
- lapply(names(snapshot_history())[-1L], function(s) {+ #' @examples |
|
265 | -! | +
- id_pickme <- sprintf("pickme_%s", make.names(s))+ #' data <- data.frame( |
|
266 | -! | +
- id_saveme <- sprintf("saveme_%s", make.names(s))+ #' one = rep("a", length.out = 20), |
|
267 | -! | +
- id_rowme <- sprintf("rowme_%s", make.names(s))+ #' two = rep(c("a", "b"), length.out = 20) |
|
268 |
-
+ #' ) |
||
269 |
- # Observer for restoring snapshot.+ #' ui <- fluidPage( |
||
270 | -! | +
- if (!is.element(id_pickme, names(observers))) {+ #' selectInput( |
|
271 | -! | +
- observers[[id_pickme]] <- observeEvent(input[[id_pickme]], {+ #' "var", |
|
272 |
- ### Begin restore procedure. ###+ #' "Select variable", |
||
273 | -! | +
- snapshot <- snapshot_history()[[s]]+ #' choices = c("one", "two", "three", "four"), |
|
274 | -! | +
- snapshot_state <- as.teal_slices(snapshot)+ #' selected = "one" |
|
275 | -! | +
- mapping_unfolded <- unfold_mapping(attr(snapshot_state, "mapping"), names(filtered_data_list))+ #' ), |
|
276 | -! | +
- mapply(+ #' verbatimTextOutput("summary") |
|
277 | -! | +
- function(filtered_data, filter_ids) {+ #' ) |
|
278 | -! | +
- filtered_data$clear_filter_states(force = TRUE)+ #' |
|
279 | -! | +
- slices <- Filter(function(x) x$id %in% filter_ids, snapshot_state)+ #' server <- function(input, output) { |
|
280 | -! | +
- filtered_data$set_filter_state(slices)+ #' output$summary <- renderText({ |
|
281 |
- },+ #' validate_has_variable(data, input$var) |
||
282 | -! | +
- filtered_data = filtered_data_list,+ #' paste0("Selected treatment variables: ", paste(input$var, collapse = ", ")) |
|
283 | -! | +
- filter_ids = mapping_unfolded+ #' }) |
|
284 |
- )+ #' } |
||
285 | -! | +
- slices_global(snapshot_state)+ #' if (interactive()) { |
|
286 | -! | +
- removeModal()+ #' shinyApp(ui, server) |
|
287 |
- ### End restore procedure. ###+ #' } |
||
288 |
- })+ validate_has_variable <- function(data, varname, msg) { |
||
289 | -+ | ! |
- }+ if (length(varname) != 0) { |
290 | -+ | ! |
- # Create handler for downloading snapshot.+ has_vars <- varname %in% names(data) |
291 | -! | +
- if (!is.element(id_saveme, names(handlers))) {+ |
|
292 | ! |
- output[[id_saveme]] <- downloadHandler(+ if (!all(has_vars)) { |
|
293 | ! |
- filename = function() {+ if (missing(msg)) { |
|
294 | ! |
- sprintf("teal_snapshot_%s_%s.json", s, Sys.Date())+ msg <- sprintf( |
|
295 | -+ | ! |
- },+ "%s does not have the required variables: %s.", |
296 | ! |
- content = function(file) {+ deparse(substitute(data)), |
|
297 | ! |
- snapshot <- snapshot_history()[[s]]+ toString(varname[!has_vars]) |
|
298 | -! | +
- snapshot_state <- as.teal_slices(snapshot)+ ) |
|
299 | -! | +
- slices_store(tss = snapshot_state, file = file)+ } |
|
300 | -+ | ! |
- }+ validate(need(FALSE, msg)) |
301 |
- )+ } |
||
302 | -! | +
- handlers[[id_saveme]] <- id_saveme+ } |
|
303 |
- }+ } |
||
304 |
- # Create a row for the snapshot table.+ |
||
305 | -! | +
- if (!is.element(id_rowme, names(divs))) {+ #' Validate that variables has expected number of levels |
|
306 | -! | +
- divs[[id_rowme]] <- div(+ #' |
|
307 | -! | +
- class = "snapshot_table_row",+ #' `r lifecycle::badge("stable")` |
|
308 | -! | +
- span(h5(s)),+ #' |
|
309 | -! | +
- actionLink(inputId = ns(id_pickme), label = icon("circle-check"), title = "select"),+ #' If the number of levels of `x` is less than `min_levels` |
|
310 | -! | +
- downloadLink(outputId = ns(id_saveme), label = icon("save"), title = "save to file")+ #' or greater than `max_levels` the validation will fail. |
|
311 |
- )+ #' This function is a wrapper for `shiny::validate`. |
||
312 |
- }+ #' |
||
313 |
- })+ #' @param x variable name. If `x` is not a factor, the unique values |
||
314 |
- })+ #' are treated as levels. |
||
315 |
-
+ #' @param min_levels cutoff for minimum number of levels of `x` |
||
316 |
- # Create table to display list of snapshots and their actions.+ #' @param max_levels cutoff for maximum number of levels of `x` |
||
317 | -6x | +
- output$snapshot_list <- renderUI({+ #' @param var_name name of variable being validated for use in |
|
318 | -2x | +
- rows <- lapply(rev(reactiveValuesToList(divs)), function(d) d)+ #' validation message |
|
319 | -2x | +
- if (length(rows) == 0L) {+ #' |
|
320 | -2x | +
- div(+ #' @export |
|
321 | -2x | +
- class = "snapshot_manager_placeholder",+ #' @examples |
|
322 | -2x | +
- "Snapshots will appear here."+ #' data <- data.frame( |
|
323 |
- )+ #' one = rep("a", length.out = 20), |
||
324 |
- } else {+ #' two = rep(c("a", "b"), length.out = 20), |
||
325 | -! | +
- rows+ #' three = rep(c("a", "b", "c"), length.out = 20), |
|
326 |
- }+ #' four = rep(c("a", "b", "c", "d"), length.out = 20), |
||
327 |
- })+ #' stringsAsFactors = TRUE |
||
328 |
- })+ #' ) |
||
329 |
- }+ #' ui <- fluidPage( |
||
330 |
-
+ #' selectInput( |
||
331 |
- ### utility functions ----+ #' "var", |
||
332 |
-
+ #' "Select variable", |
||
333 |
- #' Explicitly enumerate global filters.+ #' choices = c("one", "two", "three", "four"), |
||
334 |
- #'+ #' selected = "one" |
||
335 |
- #' Transform module mapping such that global filters are explicitly specified for every module.+ #' ), |
||
336 |
- #'+ #' verbatimTextOutput("summary") |
||
337 |
- #' @param mapping (named `list`) as stored in mapping parameter of `teal_slices`+ #' ) |
||
338 |
- #' @param module_names (`character`) vector containing names of all modules in the app+ #' |
||
339 |
- #' @return A `named_list` with one element per module, each element containing all filters applied to that module.+ #' server <- function(input, output) { |
||
340 |
- #' @keywords internal+ #' output$summary <- renderText({ |
||
341 |
- #'+ #' validate_n_levels(data[[input$var]], min_levels = 2, max_levels = 15, var_name = input$var) |
||
342 |
- unfold_mapping <- function(mapping, module_names) {+ #' paste0( |
||
343 | -! | +
- module_names <- structure(module_names, names = module_names)+ #' "Levels of selected treatment variable: ", |
|
344 | -! | +
- lapply(module_names, function(x) c(mapping[[x]], mapping[["global_filters"]]))+ #' paste(levels(data[[input$var]]), |
|
345 |
- }+ #' collapse = ", " |
||
346 |
-
+ #' ) |
||
347 |
- #' Convert mapping matrix to filter mapping specification.+ #' ) |
||
348 |
- #'+ #' }) |
||
349 |
- #' Transform a mapping matrix, i.e. a data frame that maps each filter state to each module,+ #' } |
||
350 |
- #' to a list specification like the one used in the `mapping` attribute of `teal_slices`.+ #' if (interactive()) { |
||
351 |
- #' Global filters are gathered in one list element.+ #' shinyApp(ui, server) |
||
352 |
- #' If a module has no active filters but the global ones, it will not be mentioned in the output.+ #' } |
||
353 |
- #'+ validate_n_levels <- function(x, min_levels = 1, max_levels = 12, var_name) { |
||
354 | -+ | ! |
- #' @param mapping_matrix (`data.frame`) of logical vectors where+ x_levels <- if (is.factor(x)) { |
355 | -+ | ! |
- #' columns represent modules and row represent `teal_slice`s+ levels(x) |
356 |
- #' @return Named `list` like that in the `mapping` attribute of a `teal_slices` object.+ } else { |
||
357 | -+ | ! |
- #' @keywords internal+ unique(x) |
358 |
- #'+ } |
||
359 |
- matrix_to_mapping <- function(mapping_matrix) {+ |
||
360 | ! |
- mapping_matrix[] <- lapply(mapping_matrix, function(x) x | is.na(x))+ if (!is.null(min_levels) && !(is.null(max_levels))) { |
|
361 | ! |
- global <- vapply(as.data.frame(t(mapping_matrix)), all, logical(1L))+ validate(need( |
|
362 | ! |
- global_filters <- names(global[global])+ length(x_levels) >= min_levels && length(x_levels) <= max_levels, |
|
363 | ! |
- local_filters <- mapping_matrix[!rownames(mapping_matrix) %in% global_filters, ]+ sprintf( |
|
364 | -+ | ! |
-
+ "%s variable needs minimum %s level(s) and maximum %s level(s).", |
365 | ! |
- mapping <- c(lapply(local_filters, function(x) rownames(local_filters)[x]), list(global_filters = global_filters))+ var_name, min_levels, max_levels |
|
366 | -! | +
- Filter(function(x) length(x) != 0L, mapping)+ ) |
|
367 | + |
+ ))+ |
+ |
368 | +! | +
+ } else if (!is.null(min_levels)) {+ |
+ |
369 | +! | +
+ validate(need(+ |
+ |
370 | +! | +
+ length(x_levels) >= min_levels,+ |
+ |
371 | +! | +
+ sprintf("%s variable needs minimum %s levels(s)", var_name, min_levels)+ |
+ |
372 | ++ |
+ ))+ |
+ |
373 | +! | +
+ } else if (!is.null(max_levels)) {+ |
+ |
374 | +! | +
+ validate(need(+ |
+ |
375 | +! | +
+ length(x_levels) <= max_levels,+ |
+ |
376 | +! | +
+ sprintf("%s variable needs maximum %s level(s)", var_name, max_levels)+ |
+ |
377 | ++ |
+ ))+ |
+ |
378 | ++ |
+ }+ |
+ |
379 | +
} |
@@ -2686,14 +2770,14 @@
1 |
- #' Filter settings for `teal` applications+ #' Filter state snapshot management. |
||
3 |
- #' Specify initial filter states and filtering settings for a `teal` app.+ #' Capture and restore snapshots of the global (app) filter state. |
||
5 |
- #' Produces a `teal_slices` object.+ #' This module introduces snapshots: stored descriptions of the filter state of the entire application. |
||
6 |
- #' The `teal_slice` components will specify filter states that will be active when the app starts.+ #' Snapshots allow the user to save the current filter state of the application for later use in the session, |
||
7 |
- #' Attributes (created with the named arguments) will configure the way the app applies filters.+ #' as well as to save it to file in order to share it with an app developer or other users, |
||
8 |
- #' See argument descriptions for details.+ #' who in turn can upload it to their own session. |
||
10 |
- #' @inheritParams teal.slice::teal_slices+ #' The snapshot manager is accessed through the filter manager, with the cog icon in the top right corner. |
||
11 |
- #'+ #' At the beginning of a session it presents three icons: a camera, an upload, and an circular arrow. |
||
12 |
- #' @param module_specific optional (`logical(1)`)+ #' Clicking the camera captures a snapshot, clicking the upload adds a snapshot from a file |
||
13 |
- #' - `FALSE` (default) when one filter panel applied to all modules.+ #' and applies the filter states therein, and clicking the arrow resets initial application state. |
||
14 |
- #' All filters will be shared by all modules.+ #' As snapshots are added, they will show up as rows in a table and each will have a select button and a save button. |
||
15 |
- #' - `TRUE` when filter panel module-specific.+ #' |
||
16 |
- #' Modules can have different set of filters specified - see `mapping` argument.+ #' @section Server logic: |
||
17 |
- #' @param mapping `r lifecycle::badge("experimental")`+ #' Snapshots are basically `teal_slices` objects, however, since each module is served by a separate instance |
||
18 |
- #' _This is a new feature. Do kindly share your opinions on+ #' of `FilteredData` and these objects require shared state, `teal_slice` is a `reactiveVal` so `teal_slices` |
||
19 |
- #' [`teal`'s GitHub repository](https://github.com/insightsengineering/teal/)._+ #' cannot be stored as is. Therefore, `teal_slices` are reversibly converted to a list of lists representation |
||
20 |
- #'+ #' (attributes are maintained). |
||
21 |
- #' (named `list`) specifies which filters will be active in which modules on app start.+ #' |
||
22 |
- #' Elements should contain character vector of `teal_slice` `id`s (see [`teal.slice::teal_slice`]).+ #' Snapshots are stored in a `reactiveVal` as a named list. |
||
23 |
- #' Names of the list should correspond to `teal_module` `label` set in [module()] function.+ #' The first snapshot is the initial state of the application and the user can add a snapshot whenever they see fit. |
||
24 |
- #' - `id`s listed under `"global_filters` will be active in all modules.+ #' |
||
25 |
- #' - If missing, all filters will be applied to all modules.+ #' For every snapshot except the initial one, a piece of UI is generated that contains |
||
26 |
- #' - If empty list, all filters will be available to all modules but will start inactive.+ #' the snapshot name, a select button to restore that snapshot, and a save button to save it to a file. |
||
27 |
- #' - If `module_specific` is `FALSE`, only `global_filters` will be active on start.+ #' The initial snapshot is restored by a separate "reset" button. |
||
28 |
- #' @param app_id (`character(1)`)+ #' It cannot be saved directly but a user is welcome to capture the initial state as a snapshot and save that. |
||
29 |
- #' For internal use only, do not set manually.+ #' |
||
30 |
- #' Added by `init` so that a `teal_slices` can be matched to the app in which it was used.+ #' @section Snapshot mechanics: |
||
31 |
- #' Used for verifying snapshots uploaded from file. See `snapshot`.+ #' When a snapshot is captured, the user is prompted to name it. |
||
32 |
- #'+ #' Names are displayed as is but since they are used to create button ids, |
||
33 |
- #' @param x (`list`) of lists to convert to `teal_slices`+ #' under the hood they are converted to syntactically valid strings. |
||
34 |
- #'+ #' New snapshot names are validated so that their valid versions are unique. |
||
35 |
- #' @return+ #' Leading and trailing white space is trimmed. |
||
36 |
- #' A `teal_slices` object.+ #' |
||
37 |
- #'+ #' The module can read the global state of the application from `slices_global` and `mapping_matrix`. |
||
38 |
- #' @seealso [`teal.slice::teal_slices`], [`teal.slice::teal_slice`], [slices_store()]+ #' The former provides a list of all existing `teal_slice`s and the latter says which slice is active in which module. |
||
39 |
- #'+ #' Once a name has been accepted, `slices_global` is converted to a list of lists - a snapshot. |
||
40 |
- #' @examples+ #' The snapshot contains the `mapping` attribute of the initial application state |
||
41 |
- #' filter <- teal_slices(+ #' (or one that has been restored), which may not reflect the current one, |
||
42 |
- #' teal_slice(dataname = "iris", varname = "Species", id = "species"),+ #' so `mapping_matrix` is transformed to obtain the current mapping, i.e. a list that, |
||
43 |
- #' teal_slice(dataname = "iris", varname = "Sepal.Length", id = "sepal_length"),+ #' when passed to the `mapping` argument of [teal_slices()], would result in the current mapping. |
||
44 |
- #' teal_slice(+ #' This is substituted as the snapshot's `mapping` attribute and the snapshot is added to the snapshot list. |
||
45 |
- #' dataname = "iris", id = "long_petals", title = "Long petals", expr = "Petal.Length > 5"+ #' |
||
46 |
- #' ),+ #' To restore app state, a snapshot is retrieved from storage and rebuilt into a `teal_slices` object. |
||
47 |
- #' teal_slice(dataname = "mtcars", varname = "mpg", id = "mtcars_mpg"),+ #' Then state of all `FilteredData` objects (provided in `filtered_data_list`) is cleared |
||
48 |
- #' mapping = list(+ #' and set anew according to the `mapping` attribute of the snapshot. |
||
49 |
- #' module1 = c("species", "sepal_length"),+ #' The snapshot is then set as the current content of `slices_global`. |
||
50 |
- #' module2 = c("mtcars_mpg"),+ #' |
||
51 |
- #' global_filters = "long_petals"+ #' To save a snapshot, the snapshot is retrieved and reassembled just like for restoring, |
||
52 |
- #' )+ #' and then saved to file with [slices_store()]. |
||
53 |
- #' )+ #' |
||
54 |
- #'+ #' When a snapshot is uploaded, it will first be added to storage just like a newly created one, |
||
55 |
- #' app <- init(+ #' and then used to restore app state much like a snapshot taken from storage. |
||
56 |
- #' data = teal_data(iris = iris, mtcars = mtcars),+ #' Upon clicking the upload icon the user will be prompted for a file to upload |
||
57 |
- #' modules = list(+ #' and may choose to name the new snapshot. The name defaults to the name of the file (the extension is dropped) |
||
58 |
- #' module("module1"),+ #' and normal naming rules apply. Loading the file yields a `teal_slices` object, |
||
59 |
- #' module("module2")+ #' which is disassembled for storage and used directly for restoring app state. |
||
60 |
- #' ),+ #' |
||
61 |
- #' filter = filter+ #' @section Transferring snapshots: |
||
62 |
- #' )+ #' Snapshots uploaded from disk should only be used in the same application they come from, |
||
63 |
- #'+ #' _i.e._ an application that uses the same data and the same modules. |
||
64 |
- #' if (interactive()) {+ #' To ensure this is the case, `init` stamps `teal_slices` with an app id that is stored in the `app_id` attribute of |
||
65 |
- #' shinyApp(app$ui, app$server)+ #' 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. |
||
68 |
- #' @export+ #' @param id (`character(1)`) `shiny` module id |
||
69 |
- teal_slices <- function(...,+ #' @param slices_global (`reactiveVal`) that contains a `teal_slices` object |
||
70 |
- exclude_varnames = NULL,+ #' containing all `teal_slice`s existing in the app, both active and inactive |
||
71 |
- include_varnames = NULL,+ #' @param mapping_matrix (`reactive`) that contains a `data.frame` representation |
||
72 |
- count_type = NULL,+ #' of the mapping of filter state ids (rows) to modules labels (columns); |
||
73 |
- allow_add = TRUE,+ #' all columns are `logical` vectors |
||
74 |
- module_specific = FALSE,+ #' @param filtered_data_list non-nested (named `list`) that contains `FilteredData` objects |
||
75 |
- mapping,+ #' |
||
76 |
- app_id = NULL) {+ #' @return Nothing is returned. |
||
77 | -78x | +
- shiny::isolate({+ #' |
|
78 | -78x | +
- checkmate::assert_flag(allow_add)+ #' @name snapshot_manager_module |
|
79 | -78x | +
- checkmate::assert_flag(module_specific)+ #' @aliases snapshot snapshot_manager |
|
80 | -32x | +
- if (!missing(mapping)) checkmate::assert_list(mapping, types = c("character", "NULL"), names = "named")+ #' |
|
81 | -75x | +
- checkmate::assert_string(app_id, null.ok = TRUE)+ #' @author Aleksander Chlebowski |
|
82 |
-
+ #' |
||
83 | -75x | +
- slices <- list(...)+ #' @rdname snapshot_manager_module |
|
84 | -75x | +
- all_slice_id <- vapply(slices, `[[`, character(1L), "id")+ #' @keywords internal |
|
85 |
-
+ #' |
||
86 | -75x | +
- if (missing(mapping)) {+ snapshot_manager_ui <- function(id) { |
|
87 | -46x | +! |
- mapping <- list(global_filters = all_slice_id)+ ns <- NS(id) |
88 | -+ | ! |
- }+ div( |
89 | -75x | +! |
- if (!module_specific) {+ class = "snapshot_manager_content", |
90 | -71x | +! |
- mapping[setdiff(names(mapping), "global_filters")] <- NULL+ div( |
91 | -+ | ! |
- }+ class = "snapshot_table_row", |
92 | -+ | ! |
-
+ span(tags$b("Snapshot manager")), |
93 | -75x | +! |
- failed_slice_id <- setdiff(unlist(mapping), all_slice_id)+ actionLink(ns("snapshot_add"), label = NULL, icon = icon("camera"), title = "add snapshot"), |
94 | -75x | +! |
- if (length(failed_slice_id)) {+ actionLink(ns("snapshot_load"), label = NULL, icon = icon("upload"), title = "upload snapshot"), |
95 | -1x | +! |
- stop(sprintf(+ actionLink(ns("snapshot_reset"), label = NULL, icon = icon("undo"), title = "reset initial state"), |
96 | -1x | +! |
- "Filters in mapping don't match any available filter.\n %s not in %s",+ NULL |
97 | -1x | +
- toString(failed_slice_id),+ ), |
|
98 | -1x | +! |
- toString(all_slice_id)+ uiOutput(ns("snapshot_list")) |
99 |
- ))+ ) |
||
100 |
- }+ } |
||
102 | -74x | +
- tss <- teal.slice::teal_slices(+ #' @rdname snapshot_manager_module |
|
103 |
- ...,+ #' @keywords internal |
||
104 | -74x | +
- exclude_varnames = exclude_varnames,+ #' |
|
105 | -74x | +
- include_varnames = include_varnames,+ snapshot_manager_srv <- function(id, slices_global, mapping_matrix, filtered_data_list) { |
|
106 | -74x | +6x |
- count_type = count_type,+ checkmate::assert_character(id) |
107 | -74x | +6x |
- allow_add = allow_add+ checkmate::assert_true(is.reactive(slices_global)) |
108 | -+ | 6x |
- )+ checkmate::assert_class(isolate(slices_global()), "teal_slices") |
109 | -74x | +6x |
- attr(tss, "mapping") <- mapping+ checkmate::assert_true(is.reactive(mapping_matrix)) |
110 | -74x | +6x |
- attr(tss, "module_specific") <- module_specific+ checkmate::assert_data_frame(isolate(mapping_matrix()), null.ok = TRUE) |
111 | -74x | +6x |
- attr(tss, "app_id") <- app_id+ checkmate::assert_list(filtered_data_list, types = "FilteredData", any.missing = FALSE, names = "named") |
112 | -74x | +
- class(tss) <- c("modules_teal_slices", class(tss))+ |
|
113 | -74x | +6x |
- tss+ moduleServer(id, function(input, output, session) { |
114 | -+ | 6x |
- })+ ns <- session$ns |
115 |
- }+ |
||
116 |
-
+ # Store global filter states ---- |
||
117 | -+ | 6x |
-
+ filter <- isolate(slices_global()) |
118 | -+ | 6x |
- #' @rdname teal_slices+ snapshot_history <- reactiveVal({ |
119 | -+ | 6x |
- #' @export+ list( |
120 | -+ | 6x |
- #' @keywords internal+ "Initial application state" = as.list(filter, recursive = TRUE) |
121 |
- #'+ ) |
||
122 |
- as.teal_slices <- function(x) { # nolint+ }) |
||
123 | -10x | +
- checkmate::assert_list(x)+ |
|
124 | -10x | +
- lapply(x, checkmate::assert_list, names = "named", .var.name = "list element")+ # Snapshot current application state ---- |
|
125 |
-
+ # Name snaphsot. |
||
126 | -10x | +6x |
- attrs <- attributes(unclass(x))+ observeEvent(input$snapshot_add, { |
127 | -10x | +! |
- ans <- lapply(x, function(x) if (is.teal_slice(x)) x else as.teal_slice(x))+ showModal( |
128 | -10x | +! |
- do.call(teal_slices, c(ans, attrs))+ modalDialog( |
129 | -+ | ! |
- }+ textInput(ns("snapshot_name"), "Name the snapshot", width = "100%", placeholder = "Meaningful, unique name"), |
130 | -+ | ! |
-
+ footer = tagList( |
131 | -+ | ! |
-
+ actionButton(ns("snapshot_name_accept"), "Accept", icon = icon("thumbs-up")), |
132 | -+ | ! |
- #' @rdname teal_slices+ modalButton(label = "Cancel", icon = icon("thumbs-down")) |
133 |
- #' @export+ ), |
||
134 | -+ | ! |
- #' @keywords internal+ size = "s" |
135 |
- #'+ ) |
||
136 |
- c.teal_slices <- function(...) {+ ) |
||
137 | -! | +
- x <- list(...)+ }) |
|
138 | -! | +
- checkmate::assert_true(all(vapply(x, is.teal_slices, logical(1L))), .var.name = "all arguments are teal_slices")+ # Store snaphsot. |
|
139 | -+ | 6x |
-
+ observeEvent(input$snapshot_name_accept, { |
140 | ! |
- all_attributes <- lapply(x, attributes)+ snapshot_name <- trimws(input$snapshot_name) |
|
141 | ! |
- all_attributes <- coalesce_r(all_attributes)+ if (identical(snapshot_name, "")) { |
|
142 | ! |
- all_attributes <- all_attributes[names(all_attributes) != "class"]+ showNotification( |
|
143 | -+ | ! |
-
+ "Please name the snapshot.", |
144 | ! |
- do.call(+ type = "message" |
|
145 | -! | +
- teal_slices,+ ) |
|
146 | ! |
- c(+ updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name") |
|
147 | ! |
- unique(unlist(x, recursive = FALSE)),+ } else if (is.element(make.names(snapshot_name), make.names(names(snapshot_history())))) { |
|
148 | ! |
- all_attributes+ showNotification( |
|
149 | -+ | ! |
- )+ "This name is in conflict with other snapshot names. Please choose a different one.", |
150 | -+ | ! |
- )+ type = "message" |
151 |
- }+ ) |
||
152 | -+ | ! |
-
+ updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name") |
153 |
-
+ } else { |
||
154 | -+ | ! |
- #' Deep copy `teal_slices`+ snapshot <- as.list(slices_global(), recursive = TRUE) |
155 | -+ | ! |
- #'+ attr(snapshot, "mapping") <- matrix_to_mapping(mapping_matrix()) |
156 | -+ | ! |
- #' it's important to create a new copy of `teal_slices` when+ snapshot_update <- c(snapshot_history(), list(snapshot)) |
157 | -+ | ! |
- #' starting a new `shiny` session. Otherwise, object will be shared+ names(snapshot_update)[length(snapshot_update)] <- snapshot_name |
158 | -+ | ! |
- #' by multiple users as it is created in global environment before+ snapshot_history(snapshot_update) |
159 | -+ | ! |
- #' `shiny` session starts.+ removeModal() |
160 |
- #' @param filter (`teal_slices`)+ # Reopen filter manager modal by clicking button in the main application. |
||
161 | -+ | ! |
- #' @return `teal_slices`+ shinyjs::click(id = "teal-main_ui-filter_manager-show", asis = TRUE) |
162 |
- #' @keywords internal+ } |
||
163 |
- deep_copy_filter <- function(filter) {+ }) |
||
164 | -1x | +
- checkmate::assert_class(filter, "teal_slices")+ |
|
165 | -1x | +
- shiny::isolate({+ # Upload a snapshot file ---- |
|
166 | -1x | +
- filter_copy <- lapply(filter, function(slice) {+ # Select file. |
|
167 | -2x | +6x |
- teal.slice::as.teal_slice(as.list(slice))+ observeEvent(input$snapshot_load, { |
168 | -+ | ! |
- })+ showModal( |
169 | -1x | +! |
- attributes(filter_copy) <- attributes(filter)+ modalDialog( |
170 | -1x | +! |
- filter_copy+ fileInput(ns("snapshot_file"), "Choose snapshot file", accept = ".json", width = "100%"), |
171 | -+ | ! |
- })+ textInput( |
172 | -+ | ! |
- }+ ns("snapshot_name"), |
1 | -+ | |||
173 | +! |
- #' Manage multiple `FilteredData` objects+ "Name the snapshot (optional)", |
||
2 | -+ | |||
174 | +! |
- #'+ width = "100%", |
||
3 | -+ | |||
175 | +! |
- #' Oversee filter states across the entire application.+ placeholder = "Meaningful, unique name" |
||
4 | +176 |
- #'+ ), |
||
5 | -+ | |||
177 | +! |
- #' This module observes changes in the filters of each `FilteredData` object+ footer = tagList( |
||
6 | -+ | |||
178 | +! |
- #' and keeps track of all filters used. A mapping of filters to modules+ actionButton(ns("snaphot_file_accept"), "Accept", icon = icon("thumbs-up")), |
||
7 | -+ | |||
179 | +! |
- #' is kept in the `mapping_matrix` object (which is actually a `data.frame`)+ modalButton(label = "Cancel", icon = icon("thumbs-down")) |
||
8 | +180 |
- #' that tracks which filters (rows) are active in which modules (columns).+ ) |
||
9 | +181 |
- #'+ ) |
||
10 | +182 |
- #' @name module_filter_manager+ ) |
||
11 | +183 |
- #'+ }) |
||
12 | +184 |
- #' @param id (`character(1)`)+ # Store new snapshot to list and restore filter states. |
||
13 | -+ | |||
185 | +6x |
- #' `shiny` module id.+ observeEvent(input$snaphot_file_accept, { |
||
14 | -+ | |||
186 | +! |
- #' @param filtered_data_list (named `list`)+ snapshot_name <- trimws(input$snapshot_name) |
||
15 | -+ | |||
187 | +! |
- #' A list, possibly nested, of `FilteredData` objects.+ if (identical(snapshot_name, "")) { |
||
16 | -+ | |||
188 | +! |
- #' Each `FilteredData` will be served to one module in the `teal` application.+ snapshot_name <- tools::file_path_sans_ext(input$snapshot_file$name) |
||
17 | +189 |
- #' The structure of the list must reflect the nesting of modules in tabs+ } |
||
18 | -+ | |||
190 | +! |
- #' and the names of the list must match the labels of their respective modules.+ if (is.element(make.names(snapshot_name), make.names(names(snapshot_history())))) { |
||
19 | -+ | |||
191 | +! |
- #' @inheritParams init+ showNotification( |
||
20 | -+ | |||
192 | +! |
- #' @return A list of `reactive`s, each holding a `teal_slices`, as returned by `filter_manager_module_srv`.+ "This name is in conflict with other snapshot names. Please choose a different one.", |
||
21 | -+ | |||
193 | +! |
- #' @keywords internal+ type = "message" |
||
22 | +194 |
- #'+ ) |
||
23 | -+ | |||
195 | +! |
- NULL+ updateTextInput(inputId = "snapshot_name", value = "", placeholder = "Meaningful, unique name") |
||
24 | +196 |
-
+ } else { |
||
25 | +197 |
- #' Filter manager modal+ # Restore snapshot and verify app compatibility. |
||
26 | -+ | |||
198 | +! |
- #'+ snapshot_state <- try(slices_restore(input$snapshot_file$datapath)) |
||
27 | -+ | |||
199 | +! |
- #' Opens a modal containing the filter manager UI.+ if (!inherits(snapshot_state, "modules_teal_slices")) { |
||
28 | -+ | |||
200 | +! |
- #'+ showNotification( |
||
29 | -+ | |||
201 | +! |
- #' @name module_filter_manager_modal+ "File appears to be corrupt.", |
||
30 | -+ | |||
202 | +! |
- #' @inheritParams module_filter_manager+ type = "error" |
||
31 | +203 |
- #' @examples+ ) |
||
32 | -+ | |||
204 | +! |
- #' # use non-exported function from teal+ } else if (!identical(attr(snapshot_state, "app_id"), attr(slices_global(), "app_id"))) { |
||
33 | -+ | |||
205 | +! |
- #' filter_manager_modal_ui <- getFromNamespace("filter_manager_modal_ui", "teal")+ showNotification( |
||
34 | -+ | |||
206 | +! |
- #' filter_manager_modal_srv <- getFromNamespace("filter_manager_modal_srv", "teal")+ "This snapshot file is not compatible with the app and cannot be loaded.", |
||
35 | -+ | |||
207 | +! |
- #'+ type = "warning" |
||
36 | +208 |
- #' fd1 <- teal.slice::init_filtered_data(list(iris = list(dataset = iris)))+ ) |
||
37 | +209 |
- #' fd2 <- teal.slice::init_filtered_data(+ } else { |
||
38 | +210 |
- #' list(iris = list(dataset = iris), mtcars = list(dataset = mtcars))+ # Add to snapshot history. |
||
39 | -+ | |||
211 | +! |
- #' )+ snapshot <- as.list(snapshot_state, recursive = TRUE) |
||
40 | -+ | |||
212 | +! |
- #' fd3 <- teal.slice::init_filtered_data(+ snapshot_update <- c(snapshot_history(), list(snapshot)) |
||
41 | -+ | |||
213 | +! |
- #' list(iris = list(dataset = iris), women = list(dataset = women))+ names(snapshot_update)[length(snapshot_update)] <- snapshot_name |
||
42 | -+ | |||
214 | +! |
- #' )+ snapshot_history(snapshot_update) |
||
43 | +215 |
- #' filter <- teal_slices(+ ### Begin simplified restore procedure. ### |
||
44 | -+ | |||
216 | +! |
- #' teal.slice::teal_slice(dataname = "iris", varname = "Sepal.Length"),+ mapping_unfolded <- unfold_mapping(attr(snapshot_state, "mapping"), names(filtered_data_list)) |
||
45 | -+ | |||
217 | +! |
- #' teal.slice::teal_slice(dataname = "iris", varname = "Species"),+ mapply( |
||
46 | -+ | |||
218 | +! |
- #' teal.slice::teal_slice(dataname = "mtcars", varname = "mpg"),+ function(filtered_data, filter_ids) { |
||
47 | -+ | |||
219 | +! |
- #' teal.slice::teal_slice(dataname = "women", varname = "height"),+ filtered_data$clear_filter_states(force = TRUE) |
||
48 | -+ | |||
220 | +! |
- #' mapping = list(+ slices <- Filter(function(x) x$id %in% filter_ids, snapshot_state) |
||
49 | -+ | |||
221 | +! |
- #' module2 = c("mtcars mpg"),+ filtered_data$set_filter_state(slices) |
||
50 | +222 |
- #' module3 = c("women height"),+ }, |
||
51 | -+ | |||
223 | +! |
- #' global_filters = "iris Species"+ filtered_data = filtered_data_list, |
||
52 | -+ | |||
224 | +! |
- #' )+ filter_ids = mapping_unfolded |
||
53 | +225 |
- #' )+ ) |
||
54 | -+ | |||
226 | +! |
- #'+ slices_global(snapshot_state) |
||
55 | -+ | |||
227 | +! |
- #' ui <- fluidPage(+ removeModal() |
||
56 | +228 |
- #' filter_manager_modal_ui("manager")+ ### End simplified restore procedure. ### |
||
57 | +229 |
- #' )+ } |
||
58 | +230 |
- #'+ } |
||
59 | +231 |
- #' server <- function(input, output, session) {+ }) |
||
60 | +232 |
- #' observe({+ # Apply newly added snapshot. |
||
61 | +233 |
- #' filter_manager_modal_srv(+ |
||
62 | +234 |
- #' "manager",+ # Restore initial state ---- |
||
63 | -+ | |||
235 | +6x |
- #' filtered_data_list = list(module1 = fd1, module2 = fd2, module3 = fd3),+ observeEvent(input$snapshot_reset, { |
||
64 | -+ | |||
236 | +! |
- #' filter = filter+ s <- "Initial application state" |
||
65 | +237 |
- #' )+ ### Begin restore procedure. ### |
||
66 | -+ | |||
238 | +! |
- #' })+ snapshot <- snapshot_history()[[s]] |
||
67 | -+ | |||
239 | +! |
- #' }+ snapshot_state <- as.teal_slices(snapshot) |
||
68 | -+ | |||
240 | +! |
- #'+ mapping_unfolded <- unfold_mapping(attr(snapshot_state, "mapping"), names(filtered_data_list)) |
||
69 | -+ | |||
241 | +! |
- #' if (interactive()) {+ mapply( |
||
70 | -+ | |||
242 | +! |
- #' shinyApp(ui, server)+ function(filtered_data, filter_ids) { |
||
71 | -+ | |||
243 | +! |
- #' }+ filtered_data$clear_filter_states(force = TRUE) |
||
72 | -+ | |||
244 | +! |
- #'+ slices <- Filter(function(x) x$id %in% filter_ids, snapshot_state) |
||
73 | -- |
- #' @keywords internal- |
- ||
74 | -- |
- #'- |
- ||
75 | -- |
- NULL- |
- ||
76 | -+ | |||
245 | +! |
-
+ filtered_data$set_filter_state(slices) |
||
77 | +246 |
- #' @rdname module_filter_manager_modal+ }, |
||
78 | -+ | |||
247 | +! |
- filter_manager_modal_ui <- function(id) {+ filtered_data = filtered_data_list, |
||
79 | +248 | ! |
- ns <- NS(id)+ filter_ids = mapping_unfolded |
|
80 | -! | +|||
249 | +
- tags$button(+ ) |
|||
81 | +250 | ! |
- id = ns("show"),+ slices_global(snapshot_state) |
|
82 | +251 | ! |
- class = "btn action-button filter_manager_button",+ removeModal() |
|
83 | -! | +|||
252 | +
- title = "Show filters manager modal",+ ### End restore procedure. ### |
|||
84 | -! | +|||
253 | +
- icon("gear")+ }) |
|||
85 | +254 |
- )+ |
||
86 | +255 |
- }+ # Build snapshot table ---- |
||
87 | +256 |
-
+ # Create UI elements and server logic for the snapshot table. |
||
88 | +257 |
- #' @rdname module_filter_manager_modal+ # Observers must be tracked to avoid duplication and excess reactivity. |
||
89 | +258 |
- filter_manager_modal_srv <- function(id, filtered_data_list, filter) {+ # Remaining elements are tracked likewise for consistency and a slight speed margin. |
||
90 | -3x | +259 | +6x |
- moduleServer(id, function(input, output, session) {+ observers <- reactiveValues() |
91 | -3x | +260 | +6x |
- observeEvent(input$show, {+ handlers <- reactiveValues() |
92 | -! | +|||
261 | +6x |
- logger::log_trace("filter_manager_modal_srv@1 show button has been clicked.")+ divs <- reactiveValues() |
||
93 | -! | +|||
262 | +
- showModal(+ |
|||
94 | -! | +|||
263 | +6x |
- modalDialog(+ observeEvent(snapshot_history(), { |
||
95 | -! | +|||
264 | +2x |
- filter_manager_ui(session$ns("filter_manager")),+ lapply(names(snapshot_history())[-1L], function(s) { |
||
96 | +265 | ! |
- size = "l",+ id_pickme <- sprintf("pickme_%s", make.names(s)) |
|
97 | +266 | ! |
- footer = NULL,+ id_saveme <- sprintf("saveme_%s", make.names(s)) |
|
98 | +267 | ! |
- easyClose = TRUE- |
- |
99 | -- |
- )- |
- ||
100 | -- |
- )+ id_rowme <- sprintf("rowme_%s", make.names(s)) |
||
101 | +268 |
- })+ |
||
102 | +269 |
-
+ # Observer for restoring snapshot. |
||
103 | -3x | +|||
270 | +! |
- filter_manager_srv("filter_manager", filtered_data_list, filter)+ if (!is.element(id_pickme, names(observers))) { |
||
104 | -+ | |||
271 | +! |
- })+ observers[[id_pickme]] <- observeEvent(input[[id_pickme]], { |
||
105 | +272 |
- }+ ### Begin restore procedure. ### |
||
106 | -+ | |||
273 | +! |
-
+ snapshot <- snapshot_history()[[s]] |
||
107 | -+ | |||
274 | +! |
- #' @rdname module_filter_manager+ snapshot_state <- as.teal_slices(snapshot) |
||
108 | -+ | |||
275 | +! |
- filter_manager_ui <- function(id) {+ mapping_unfolded <- unfold_mapping(attr(snapshot_state, "mapping"), names(filtered_data_list)) |
||
109 | +276 | ! |
- ns <- NS(id)+ mapply( |
|
110 | +277 | ! |
- div(+ function(filtered_data, filter_ids) { |
|
111 | +278 | ! |
- class = "filter_manager_content",+ filtered_data$clear_filter_states(force = TRUE) |
|
112 | +279 | ! |
- tableOutput(ns("slices_table")),+ slices <- Filter(function(x) x$id %in% filter_ids, snapshot_state) |
|
113 | +280 | ! |
- snapshot_manager_ui(ns("snapshot_manager"))+ filtered_data$set_filter_state(slices) |
|
114 | +281 |
- )+ }, |
||
115 | -+ | |||
282 | +! |
- }+ filtered_data = filtered_data_list, |
||
116 | -+ | |||
283 | +! |
-
+ filter_ids = mapping_unfolded |
||
117 | +284 |
- #' @rdname module_filter_manager+ ) |
||
118 | -+ | |||
285 | +! |
- filter_manager_srv <- function(id, filtered_data_list, filter) {+ slices_global(snapshot_state) |
||
119 | -5x | +|||
286 | +! |
- moduleServer(id, function(input, output, session) {+ removeModal() |
||
120 | -5x | +|||
287 | +
- logger::log_trace("filter_manager_srv initializing for: { paste(names(filtered_data_list), collapse = ', ')}.")+ ### End restore procedure. ### |
|||
121 | +288 |
-
+ }) |
||
122 | -5x | +|||
289 | +
- is_module_specific <- isTRUE(attr(filter, "module_specific"))+ } |
|||
123 | +290 |
-
+ # Create handler for downloading snapshot. |
||
124 | -+ | |||
291 | +! |
- # Create a global list of slices.+ if (!is.element(id_saveme, names(handlers))) { |
||
125 | -+ | |||
292 | +! |
- # Contains all available teal_slice objects available to all modules.+ output[[id_saveme]] <- downloadHandler( |
||
126 | -+ | |||
293 | +! |
- # Passed whole to instances of FilteredData used for individual modules.+ filename = function() {+ |
+ ||
294 | +! | +
+ sprintf("teal_snapshot_%s_%s.json", s, Sys.Date()) |
||
127 | +295 |
- # Down there a subset that pertains to the data sets used in that module is applied and displayed.+ }, |
||
128 | -5x | +|||
296 | +! |
- slices_global <- reactiveVal(filter)+ content = function(file) { |
||
129 | -+ | |||
297 | +! |
-
+ snapshot <- snapshot_history()[[s]] |
||
130 | -5x | +|||
298 | +! |
- filtered_data_list <-+ snapshot_state <- as.teal_slices(snapshot) |
||
131 | -5x | +|||
299 | +! |
- if (!is_module_specific) {+ slices_store(tss = snapshot_state, file = file) |
||
132 | +300 |
- # Retrieve the first FilteredData from potentially nested list.+ } |
||
133 | +301 |
- # List of length one is named "global_filters" because that name is forbidden for a module label.+ ) |
||
134 | -4x | +|||
302 | +! |
- list(global_filters = unlist(filtered_data_list)[[1]])+ handlers[[id_saveme]] <- id_saveme |
||
135 | +303 |
- } else {+ } |
||
136 | +304 |
- # Flatten potentially nested list of FilteredData objects while maintaining useful names.+ # Create a row for the snapshot table. |
||
137 | -+ | |||
305 | +! |
- # Simply using `unlist` would result in concatenated names.+ if (!is.element(id_rowme, names(divs))) { |
||
138 | -1x | +|||
306 | +! |
- flatten_nested <- function(x, name = NULL) {+ divs[[id_rowme]] <- div( |
||
139 | -5x | +|||
307 | +! |
- if (inherits(x, "FilteredData")) {+ class = "snapshot_table_row", |
||
140 | -3x | +|||
308 | +! |
- setNames(list(x), name)+ span(h5(s)), |
||
141 | -+ | |||
309 | +! |
- } else {+ actionLink(inputId = ns(id_pickme), label = icon("circle-check"), title = "select"), |
||
142 | -2x | +|||
310 | +! |
- unlist(lapply(names(x), function(name) flatten_nested(x[[name]], name)))+ downloadLink(outputId = ns(id_saveme), label = icon("save"), title = "save to file") |
||
143 | +311 |
- }+ ) |
||
144 | +312 |
} |
||
145 | -1x | -
- flatten_nested(filtered_data_list)- |
- ||
146 | +313 |
- }+ }) |
||
147 | +314 |
-
+ }) |
||
148 | +315 |
- # Create mapping of filters to modules in matrix form (presented as data.frame).+ |
||
149 | +316 |
- # Modules get NAs for filters that cannot be set for them.+ # Create table to display list of snapshots and their actions. |
||
150 | -5x | +317 | +6x |
- mapping_matrix <- reactive({+ output$snapshot_list <- renderUI({ |
151 | -5x | +318 | +2x |
- state_ids_global <- vapply(slices_global(), `[[`, character(1L), "id")+ rows <- lapply(rev(reactiveValuesToList(divs)), function(d) d) |
152 | -5x | +319 | +2x |
- mapping_smooth <- lapply(filtered_data_list, function(x) {+ if (length(rows) == 0L) { |
153 | -7x | +320 | +2x |
- state_ids_local <- vapply(x$get_filter_state(), `[[`, character(1L), "id")+ div( |
154 | -7x | +321 | +2x |
- state_ids_allowed <- vapply(x$get_available_teal_slices()(), `[[`, character(1L), "id")+ class = "snapshot_manager_placeholder", |
155 | -7x | +322 | +2x |
- states_active <- state_ids_global %in% state_ids_local+ "Snapshots will appear here." |
156 | -7x | +|||
323 | +
- ifelse(state_ids_global %in% state_ids_allowed, states_active, NA)+ ) |
|||
157 | +324 |
- })+ } else { |
||
158 | -+ | |||
325 | +! |
-
+ rows |
||
159 | -5x | +|||
326 | +
- as.data.frame(mapping_smooth, row.names = state_ids_global, check.names = FALSE)+ } |
|||
160 | +327 |
}) |
||
161 | +328 | - - | -||
162 | -5x | -
- output$slices_table <- renderTable(- |
- ||
163 | -5x | -
- expr = {+ }) |
||
164 | +329 |
- # Display logical values as UTF characters.- |
- ||
165 | -2x | -
- mm <- mapping_matrix()- |
- ||
166 | -2x | -
- mm[] <- lapply(mm, ifelse, yes = intToUtf8(9989), no = intToUtf8(10060))- |
- ||
167 | -2x | -
- mm[] <- lapply(mm, function(x) ifelse(is.na(x), intToUtf8(128306), x))- |
- ||
168 | -2x | -
- if (!is_module_specific) colnames(mm) <- "Global Filters"+ } |
||
169 | +330 | |||
170 | +331 |
- # Display placeholder if no filters defined.- |
- ||
171 | -2x | -
- if (nrow(mm) == 0L) {- |
- ||
172 | -2x | -
- mm <- data.frame(`Filter manager` = "No filters specified.", check.names = FALSE)- |
- ||
173 | -2x | -
- rownames(mm) <- ""+ ### utility functions ---- |
||
174 | +332 |
- }+ |
||
175 | +333 |
-
+ #' Explicitly enumerate global filters. |
||
176 | +334 |
- # Report Previewer will not be displayed.- |
- ||
177 | -2x | -
- mm[names(mm) != "Report previewer"]+ #' |
||
178 | +335 |
- },- |
- ||
179 | -5x | -
- align = paste(c("l", rep("c", sum(names(filtered_data_list) != "Report previewer"))), collapse = ""),+ #' Transform module mapping such that global filters are explicitly specified for every module. |
||
180 | -5x | +|||
336 | +
- rownames = TRUE+ #' |
|||
181 | +337 |
- )+ #' @param mapping (named `list`) as stored in mapping parameter of `teal_slices` |
||
182 | +338 |
-
+ #' @param module_names (`character`) vector containing names of all modules in the app |
||
183 | +339 |
- # Create list of module calls.+ #' @return A `named_list` with one element per module, each element containing all filters applied to that module. |
||
184 | -5x | +|||
340 | +
- modules_out <- lapply(names(filtered_data_list), function(module_name) {+ #' @keywords internal |
|||
185 | -7x | +|||
341 | +
- filter_manager_module_srv(+ #' |
|||
186 | -7x | +|||
342 | +
- id = module_name,+ unfold_mapping <- function(mapping, module_names) { |
|||
187 | -7x | +|||
343 | +! |
- module_fd = filtered_data_list[[module_name]],+ module_names <- structure(module_names, names = module_names) |
||
188 | -7x | +|||
344 | +! |
- slices_global = slices_global+ lapply(module_names, function(x) c(mapping[[x]], mapping[["global_filters"]])) |
||
189 | +345 |
- )+ } |
||
190 | +346 |
- })+ |
||
191 | +347 |
-
+ #' Convert mapping matrix to filter mapping specification. |
||
192 | +348 |
- # Call snapshot manager.+ #' |
||
193 | -5x | +|||
349 | +
- snapshot_manager_srv("snapshot_manager", slices_global, mapping_matrix, filtered_data_list)+ #' Transform a mapping matrix, i.e. a data frame that maps each filter state to each module, |
|||
194 | +350 |
-
+ #' to a list specification like the one used in the `mapping` attribute of `teal_slices`. |
||
195 | -5x | +|||
351 | +
- modules_out # returned for testing purpose+ #' Global filters are gathered in one list element. |
|||
196 | +352 |
- })+ #' If a module has no active filters but the global ones, it will not be mentioned in the output. |
||
197 | +353 |
- }+ #' |
||
198 | +354 |
-
+ #' @param mapping_matrix (`data.frame`) of logical vectors where |
||
199 | +355 |
- #' Module specific filter manager+ #' columns represent modules and row represent `teal_slice`s |
||
200 | +356 |
- #'+ #' @return Named `list` like that in the `mapping` attribute of a `teal_slices` object. |
||
201 | +357 |
- #' Tracks filter states in a single module.+ #' @keywords internal |
||
202 | +358 |
#' |
||
203 | +359 |
- #' This module tracks the state of a single `FilteredData` object and global `teal_slices`+ matrix_to_mapping <- function(mapping_matrix) { |
||
204 | -+ | |||
360 | +! |
- #' and updates both objects as necessary. Filter states added in different modules+ mapping_matrix[] <- lapply(mapping_matrix, function(x) x | is.na(x))+ |
+ ||
361 | +! | +
+ global <- vapply(as.data.frame(t(mapping_matrix)), all, logical(1L))+ |
+ ||
362 | +! | +
+ global_filters <- names(global[global])+ |
+ ||
363 | +! | +
+ local_filters <- mapping_matrix[!rownames(mapping_matrix) %in% global_filters, ] |
||
205 | +364 |
- #' Filter states added any individual module are added to global `teal_slices`+ + |
+ ||
365 | +! | +
+ mapping <- c(lapply(local_filters, function(x) rownames(local_filters)[x]), list(global_filters = global_filters))+ |
+ ||
366 | +! | +
+ Filter(function(x) length(x) != 0L, mapping) |
||
206 | +367 |
- #' and from there become available in other modules+ } |
207 | +1 |
- #' by setting `private$available_teal_slices` in each `FilteredData`.+ #' @title `TealReportCard` |
||
208 | +2 |
- #'+ #' @description `r lifecycle::badge("experimental")` |
||
209 | +3 |
- #' @param id (`character(1)`)+ #' Child class of [`ReportCard`] that is used for `teal` specific applications. |
||
210 | +4 |
- #' `shiny` module id.+ #' In addition to the parent methods, it supports rendering `teal` specific elements such as |
||
211 | +5 |
- #' @param module_fd (`FilteredData`)+ #' the source code, the encodings panel content and the filter panel content as part of the |
||
212 | +6 |
- #' Object containing the data to be filtered in a single `teal` module.+ #' meta data. |
||
213 | +7 |
- #' @param slices_global (`reactiveVal`)+ #' @export |
||
214 | +8 |
- #' stores `teal_slices` with all available filters; allows the following actions:+ #' |
||
215 | +9 |
- #' - to disable/enable a specific filter in a module+ TealReportCard <- R6::R6Class( # nolint: object_name_linter. |
||
216 | +10 |
- #' - to restore saved filter settings+ classname = "TealReportCard", |
||
217 | +11 |
- #' - to save current filter panel settings+ inherit = teal.reporter::ReportCard, |
||
218 | +12 |
- #' @return A `reactive` expression containing the slices active in this module.+ public = list( |
||
219 | +13 |
- #' @keywords internal+ #' @description Appends the source code to the `content` meta data of this `TealReportCard`. |
||
220 | +14 |
- #'+ #' |
||
221 | +15 |
- filter_manager_module_srv <- function(id, module_fd, slices_global) {+ #' @param src (`character(1)`) code as text. |
||
222 | -7x | +|||
16 | +
- moduleServer(id, function(input, output, session) {+ #' @param ... any `rmarkdown` `R` chunk parameter and its value. |
|||
223 | +17 |
- # Only operate on slices that refer to data sets present in this module.+ #' But `eval` parameter is always set to `FALSE`. |
||
224 | -7x | +|||
18 | +
- module_fd$set_available_teal_slices(reactive(slices_global()))+ #' @return Object of class `TealReportCard`, invisibly. |
|||
225 | +19 |
-
+ #' @examples |
||
226 | +20 |
- # Track filter state of this module.+ #' card <- TealReportCard$new()$append_src( |
||
227 | -7x | +|||
21 | +
- slices_module <- reactive(module_fd$get_filter_state())+ #' "plot(iris)" |
|||
228 | +22 |
-
+ #' ) |
||
229 | +23 |
- # Reactive values for comparing states.+ #' card$get_content()[[1]]$get_content() |
||
230 | -7x | +|||
24 | +
- previous_slices <- reactiveVal(isolate(slices_module()))+ append_src = function(src, ...) { |
|||
231 | -7x | +25 | +4x |
- slices_added <- reactiveVal(NULL)+ checkmate::assert_character(src, min.len = 0, max.len = 1) |
232 | -+ | |||
26 | +4x |
-
+ params <- list(...) |
||
233 | -+ | |||
27 | +4x |
- # Observe changes in module filter state and trigger appropriate actions.+ params$eval <- FALSE |
||
234 | -7x | +28 | +4x |
- observeEvent(slices_module(), ignoreNULL = FALSE, {+ rblock <- RcodeBlock$new(src) |
235 | -2x | +29 | +4x |
- logger::log_trace("filter_manager_srv@1 detecting states deltas in module: { id }.")+ rblock$set_params(params) |
236 | -2x | +30 | +4x |
- added <- setdiff_teal_slices(slices_module(), slices_global())+ self$append_content(rblock) |
237 | -! | +|||
31 | +4x |
- if (length(added)) slices_added(added)+ self$append_metadata("SRC", src) |
||
238 | -2x | +32 | +4x |
- previous_slices(slices_module())+ invisible(self) |
239 | +33 |
- })+ }, |
||
240 | +34 |
-
+ #' @description Appends the filter state list to the `content` and `metadata` of this `TealReportCard`. |
||
241 | -7x | +|||
35 | +
- observeEvent(slices_added(), ignoreNULL = TRUE, {+ #' If the filter state list has an attribute named `formatted`, it appends it to the card otherwise it uses |
|||
242 | -! | +|||
36 | +
- logger::log_trace("filter_manager_srv@2 added filter in module: { id }.")+ #' the default `yaml::as.yaml` to format the list. |
|||
243 | +37 |
- # In case the new state has the same id as an existing state, add a suffix to it.+ #' If the filter state list is empty, nothing is appended to the `content`. |
||
244 | -! | +|||
38 | +
- global_ids <- vapply(slices_global(), `[[`, character(1L), "id")+ #' |
|||
245 | -! | -
- lapply(- |
- ||
246 | -! | -
- slices_added(),- |
- ||
247 | -! | -
- function(slice) {- |
- ||
248 | -! | -
- if (slice$id %in% global_ids) {- |
- ||
249 | -! | +|||
39 | +
- slice$id <- utils::tail(make.unique(c(global_ids, slice$id), sep = "_"), 1)+ #' @param fs (`teal_slices`) object returned from [teal_slices()] function. |
|||
250 | +40 |
- }+ #' @return `self`, invisibly. |
||
251 | +41 |
- }+ append_fs = function(fs) { |
||
252 | -+ | |||
42 | +5x |
- )+ checkmate::assert_class(fs, "teal_slices") |
||
253 | -! | +|||
43 | +4x |
- slices_global_new <- c(slices_global(), slices_added())+ self$append_text("Filter State", "header3") |
||
254 | -! | +|||
44 | +4x |
- slices_global(slices_global_new)+ self$append_content(TealSlicesBlock$new(fs)) |
||
255 | -! | +|||
45 | +4x |
- slices_added(NULL)+ invisible(self) |
||
256 | +46 |
- })+ }, |
||
257 | +47 |
-
+ #' @description Appends the encodings list to the `content` and `metadata` of this `TealReportCard`. |
||
258 | -7x | +|||
48 | +
- slices_module # returned for testing purpose+ #' |
|||
259 | +49 |
- })+ #' @param encodings (`list`) list of encodings selections of the `teal` app. |
||
260 | +50 |
- }+ #' @return `self`, invisibly. |
1 | +51 |
- # This module is the main teal module that puts everything together.+ #' @examples |
||
2 | +52 |
-
+ #' card <- TealReportCard$new()$append_encodings(list(variable1 = "X")) |
||
3 | +53 |
- #' `teal` main app module+ #' card$get_content()[[1]]$get_content() |
||
4 | +54 |
- #'+ #' |
||
5 | +55 |
- #' This is the main `teal` app that puts everything together.+ append_encodings = function(encodings) { |
||
6 | -+ | |||
56 | +4x |
- #'+ checkmate::assert_list(encodings) |
||
7 | -+ | |||
57 | +4x |
- #' It displays the splash UI which is used to fetch the data, possibly+ self$append_text("Selected Options", "header3") |
||
8 | -+ | |||
58 | +4x |
- #' prompting for a password input to fetch the data. Once the data is ready,+ if (requireNamespace("yaml", quietly = TRUE)) { |
||
9 | -+ | |||
59 | +4x |
- #' the splash screen is replaced by the actual `teal` UI that is tabsetted and+ self$append_text(yaml::as.yaml(encodings, handlers = list( |
||
10 | -+ | |||
60 | +4x |
- #' has a filter panel with `datanames` that are relevant for the current tab.+ POSIXct = function(x) format(x, "%Y-%m-%d"), |
||
11 | -+ | |||
61 | +4x |
- #' Nested tabs are possible, but we limit it to two nesting levels for reasons+ POSIXlt = function(x) format(x, "%Y-%m-%d"), |
||
12 | -+ | |||
62 | +4x |
- #' of clarity of the UI.+ Date = function(x) format(x, "%Y-%m-%d") |
||
13 | -+ | |||
63 | +4x |
- #'+ )), "verbatim") |
||
14 | +64 |
- #' The splash screen functionality can also be used+ } else { |
||
15 | -+ | |||
65 | +! |
- #' for non-delayed data which takes time to load into memory, avoiding+ stop("yaml package is required to format the encodings list") |
||
16 | +66 |
- #' `shiny` session timeouts.+ } |
||
17 | -+ | |||
67 | +4x |
- #'+ self$append_metadata("Encodings", encodings) |
||
18 | -+ | |||
68 | +4x |
- #' Server evaluates the `teal_data_rv` (delayed data mechanism) and creates the+ invisible(self) |
||
19 | +69 |
- #' `datasets` object that is shared across modules.+ } |
||
20 | +70 |
- #' Once it is ready and non-`NULL`, the splash screen is replaced by the+ ), |
||
21 | +71 |
- #' main `teal` UI that depends on the data.+ private = list() |
||
22 | +72 |
- #' The currently active tab is tracked and the right filter panel+ ) |
||
23 | +73 |
- #' updates the displayed datasets to filter for according to the active `datanames`+ |
||
24 | +74 |
- #' of the tab.+ #' @title `RcodeBlock` |
||
25 | +75 |
- #'+ #' @keywords internal |
||
26 | +76 |
- #' It is written as a `shiny` module so it can be added into other apps as well.+ TealSlicesBlock <- R6::R6Class( # nolint: object_name_linter. |
||
27 | +77 |
- #'+ classname = "TealSlicesBlock", |
||
28 | +78 |
- #' @name module_teal+ inherit = teal.reporter:::TextBlock, |
||
29 | +79 |
- #'+ public = list( |
||
30 | +80 |
- #' @inheritParams module_teal_with_splash+ #' @description Returns a `TealSlicesBlock` object. |
||
31 | +81 |
- #'+ #' |
||
32 | +82 |
- #' @param splash_ui (`shiny.tag`) UI to display initially,+ #' @details Returns a `TealSlicesBlock` object with no content and no parameters. |
||
33 | +83 |
- #' can be a splash screen or a `shiny` module UI. For the latter, see+ #' |
||
34 | +84 |
- #' [init()] about how to call the corresponding server function.+ #' @param content (`teal_slices`) object returned from [teal_slices()] function. |
||
35 | +85 |
- #'+ #' @param style (`character(1)`) string specifying style to apply. |
||
36 | +86 |
- #' @param teal_data_rv (`reactive`)+ #' |
||
37 | +87 |
- #' returns the `teal_data`, only evaluated once, `NULL` value is ignored+ #' @return Object of class `TealSlicesBlock`, invisibly. |
||
38 | +88 |
- #'+ #' |
||
39 | +89 |
- #' @return+ initialize = function(content = teal_slices(), style = "verbatim") { |
||
40 | -+ | |||
90 | +10x |
- #' Returns a `reactive` expression which returns the currently active module.+ self$set_content(content) |
||
41 | -+ | |||
91 | +9x |
- #'+ self$set_style(style) |
||
42 | -+ | |||
92 | +9x |
- #' @examples+ invisible(self) |
||
43 | +93 |
- #' # use non-exported function from teal+ }, |
||
44 | +94 |
- #' ui_teal <- getFromNamespace("ui_teal", "teal")+ |
||
45 | +95 |
- #' srv_teal <- getFromNamespace("srv_teal", "teal")+ #' @description Sets content of this `TealSlicesBlock`. |
||
46 | +96 |
- #'+ #' Sets content as `YAML` text which represents a list generated from `teal_slices`. |
||
47 | +97 |
- #' mods <- modules(+ #' The list displays limited number of fields from `teal_slice` objects, but this list is |
||
48 | +98 |
- #' label = "example app",+ #' sufficient to conclude which filters were applied. |
||
49 | +99 |
- #' example_module(label = "example dataset", datanames = c("iris", "mtcars"))+ #' When `selected` field in `teal_slice` object is a range, then it is displayed as a "min" |
||
50 | +100 |
- #' )+ #' |
||
51 | +101 |
- #'+ #' |
||
52 | +102 |
- #' teal_data_rv <- reactive(teal_data(iris = iris, mtcars = mtcars))+ #' @param content (`teal_slices`) object returned from [teal_slices()] function. |
||
53 | +103 |
- #'+ #' @return `self`, invisibly. |
||
54 | +104 |
- #' ui <- function() {+ set_content = function(content) { |
||
55 | -+ | |||
105 | +11x |
- #' ui_teal("dummy")+ checkmate::assert_class(content, "teal_slices") |
||
56 | -+ | |||
106 | +10x |
- #' }+ if (length(content) != 0) { |
||
57 | -+ | |||
107 | +7x |
- #'+ states_list <- lapply(content, function(x) { |
||
58 | -+ | |||
108 | +7x |
- #' server <- function(input, output, session) {+ x_list <- shiny::isolate(as.list(x)) |
||
59 | -+ | |||
109 | +7x |
- #' active_module <- srv_teal(id = "dummy", modules = mods, teal_data_rv = teal_data_rv)+ if ( |
||
60 | -+ | |||
110 | +7x |
- #' }+ inherits(x_list$choices, c("integer", "numeric", "Date", "POSIXct", "POSIXlt")) && |
||
61 | -+ | |||
111 | +7x |
- #'+ length(x_list$choices) == 2 && |
||
62 | -+ | |||
112 | +7x |
- #' if (interactive()) {+ length(x_list$selected) == 2 |
||
63 | +113 |
- #' shinyApp(ui, server)+ ) { |
||
64 | -+ | |||
114 | +! |
- #' }+ x_list$range <- paste(x_list$selected, collapse = " - ") |
||
65 | -+ | |||
115 | +! |
- #'+ x_list["selected"] <- NULL |
||
66 | +116 |
- #' @keywords internal+ } |
||
67 | -+ | |||
117 | +7x |
- #'+ if (!is.null(x_list$arg)) { |
||
68 | -+ | |||
118 | +! |
- NULL+ x_list$arg <- if (x_list$arg == "subset") "Genes" else "Samples" |
||
69 | +119 |
-
+ } |
||
70 | +120 |
- #' @rdname module_teal+ |
||
71 | -+ | |||
121 | +7x |
- ui_teal <- function(id,+ x_list <- x_list[ |
||
72 | -+ | |||
122 | +7x |
- splash_ui = tags$h2("Starting the Teal App"),+ c("dataname", "varname", "experiment", "arg", "expr", "selected", "range", "keep_na", "keep_inf") |
||
73 | +123 |
- title = build_app_title(),+ ] |
||
74 | -+ | |||
124 | +7x |
- header = tags$p(),+ names(x_list) <- c( |
||
75 | -- |
- footer = tags$p()) {+ | ||
125 | +7x | +
+ "Dataset name", "Variable name", "Experiment", "Filtering by", "Applied expression", |
||
76 | +126 | 7x |
- checkmate::assert_character(id, max.len = 1, any.missing = FALSE)+ "Selected Values", "Selected range", "Include NA values", "Include Inf values" |
|
77 | +127 | ++ |
+ )+ |
+ |
128 | ||||
78 | +129 | 7x |
- checkmate::assert_multi_class(splash_ui, c("shiny.tag", "shiny.tag.list", "html"))+ Filter(Negate(is.null), x_list) |
|
79 | +130 | ++ |
+ })+ |
+ |
131 | ||||
80 | +132 | 7x |
- if (is.character(title)) {+ if (requireNamespace("yaml", quietly = TRUE)) { |
|
81 | -! | +|||
133 | +7x |
- title <- build_app_title(title)+ super$set_content(yaml::as.yaml(states_list)) |
||
82 | +134 |
- } else {+ } else { |
||
83 | -7x | +|||
135 | +! |
- validate_app_title_tag(title)+ stop("yaml package is required to format the filter state list") |
||
84 | +136 |
- }+ } |
||
85 | +137 | - - | -||
86 | -7x | -
- checkmate::assert(+ } |
||
87 | -7x | +138 | +10x |
- .var.name = "header",+ private$teal_slices <- content |
88 | -7x | +139 | +10x |
- checkmate::check_string(header),+ invisible(self) |
89 | -7x | +|||
140 | +
- checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html"))+ }, |
|||
90 | +141 |
- )+ #' @description Create the `RcodeBlock` from a list. |
||
91 | -7x | +|||
142 | +
- if (checkmate::test_string(header)) {+ #' @param x (named `list`) with two fields `c("text", "params")`. |
|||
92 | -! | +|||
143 | +
- header <- tags$p(header)+ #' Use the `get_available_params` method to get all possible parameters. |
|||
93 | +144 |
- }+ #' @return `self`, invisibly. |
||
94 | +145 |
-
+ from_list = function(x) { |
||
95 | -7x | +146 | +1x |
- checkmate::assert(+ checkmate::assert_list(x) |
96 | -7x | +147 | +1x |
- .var.name = "footer",+ checkmate::assert_names(names(x), must.include = c("teal_slices")) |
97 | -7x | +148 | +1x |
- checkmate::check_string(footer),+ self$set_content(x$teal_slices) |
98 | -7x | +149 | +1x |
- checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html"))+ invisible(self) |
99 | +150 |
- )+ }, |
||
100 | -7x | +|||
151 | +
- if (checkmate::test_string(footer)) {+ #' @description Convert the `RcodeBlock` to a list. |
|||
101 | -! | +|||
152 | +
- footer <- tags$p(footer)+ #' @return named `list` with a text and `params`. |
|||
102 | +153 |
- }+ |
||
103 | +154 |
-
+ to_list = function() { |
||
104 | -7x | +155 | +2x |
- ns <- NS(id)+ list(teal_slices = private$teal_slices) |
105 | +156 |
-
+ } |
||
106 | +157 |
- # Once the data is loaded, we will remove this element and add the real teal UI instead+ ), |
||
107 | -7x | +|||
158 | +
- splash_ui <- div(+ private = list( |
|||
108 | +159 |
- # id so we can remove the splash screen once ready, which is the first child of this container+ style = "verbatim", |
||
109 | -7x | +|||
160 | +
- id = ns("main_ui_container"),+ teal_slices = NULL # teal_slices |
|||
110 | +161 |
- # we put it into a div, so it can easily be removed as a whole, also when it is a tagList (and not+ ) |
||
111 | +162 |
- # just the first item of the tagList)+ ) |
||
112 | -7x | +
1 | +
- div(splash_ui)+ #' Add right filter panel into each of the top-level `teal_modules` UIs. |
|||
113 | +2 |
- )+ #' |
||
114 | +3 |
-
+ #' The [ui_nested_tabs] function returns a nested tabbed UI corresponding |
||
115 | +4 |
- # show busy icon when `shiny` session is busy computing stuff+ #' to the nested modules. |
||
116 | +5 |
- # based on https://stackoverflow.com/questions/17325521/r-shiny-display-loading-message-while-function-is-running/22475216#22475216 #nolint+ #' This function adds the right filter panel to each main tab. |
||
117 | -7x | +|||
6 | +
- shiny_busy_message_panel <- conditionalPanel(- |
- |||
118 | -7x | -
- condition = "(($('html').hasClass('shiny-busy')) && (document.getElementById('shiny-notification-panel') == null))", # nolint- |
- ||
119 | -7x | -
- div(- |
- ||
120 | -7x | -
- icon("arrows-rotate", "spin fa-spin"),- |
- ||
121 | -7x | -
- "Computing ...",+ #' |
||
122 | +7 |
- # CSS defined in `custom.css`- |
- ||
123 | -7x | -
- class = "shinybusymessage"+ #' The right filter panel's filter choices affect the `datasets` object. Therefore, |
||
124 | +8 |
- )+ #' all modules using the same `datasets` share the same filters. |
||
125 | +9 |
- )+ #' |
||
126 | +10 |
-
+ #' This works with nested modules of depth greater than 2, though the filter |
||
127 | -7x | +|||
11 | +
- fluidPage(+ #' panel is inserted at the right of the modules at depth 1 and not at the leaves. |
|||
128 | -7x | +|||
12 | +
- title = title,+ #' |
|||
129 | -7x | +|||
13 | +
- theme = get_teal_bs_theme(),+ #' @name module_tabs_with_filters |
|||
130 | -7x | +|||
14 | +
- include_teal_css_js(),+ #' |
|||
131 | -7x | +|||
15 | +
- tags$header(header),+ #' @inheritParams module_teal |
|||
132 | -7x | +|||
16 | +
- tags$hr(class = "my-2"),+ #' |
|||
133 | -7x | +|||
17 | +
- shiny_busy_message_panel,+ #' @param datasets (named `list` of `FilteredData`) |
|||
134 | -7x | +|||
18 | +
- splash_ui,+ #' object to store filter state and filtered datasets, shared across modules. For more |
|||
135 | -7x | +|||
19 | +
- tags$hr(),+ #' details see [`teal.slice::FilteredData`]. Structure of the list must be the same as structure |
|||
136 | -7x | +|||
20 | +
- tags$footer(+ #' of the `modules` argument and list names must correspond to the labels in `modules`. |
|||
137 | -7x | +|||
21 | +
- div(+ #' When filter is not module-specific then list contains the same object in all elements. |
|||
138 | -7x | +|||
22 | +
- footer,+ #' @param reporter (`Reporter`) object from `teal.reporter` |
|||
139 | -7x | +|||
23 | +
- teal.widgets::verbatim_popup_ui(ns("sessionInfo"), "Session Info", type = "link"),+ #' |
|||
140 | -7x | +|||
24 | +
- textOutput(ns("identifier"))+ #' @return |
|||
141 | +25 |
- )+ #' A `shiny.tag.list` containing the main menu, placeholders for filters and placeholders for the `teal` modules. |
||
142 | +26 |
- )+ #' |
||
143 | +27 |
- )+ #' @keywords internal |
||
144 | +28 |
- }+ #' |
||
145 | +29 |
-
+ NULL |
||
146 | +30 | |||
147 | +31 |
- #' @rdname module_teal+ #' @rdname module_tabs_with_filters |
||
148 | +32 |
- srv_teal <- function(id, modules, teal_data_rv, filter = teal_slices()) {+ ui_tabs_with_filters <- function(id, modules, datasets, filter = teal_slices()) { |
||
149 | -19x | +|||
33 | +! |
- stopifnot(is.reactive(teal_data_rv))+ checkmate::assert_class(modules, "teal_modules") |
||
150 | -18x | +|||
34 | +! |
- moduleServer(id, function(input, output, session) {+ checkmate::assert_list(datasets, types = c("list", "FilteredData")) |
||
151 | -18x | +|||
35 | +! |
- logger::log_trace("srv_teal initializing the module.")+ checkmate::assert_class(filter, "teal_slices") |
||
152 | +36 | |||
153 | -18x | -
- output$identifier <- renderText(- |
- ||
154 | -18x | +|||
37 | +! |
- paste0("Pid:", Sys.getpid(), " Token:", substr(session$token, 25, 32))+ ns <- NS(id) |
||
155 | -+ | |||
38 | +! |
- )+ is_module_specific <- isTRUE(attr(filter, "module_specific")) |
||
156 | +39 | |||
157 | -18x | +|||
40 | +! |
- teal.widgets::verbatim_popup_srv(+ teal_ui <- ui_nested_tabs(ns("root"), modules = modules, datasets, is_module_specific = is_module_specific) |
||
158 | -18x | +|||
41 | +! |
- "sessionInfo",+ filter_panel_btns <- tags$li( |
||
159 | -18x | +|||
42 | +! |
- verbatim_content = utils::capture.output(utils::sessionInfo()),+ class = "flex-grow", |
||
160 | -18x | +|||
43 | +! |
- title = "SessionInfo"+ tags$button( |
||
161 | -+ | |||
44 | +! |
- )+ class = "btn action-button filter_hamburger", # see sidebar.css for style filter_hamburger |
||
162 | -+ | |||
45 | +! |
-
+ href = "javascript:void(0)", |
||
163 | -+ | |||
46 | +! |
- # `JavaScript` code+ onclick = "toggleFilterPanel();", # see sidebar.js |
||
164 | -18x | +|||
47 | +! |
- run_js_files(files = "init.js")+ title = "Toggle filter panels", |
||
165 | -+ | |||
48 | +! |
-
+ icon("fas fa-bars") |
||
166 | +49 |
- # set timezone in shiny app+ ), |
||
167 | -+ | |||
50 | +! |
- # timezone is set in the early beginning so it will be available also+ filter_manager_modal_ui(ns("filter_manager")) |
||
168 | +51 |
- # for `DDL` and all shiny modules+ ) |
||
169 | -18x | +|||
52 | +! |
- get_client_timezone(session$ns)+ teal_ui$children[[1]] <- tagAppendChild(teal_ui$children[[1]], filter_panel_btns) |
||
170 | -18x | +|||
53 | +
- observeEvent(+ |
|||
171 | -18x | +|||
54 | +! |
- eventExpr = input$timezone,+ if (!is_module_specific) { |
||
172 | -18x | +|||
55 | +
- once = TRUE,+ # need to rearrange html so that filter panel is within tabset |
|||
173 | -18x | +|||
56 | +! |
- handlerExpr = {+ tabset_bar <- teal_ui$children[[1]] |
||
174 | +57 | ! |
- session$userData$timezone <- input$timezone+ teal_modules <- teal_ui$children[[2]] |
|
175 | +58 | ! |
- logger::log_trace("srv_teal@1 Timezone set to client's timezone: { input$timezone }.")+ filter_ui <- unlist(datasets)[[1]]$ui_filter_panel(ns("filter_panel")) |
|
176 | -+ | |||
59 | +! |
- }+ list( |
||
177 | -+ | |||
60 | +! |
- )+ tabset_bar, |
||
178 | -+ | |||
61 | +! |
-
+ tags$hr(class = "my-2"), |
||
179 | -18x | +|||
62 | +! |
- reporter <- teal.reporter::Reporter$new()+ fluidRow( |
||
180 | -18x | +|||
63 | +! |
- if (is_arg_used(modules, "reporter") && length(extract_module(modules, "teal_module_previewer")) == 0) {+ column(width = 9, teal_modules, class = "teal_primary_col"), |
||
181 | +64 | ! |
- modules <- append_module(modules, reporter_previewer_module())+ column(width = 3, filter_ui, class = "teal_secondary_col") |
|
182 | +65 |
- }+ ) |
||
183 | +66 |
-
+ ) |
||
184 | -18x | +|||
67 | +
- env <- environment()+ } else { |
|||
185 | -18x | +|||
68 | +! |
- datasets_reactive <- eventReactive(teal_data_rv(), {+ teal_ui |
||
186 | -4x | +|||
69 | +
- env$progress <- shiny::Progress$new(session)+ } |
|||
187 | -4x | +|||
70 | +
- env$progress$set(0.25, message = "Setting data")+ } |
|||
188 | +71 | |||
189 | +72 |
- # create a list of data following structure of the nested modules list structure.+ #' @rdname module_tabs_with_filters |
||
190 | +73 |
- # Because it's easier to unpack modules and datasets when they follow the same nested structure.+ srv_tabs_with_filters <- function(id, |
||
191 | -4x | +|||
74 | +
- datasets_singleton <- teal_data_to_filtered_data(teal_data_rv())+ datasets, |
|||
192 | +75 |
-
+ modules, |
||
193 | +76 |
- # Singleton starts with only global filters active.+ reporter = teal.reporter::Reporter$new(), |
||
194 | -4x | +|||
77 | +
- filter_global <- Filter(function(x) x$id %in% attr(filter, "mapping")$global_filters, filter)+ filter = teal_slices()) { |
|||
195 | -4x | -
- datasets_singleton$set_filter_state(filter_global)- |
- ||
196 | -+ | 78 | +5x |
-
+ checkmate::assert_class(modules, "teal_modules") |
197 | -4x | +79 | +5x |
- module_datasets <- function(modules) {+ checkmate::assert_list(datasets, types = c("list", "FilteredData")) |
198 | -18x | +80 | +5x |
- if (inherits(modules, "teal_modules")) {+ checkmate::assert_class(reporter, "Reporter") |
199 | -7x | +81 | +3x |
- datasets <- lapply(modules$children, module_datasets)+ checkmate::assert_class(filter, "teal_slices")+ |
+
82 | ++ | + | ||
200 | -7x | +83 | +3x |
- labels <- vapply(modules$children, `[[`, character(1), "label")+ moduleServer(id, function(input, output, session) { |
201 | -7x | +84 | +3x |
- names(datasets) <- labels+ logger::log_trace("srv_tabs_with_filters initializing the module.")+ |
+
85 | ++ | + | ||
202 | -7x | +86 | +3x |
- datasets+ is_module_specific <- isTRUE(attr(filter, "module_specific")) |
203 | -11x | +87 | +3x |
- } else if (isTRUE(attr(filter, "module_specific"))) {+ manager_out <- filter_manager_modal_srv("filter_manager", filtered_data_list = datasets, filter = filter) |
204 | +88 |
- # we should create FilteredData even if modules$datanames is null+ |
||
205 | -+ | |||
89 | +3x |
- # null controls a display of filter panel but data should be still passed+ active_module <- srv_nested_tabs( |
||
206 | +90 | 3x |
- datanames <- if (is.null(modules$datanames) || modules$datanames == "all") {+ id = "root", |
|
207 | +91 | 3x |
- include_parent_datanames(+ datasets = datasets, |
|
208 | +92 | 3x |
- teal_data_datanames(teal_data_rv()),+ modules = modules, |
|
209 | +93 | 3x |
- teal.data::join_keys(teal_data_rv())+ reporter = reporter,+ |
+ |
94 | +3x | +
+ is_module_specific = is_module_specific |
||
210 | +95 |
- )+ ) |
||
211 | +96 |
- } else {+ |
||
212 | -! | +|||
97 | +3x |
- modules$datanames+ if (!is_module_specific) { |
||
213 | -+ | |||
98 | +3x |
- }+ active_datanames <- reactive({ |
||
214 | -+ | |||
99 | +6x |
- # todo: subset teal_data to datanames+ if (identical(active_module()$datanames, "all")) { |
||
215 | -3x | +|||
100 | +! |
- datasets_module <- teal_data_to_filtered_data(teal_data_rv(), datanames = datanames)+ singleton$datanames() |
||
216 | +101 |
-
+ } else { |
||
217 | -+ | |||
102 | +5x |
- # set initial filters+ include_parent_datanames( |
||
218 | -+ | |||
103 | +5x |
- # - filtering filters for this module+ active_module()$datanames, |
||
219 | -3x | +104 | +5x |
- slices <- Filter(x = filter, f = function(x) {+ singleton$get_join_keys() |
220 | -! | +|||
105 | +
- x$id %in% unique(unlist(attr(filter, "mapping")[c(modules$label, "global_filters")])) &&+ ) |
|||
221 | -! | +|||
106 | +
- x$dataname %in% datanames+ } |
|||
222 | +107 |
- })+ }) |
||
223 | +108 | 3x |
- include_varnames <- attr(slices, "include_varnames")[names(attr(slices, "include_varnames")) %in% datanames]+ singleton <- unlist(datasets)[[1]] |
|
224 | +109 | 3x |
- exclude_varnames <- attr(slices, "exclude_varnames")[names(attr(slices, "exclude_varnames")) %in% datanames]+ singleton$srv_filter_panel("filter_panel", active_datanames = active_datanames)+ |
+ |
110 | ++ | + | ||
225 | +111 | 3x |
- slices$include_varnames <- include_varnames+ observeEvent( |
|
226 | +112 | 3x |
- slices$exclude_varnames <- exclude_varnames+ eventExpr = active_datanames(), |
|
227 | +113 | 3x |
- datasets_module$set_filter_state(slices)+ handlerExpr = { |
|
228 | -3x | +114 | +4x |
- datasets_module+ script <- if (length(active_datanames()) == 0 || is.null(active_datanames())) { |
229 | +115 |
- } else {+ # hide the filter panel and disable the burger button |
||
230 | -8x | +|||
116 | +! |
- datasets_singleton+ "handleNoActiveDatasets();" |
||
231 | +117 |
- }+ } else { |
||
232 | +118 |
- }+ # show the filter panel and enable the burger button |
||
233 | +119 | 4x |
- module_datasets(modules)+ "handleActiveDatasetsPresent();" |
|
234 | +120 |
- })+ } |
||
235 | -+ | |||
121 | +4x |
-
+ shinyjs::runjs(script) |
||
236 | +122 |
- # Replace splash / welcome screen once data is loaded ----+ }, |
||
237 | -+ | |||
123 | +3x |
- # ignoreNULL to not trigger at the beginning when data is NULL+ ignoreNULL = FALSE |
||
238 | +124 |
- # just handle it once because data obtained through delayed loading should+ ) |
||
239 | -- |
- # usually not change afterwards- |
- ||
240 | +125 |
- # if restored from bookmarked state, `filter` is ignored+ } |
||
241 | +126 | |||
242 | -18x | -
- observeEvent(datasets_reactive(), once = TRUE, {- |
- ||
243 | -! | +127 | +3x |
- logger::log_trace("srv_teal@5 setting main ui after data was pulled")+ showNotification("Data loaded - App fully started up") |
244 | -! | +|||
128 | +3x |
- on.exit(env$progress$close())+ logger::log_trace("srv_tabs_with_filters initialized the module") |
||
245 | -! | +|||
129 | +
- env$progress$set(0.5, message = "Setting up main UI")+ |
|||
246 | -! | +|||
130 | +3x |
- datasets <- datasets_reactive()+ active_module |
||
247 | +131 |
-
+ }) |
||
248 | +132 |
- # main_ui_container contains splash screen first and we remove it and replace it by the real UI- |
- ||
249 | -! | -
- removeUI(sprintf("#%s > div:nth-child(1)", session$ns("main_ui_container")))- |
- ||
250 | -! | -
- insertUI(- |
- ||
251 | -! | -
- selector = paste0("#", session$ns("main_ui_container")),- |
- ||
252 | -! | -
- where = "beforeEnd",+ } |
253 | +1 |
- # we put it into a div, so it can easily be removed as a whole, also when it is a tagList (and not+ #' Get client timezone |
|
254 | +2 |
- # just the first item of the tagList)- |
- |
255 | -! | -
- ui = div(ui_tabs_with_filters(+ #' |
|
256 | -! | +||
3 | +
- session$ns("main_ui"),+ #' User timezone in the browser may be different to the one on the server. |
||
257 | -! | +||
4 | +
- modules = modules,+ #' This script can be run to register a `shiny` input which contains information about the timezone in the browser. |
||
258 | -! | +||
5 | +
- datasets = datasets,+ #' |
||
259 | -! | +||
6 | +
- filter = filter+ #' @param ns (`function`) namespace function passed from the `session` object in the `shiny` server. |
||
260 | +7 |
- )),+ #' For `shiny` modules this will allow for proper name spacing of the registered input. |
|
261 | +8 |
- # needed so that the UI inputs are available and can be immediately updated, otherwise, updating may not+ #' |
|
262 | +9 |
- # have any effect as they are ignored when not present+ #' @return (`shiny`) input variable accessible with `input$tz` which is a (`character`) |
|
263 | -! | +||
10 | +
- immediate = TRUE+ #' string containing the timezone of the browser/client. |
||
264 | +11 |
- )+ #' |
|
265 | +12 |
-
+ #' @keywords internal |
|
266 | +13 |
- # must make sure that this is only executed once as modules assume their observers are only+ #' |
|
267 | +14 |
- # registered once (calling server functions twice would trigger observers twice each time)+ get_client_timezone <- function(ns) { |
|
268 | -! | +||
15 | +18x |
- srv_tabs_with_filters(+ script <- sprintf( |
|
269 | -! | +||
16 | +18x |
- id = "main_ui",+ "Shiny.setInputValue(`%s`, Intl.DateTimeFormat().resolvedOptions().timeZone)", |
|
270 | -! | +||
17 | +18x |
- datasets = datasets,+ ns("timezone") |
|
271 | -! | +||
18 | +
- modules = modules,+ ) |
||
272 | -! | +||
19 | +18x |
- reporter = reporter,+ shinyjs::runjs(script) # function does not return anything |
|
273 | -! | +||
20 | +18x |
- filter = filter+ invisible(NULL) |
|
274 | +21 |
- )+ } |
|
275 | +22 |
- })+ |
|
276 | +23 |
- })+ #' Resolve the expected bootstrap theme |
|
277 | +24 |
- }+ #' @noRd |
1 | +25 |
- setOldClass("teal_data_module")+ #' @keywords internal |
||
2 | +26 |
-
+ get_teal_bs_theme <- function() { |
||
3 | -+ | |||
27 | +11x |
- #' Evaluate code on `teal_data_module`+ bs_theme <- getOption("teal.bs_theme") |
||
4 | -+ | |||
28 | +11x |
- #'+ if (is.null(bs_theme)) { |
||
5 | -+ | |||
29 | +8x |
- #' @details+ NULL |
||
6 | -+ | |||
30 | +3x |
- #' `eval_code` evaluates given code in the environment of the `teal_data` object created by the `teal_data_module`.+ } else if (!inherits(bs_theme, "bs_theme")) { |
||
7 | -+ | |||
31 | +2x |
- #' The code is added to the `@code` slot of the `teal_data`.+ warning("teal.bs_theme has to be of a bslib::bs_theme class, the default shiny bootstrap is used.")+ |
+ ||
32 | +2x | +
+ NULL |
||
8 | +33 |
- #'+ } else {+ |
+ ||
34 | +1x | +
+ bs_theme |
||
9 | +35 |
- #' @param object (`teal_data_module`)+ } |
||
10 | +36 |
- #' @inheritParams teal.code::eval_code+ } |
||
11 | +37 |
- #'+ |
||
12 | +38 |
- #' @return+ #' Return parentnames along with datanames. |
||
13 | +39 |
- #' `eval_code` returns a `teal_data_module` object with a delayed evaluation of `code` when the module is run.+ #' @noRd |
||
14 | +40 |
- #'+ #' @keywords internal |
||
15 | +41 |
- #' @examples+ include_parent_datanames <- function(dataname, join_keys) {+ |
+ ||
42 | +11x | +
+ parents <- character(0)+ |
+ ||
43 | +11x | +
+ for (i in dataname) {+ |
+ ||
44 | +16x | +
+ while (length(i) > 0) {+ |
+ ||
45 | +18x | +
+ parent_i <- teal.data::parent(join_keys, i)+ |
+ ||
46 | +18x | +
+ parents <- c(parent_i, parents)+ |
+ ||
47 | +18x | +
+ i <- parent_i |
||
16 | +48 |
- #' eval_code(tdm, "dataset1 <- subset(dataset1, Species == 'virginica')")+ } |
||
17 | +49 |
- #'+ } |
||
18 | +50 |
- #' @include teal_data_module.R+ + |
+ ||
51 | +11x | +
+ unique(c(parents, dataname)) |
||
19 | +52 |
- #' @name eval_code+ } |
||
20 | +53 |
- #' @rdname teal_data_module+ |
||
21 | +54 |
- #' @aliases eval_code,teal_data_module,character-method+ #' Create a `FilteredData` |
||
22 | +55 |
- #' @aliases eval_code,teal_data_module,language-method+ #' |
||
23 | +56 |
- #' @aliases eval_code,teal_data_module,expression-method+ #' Create a `FilteredData` object from a `teal_data` object. |
||
24 | +57 |
#' |
||
25 | +58 |
- #' @importFrom methods setMethod+ #' @param x (`teal_data`) object |
||
26 | +59 |
- #' @importMethodsFrom teal.code eval_code+ #' @param datanames (`character`) vector of data set names to include; must be subset of `datanames(x)` |
||
27 | +60 |
- #'+ #' @return A `FilteredData` object. |
||
28 | +61 |
- setMethod("eval_code", signature = c("teal_data_module", "character"), function(object, code) {+ #' @keywords internal |
||
29 | -13x | +|||
62 | +
- teal_data_module(+ teal_data_to_filtered_data <- function(x, datanames = teal_data_datanames(x)) { |
|||
30 | +63 | 13x |
- ui = function(id) {- |
- |
31 | -1x | -
- ns <- NS(id)+ checkmate::assert_class(x, "teal_data") |
||
32 | -1x | +64 | +13x |
- object$ui(ns("mutate_inner"))+ checkmate::assert_character(datanames, min.chars = 1L, any.missing = FALSE) |
33 | +65 |
- },+ |
||
34 | +66 | 13x |
- server = function(id) {+ ans <- teal.slice::init_filtered_data( |
|
35 | -11x | +67 | +13x |
- moduleServer(id, function(input, output, session) {+ x = sapply(datanames, function(dn) x[[dn]], simplify = FALSE), |
36 | -11x | +68 | +13x |
- teal_data_rv <- object$server("mutate_inner")+ join_keys = teal.data::join_keys(x) |
37 | +69 |
-
+ ) |
||
38 | -11x | +|||
70 | +
- if (!is.reactive(teal_data_rv)) {+ # Piggy-back entire pre-processing code so that filtering code can be appended later. |
|||
39 | -1x | +71 | +13x |
- stop("The `teal_data_module` must return a reactive expression.", call. = FALSE)+ attr(ans, "preprocessing_code") <- teal.code::get_code(x) |
40 | -+ | |||
72 | +13x |
- }- |
- ||
41 | -- |
-
+ attr(ans, "verification_status") <- x@verified |
||
42 | -10x | +73 | +13x |
- td <- eventReactive(teal_data_rv(),+ ans |
43 | +74 |
- {+ } |
||
44 | -10x | +|||
75 | +
- if (inherits(teal_data_rv(), c("teal_data", "qenv.error"))) {+ |
|||
45 | -6x | +|||
76 | +
- eval_code(teal_data_rv(), code)+ #' Template function for `TealReportCard` creation and customization |
|||
46 | +77 |
- } else {+ #' |
||
47 | -4x | +|||
78 | +
- teal_data_rv()+ #' This function generates a report card with a title, |
|||
48 | +79 |
- }+ #' an optional description, and the option to append the filter state list. |
||
49 | +80 |
- },+ #' |
||
50 | -10x | +|||
81 | +
- ignoreNULL = FALSE+ #' @param title (`character(1)`) title of the card (unless overwritten by label) |
|||
51 | +82 |
- )+ #' @param label (`character(1)`) label provided by the user when adding the card |
||
52 | -10x | +|||
83 | +
- td+ #' @param description (`character(1)`) optional additional description |
|||
53 | +84 |
- })+ #' @param with_filter (`logical(1)`) flag indicating to add filter state |
||
54 | +85 |
- }+ #' @param filter_panel_api (`FilterPanelAPI`) object with API that allows the generation |
||
55 | +86 |
- )+ #' of the filter state in the report |
||
56 | +87 |
- })+ #' |
||
57 | +88 |
-
+ #' @return (`TealReportCard`) populated with a title, description and filter state. |
||
58 | +89 |
- setMethod("eval_code", signature = c("teal_data_module", "language"), function(object, code) {+ #' |
||
59 | -1x | +|||
90 | +
- eval_code(object, code = paste(lang2calls(code), collapse = "\n"))+ #' @export |
|||
60 | +91 |
- })+ report_card_template <- function(title, label, description = NULL, with_filter, filter_panel_api) { |
||
61 | -+ | |||
92 | +2x |
-
+ checkmate::assert_string(title) |
||
62 | -+ | |||
93 | +2x |
- setMethod("eval_code", signature = c("teal_data_module", "expression"), function(object, code) {+ checkmate::assert_string(label) |
||
63 | -6x | +94 | +2x |
- eval_code(object, code = paste(lang2calls(code), collapse = "\n"))+ checkmate::assert_string(description, null.ok = TRUE) |
64 | -+ | |||
95 | +2x |
- })+ checkmate::assert_flag(with_filter) |
1 | -+ | |||
96 | +2x |
- #' Store and restore `teal_slices` object+ checkmate::assert_class(filter_panel_api, classes = "FilterPanelAPI") |
||
2 | +97 |
- #'+ |
||
3 | -+ | |||
98 | +2x |
- #' Functions that write a `teal_slices` object to a file in the `JSON` format,+ card <- teal::TealReportCard$new() |
||
4 | -+ | |||
99 | +2x |
- #' and also restore the object from disk.+ title <- if (label == "") title else label |
||
5 | -+ | |||
100 | +2x |
- #'+ card$set_name(title) |
||
6 | -+ | |||
101 | +2x |
- #' Date and date time objects are stored in the following formats:+ card$append_text(title, "header2") |
||
7 | -+ | |||
102 | +1x |
- #'+ if (!is.null(description)) card$append_text(description, "header3") |
||
8 | -+ | |||
103 | +1x |
- #' - `Date` class is converted to the `"ISO8601"` standard (`YYYY-MM-DD`).+ if (with_filter) card$append_fs(filter_panel_api$get_filter_state()) |
||
9 | -+ | |||
104 | +2x |
- #' - `POSIX*t` classes are converted to character by using+ card |
||
10 | +105 |
- #' `format.POSIX*t(usetz = TRUE, tz = "UTC")` (`YYYY-MM-DD HH:MM:SS UTC`, where+ } |
||
11 | +106 |
- #' `UTC` is the `Coordinated Universal Time` timezone short-code).+ |
||
12 | +107 |
- #'+ #' Resolve `datanames` for the modules |
||
13 | +108 |
- #' This format is assumed during `slices_restore`. All `POSIX*t` objects in+ #' |
||
14 | +109 |
- #' `selected` or `choices` fields of `teal_slice` objects are always printed in+ #' Modifies `module$datanames` to include names of the parent dataset (taken from `join_keys`). |
||
15 | +110 |
- #' `UTC` timezone as well.+ #' When `datanames` is set to `"all"` it is replaced with all available datasets names. |
||
16 | +111 |
- #'+ #' @param modules (`teal_modules`) object |
||
17 | +112 |
- #' @param tss (`teal_slices`) object to be stored.+ #' @param datanames (`character`) names of datasets available in the `data` object |
||
18 | +113 |
- #' @param file (`character(1)`) file path where `teal_slices` object will be+ #' @param join_keys (`join_keys`) object |
||
19 | +114 |
- #' saved and restored. The file extension should be `".json"`.+ #' @return `teal_modules` with resolved `datanames`. |
||
20 | +115 |
- #'+ #' @keywords internal |
||
21 | +116 |
- #' @return `slices_store` returns `NULL`, invisibly.+ resolve_modules_datanames <- function(modules, datanames, join_keys) { |
||
22 | -+ | |||
117 | +! |
- #'+ if (inherits(modules, "teal_modules")) { |
||
23 | -+ | |||
118 | +! |
- #' @seealso [teal_slices()]+ modules$children <- sapply( |
||
24 | -+ | |||
119 | +! |
- #'+ modules$children, |
||
25 | -+ | |||
120 | +! |
- #' @examples+ resolve_modules_datanames, |
||
26 | -+ | |||
121 | +! |
- #' # use non-exported function from teal+ simplify = FALSE, |
||
27 | -+ | |||
122 | +! |
- #' slices_store <- getFromNamespace("slices_store", "teal")+ datanames = datanames, |
||
28 | -+ | |||
123 | +! |
- #'+ join_keys = join_keys |
||
29 | +124 |
- #' # Create a teal_slices object+ ) |
||
30 | -+ | |||
125 | +! |
- #' tss <- teal_slices(+ modules |
||
31 | +126 |
- #' teal_slice(dataname = "data", varname = "var"),+ } else { |
||
32 | -+ | |||
127 | +! |
- #' teal_slice(dataname = "data", expr = "x > 0", id = "positive_x", title = "Positive x")+ modules$datanames <- if (identical(modules$datanames, "all")) { |
||
33 | -+ | |||
128 | +! |
- #' )+ datanames |
||
34 | -+ | |||
129 | +! |
- #'+ } else if (is.character(modules$datanames)) { |
||
35 | -+ | |||
130 | +! |
- #' slices_path <- tempfile(pattern = "teal_slices", fileext = ".json")+ extra_datanames <- setdiff(modules$datanames, datanames) |
||
36 | -+ | |||
131 | +! |
- #' print(slices_path)+ if (length(extra_datanames)) { |
||
37 | -+ | |||
132 | +! |
- #'+ stop( |
||
38 | -+ | |||
133 | +! |
- #' # Store the teal_slices object to a file+ sprintf( |
||
39 | -+ | |||
134 | +! | +
+ "Module %s has datanames that are not available in a 'data':\n %s not in %s",+ |
+ ||
135 | +! | +
+ modules$label,+ |
+ ||
136 | +! | +
+ toString(extra_datanames),+ |
+ ||
137 | +! |
- #' slices_store(tss, slices_path)+ toString(datanames) |
||
40 | +138 |
- #' @keywords internal+ ) |
||
41 | +139 |
- #'+ ) |
||
42 | +140 |
- slices_store <- function(tss, file) {+ } |
||
43 | -9x | +|||
141 | +! |
- checkmate::assert_class(tss, "teal_slices")+ datanames_adjusted <- intersect(modules$datanames, datanames) |
||
44 | -9x | +|||
142 | +! |
- checkmate::assert_path_for_output(file, overwrite = TRUE, extension = "json")+ include_parent_datanames(dataname = datanames_adjusted, join_keys = join_keys) |
||
45 | +143 |
-
+ } |
||
46 | -9x | +|||
144 | +! |
- cat(format(tss, trim_lines = FALSE), "\n", file = file)+ modules |
||
47 | +145 |
- }+ } |
||
48 | +146 |
-
+ } |
||
49 | +147 |
- #' @rdname slices_store+ |
||
50 | +148 |
- #' @return `slices_restore` returns a `teal_slices` object restored from the file.+ #' Check `datanames` in modules |
||
51 | +149 |
- #' @examples+ #' |
||
52 | +150 |
- #'+ #' This function ensures specified `datanames` in modules match those in the data object, |
||
53 | +151 |
- #' # use non-exported function from teal+ #' returning error messages or `TRUE` for successful validation. |
||
54 | +152 |
- #' slices_restore <- getFromNamespace("slices_restore", "teal")+ #' |
||
55 | +153 |
- #'+ #' @param modules (`teal_modules`) object |
||
56 | +154 |
- #' # Restore a teal_slices object from a file+ #' @param datanames (`character`) names of datasets available in the `data` object |
||
57 | +155 |
- #' tss_restored <- slices_restore(slices_path)+ #' |
||
58 | +156 |
- #'+ #' @return A `character(1)` containing error message or `TRUE` if validation passes. |
||
59 | +157 |
#' @keywords internal |
||
60 | +158 |
- slices_restore <- function(file) {+ check_modules_datanames <- function(modules, datanames) { |
||
61 | -9x | +159 | +12x |
- checkmate::assert_file_exists(file, access = "r", extension = "json")+ checkmate::assert_class(modules, "teal_modules")+ |
+
160 | +12x | +
+ checkmate::assert_character(datanames) |
||
62 | +161 | |||
63 | -9x | -
- tss_json <- jsonlite::fromJSON(file, simplifyDataFrame = FALSE)- |
- ||
64 | -9x | -
- tss_json$slices <-- |
- ||
65 | -9x | +162 | +12x |
- lapply(tss_json$slices, function(slice) {+ recursive_check_datanames <- function(modules, datanames) { |
66 | -9x | +|||
163 | +
- for (field in c("selected", "choices")) {+ # check teal_modules against datanames |
|||
67 | -18x | +164 | +26x |
- if (!is.null(slice[[field]])) {+ if (inherits(modules, "teal_modules")) { |
68 | +165 | 12x |
- if (length(slice[[field]]) > 0) {- |
- |
69 | -9x | -
- date_partial_regex <- "^[0-9]{4}-[0-9]{2}-[0-9]{2}"- |
- ||
70 | -9x | -
- time_stamp_regex <- paste0(date_partial_regex, "\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\sUTC$")+ sapply(modules$children, function(module) recursive_check_datanames(module, datanames = datanames)) |
||
71 | +166 |
-
+ } else { |
||
72 | -9x | +167 | +14x |
- slice[[field]] <-+ extra_datanames <- setdiff(modules$datanames, c("all", datanames)) |
73 | -9x | +168 | +14x |
- if (all(grepl(paste0(date_partial_regex, "$"), slice[[field]]))) {+ if (length(extra_datanames)) { |
74 | -3x | +169 | +2x |
- as.Date(slice[[field]])+ sprintf( |
75 | -9x | +170 | +2x |
- } else if (all(grepl(time_stamp_regex, slice[[field]]))) {+ "- Module '%s' uses datanames not available in 'data': (%s) not in (%s)", |
76 | -3x | +171 | +2x |
- as.POSIXct(slice[[field]], tz = "UTC")+ modules$label, |
77 | -+ | |||
172 | +2x |
- } else {+ toString(dQuote(extra_datanames, q = FALSE)), |
||
78 | -3x | +173 | +2x |
- slice[[field]]+ toString(dQuote(datanames, q = FALSE)) |
79 | +174 |
- }+ ) |
||
80 | +175 |
- } else {- |
- ||
81 | -3x | -
- slice[[field]] <- character(0)+ } |
||
82 | +176 |
- }+ } |
||
83 | +177 |
- }+ } |
||
84 | -+ | |||
178 | +12x |
- }+ check_datanames <- unlist(recursive_check_datanames(modules, datanames)) |
||
85 | -9x | +179 | +12x |
- slice+ if (length(check_datanames)) { |
86 | -+ | |||
180 | +2x |
- })+ paste(check_datanames, collapse = "\n") |
||
87 | +181 |
-
+ } else { |
||
88 | -9x | +182 | +10x |
- tss_elements <- lapply(tss_json$slices, as.teal_slice)+ TRUE |
89 | +183 | - - | -||
90 | -9x | -
- do.call(teal_slices, c(tss_elements, tss_json$attributes))+ } |
||
91 | +184 |
} |
1 | +185 |
- #' Add right filter panel into each of the top-level `teal_modules` UIs.+ |
||
2 | +186 |
- #'+ #' Check `datanames` in filters |
||
3 | +187 |
- #' The [ui_nested_tabs] function returns a nested tabbed UI corresponding+ #' |
||
4 | +188 |
- #' to the nested modules.+ #' This function checks whether `datanames` in filters correspond to those in `data`, |
||
5 | +189 |
- #' This function adds the right filter panel to each main tab.+ #' returning character vector with error messages or `TRUE` if all checks pass. |
||
6 | +190 |
#' |
||
7 | +191 |
- #' The right filter panel's filter choices affect the `datasets` object. Therefore,+ #' @param filters (`teal_slices`) object |
||
8 | +192 |
- #' all modules using the same `datasets` share the same filters.+ #' @param datanames (`character`) names of datasets available in the `data` object |
||
9 | +193 |
#' |
||
10 | +194 |
- #' This works with nested modules of depth greater than 2, though the filter+ #' @return A `character(1)` containing error message or TRUE if validation passes. |
||
11 | +195 |
- #' panel is inserted at the right of the modules at depth 1 and not at the leaves.+ #' @keywords internal |
||
12 | +196 |
- #'+ check_filter_datanames <- function(filters, datanames) { |
||
13 | -+ | |||
197 | +10x |
- #' @name module_tabs_with_filters+ checkmate::assert_class(filters, "teal_slices") |
||
14 | -+ | |||
198 | +10x |
- #'+ checkmate::assert_character(datanames) |
||
15 | +199 |
- #' @inheritParams module_teal+ |
||
16 | +200 |
- #'+ # check teal_slices against datanames |
||
17 | -+ | |||
201 | +10x |
- #' @param datasets (named `list` of `FilteredData`)+ out <- unlist(sapply( |
||
18 | -+ | |||
202 | +10x |
- #' object to store filter state and filtered datasets, shared across modules. For more+ filters, function(filter) { |
||
19 | -+ | |||
203 | +3x |
- #' details see [`teal.slice::FilteredData`]. Structure of the list must be the same as structure+ dataname <- shiny::isolate(filter$dataname) |
||
20 | -+ | |||
204 | +3x |
- #' of the `modules` argument and list names must correspond to the labels in `modules`.+ if (!dataname %in% datanames) { |
||
21 | -+ | |||
205 | +2x |
- #' When filter is not module-specific then list contains the same object in all elements.+ sprintf( |
||
22 | -+ | |||
206 | +2x |
- #' @param reporter (`Reporter`) object from `teal.reporter`+ "- Filter '%s' refers to dataname not available in 'data':\n %s not in (%s)", |
||
23 | -+ | |||
207 | +2x |
- #'+ shiny::isolate(filter$id), |
||
24 | -+ | |||
208 | +2x |
- #' @return+ dQuote(dataname, q = FALSE), |
||
25 | -+ | |||
209 | +2x |
- #' A `shiny.tag.list` containing the main menu, placeholders for filters and placeholders for the `teal` modules.+ toString(dQuote(datanames, q = FALSE)) |
||
26 | +210 |
- #'+ ) |
||
27 | +211 |
- #' @examples+ } |
||
28 | +212 |
- #' # use non-exported function from teal+ } |
||
29 | +213 |
- #' include_teal_css_js <- getFromNamespace("include_teal_css_js", "teal")+ )) |
||
30 | +214 |
- #' teal_data_to_filtered_data <- getFromNamespace("teal_data_to_filtered_data", "teal")+ |
||
31 | +215 |
- #' ui_tabs_with_filters <- getFromNamespace("ui_tabs_with_filters", "teal")+ |
||
32 | -+ | |||
216 | +10x |
- #' srv_tabs_with_filters <- getFromNamespace("srv_tabs_with_filters", "teal")+ if (length(out)) { |
||
33 | -+ | |||
217 | +2x |
- #'+ paste(out, collapse = "\n") |
||
34 | +218 |
- #' # creates `teal_data`+ } else { |
||
35 | -+ | |||
219 | +8x |
- #' data <- teal_data(iris = iris, mtcars = mtcars)+ TRUE |
||
36 | +220 |
- #' datanames <- datanames(data)+ } |
||
37 | +221 |
- #'+ } |
||
38 | +222 |
- #' # creates a hierarchy of `teal_modules` from which a `teal` app can be created.+ |
||
39 | +223 |
- #' mods <- modules(+ #' Wrapper on `teal.data::datanames` |
||
40 | +224 |
- #' label = "d1",+ #' |
||
41 | +225 |
- #' modules(+ #' Special function used in internals of `teal` to return names of datasets even if `datanames` |
||
42 | +226 |
- #' label = "d2",+ #' has not been set. |
||
43 | +227 |
- #' modules(+ #' @param data (`teal_data`) |
||
44 | +228 |
- #' label = "d3",+ #' @return `character` |
||
45 | +229 |
- #' example_module(label = "aaa1", datanames = datanames),+ #' @keywords internal |
||
46 | +230 |
- #' example_module(label = "aaa2", datanames = datanames)+ teal_data_datanames <- function(data) { |
||
47 | -+ | |||
231 | +51x |
- #' ),+ checkmate::assert_class(data, "teal_data") |
||
48 | -+ | |||
232 | +51x |
- #' example_module(label = "bbb", datanames = datanames)+ if (length(teal.data::datanames(data))) { |
||
49 | -+ | |||
233 | +47x |
- #' ),+ teal.data::datanames(data) |
||
50 | +234 |
- #' example_module(label = "ccc", datanames = datanames)+ } else { |
||
51 | -+ | |||
235 | +4x |
- #' )+ ls(teal.code::get_env(data), all.names = TRUE) |
||
52 | +236 |
- #'+ } |
||
53 | +237 |
- #' # creates nested list aligned with the module hierarchy created above,+ } |
||
54 | +238 |
- #' # each leaf holding the same `FilteredData` object.+ |
||
55 | +239 |
- #' datasets <- teal_data_to_filtered_data(data)+ #' Function for validating the title parameter of `teal::init` |
||
56 | +240 |
- #' datasets <- list(+ #' |
||
57 | +241 |
- #' "d2" = list(+ #' Checks if the input of the title from `teal::init` will create a valid title and favicon tag. |
||
58 | +242 |
- #' "d3" = list(+ #' @param shiny_tag (`shiny.tag`) Object to validate for a valid title. |
||
59 | +243 |
- #' "aaa1" = datasets,+ #' @keywords internal |
||
60 | +244 |
- #' "aaa2" = datasets+ validate_app_title_tag <- function(shiny_tag) { |
||
61 | -+ | |||
245 | +14x |
- #' ),+ checkmate::assert_class(shiny_tag, "shiny.tag") |
||
62 | -+ | |||
246 | +14x |
- #' "bbb" = datasets+ checkmate::assert_true(shiny_tag$name == "head") |
||
63 | -+ | |||
247 | +13x |
- #' ),+ child_names <- vapply(shiny_tag$children, `[[`, character(1L), "name") |
||
64 | -+ | |||
248 | +13x |
- #' "ccc" = datasets+ checkmate::assert_subset(c("title", "link"), child_names, .var.name = "child tags") |
||
65 | -+ | |||
249 | +11x |
- #' )+ rel_attr <- shiny_tag$children[[which(child_names == "link")]]$attribs$rel |
||
66 | -+ | |||
250 | +11x |
- #'+ checkmate::assert_subset( |
||
67 | -+ | |||
251 | +11x |
- #' ui <- function() {+ rel_attr, |
||
68 | -- |
- #' tagList(- |
- ||
69 | -+ | |||
252 | +11x |
- #' include_teal_css_js(),+ c("icon", "shortcut icon"), |
||
70 | -+ | |||
253 | +11x |
- #' textOutput("info"),+ .var.name = "Link tag's rel attribute", |
||
71 | -+ | |||
254 | +11x |
- #' fluidPage( # needed for nice tabs+ empty.ok = FALSE |
||
72 | +255 |
- #' ui_tabs_with_filters("dummy", modules = mods, datasets = datasets)+ ) |
||
73 | +256 |
- #' )+ } |
||
74 | +257 |
- #' )+ |
||
75 | +258 |
- #' }+ #' Build app title with favicon |
||
76 | +259 |
- #' server <- function(input, output, session) {+ #' |
||
77 | +260 |
- #' output$info <- renderText({+ #' A helper function to create the browser title along with a logo. |
||
78 | +261 |
- #' paste0("The currently active tab name is ", active_module()$label)+ #' |
||
79 | +262 |
- #' })+ #' @param title (`character`) The browser title for the `teal` app. |
||
80 | +263 |
- #' active_module <- srv_tabs_with_filters(id = "dummy", datasets = datasets, modules = mods)+ #' @param favicon (`character`) The path for the icon for the title. |
||
81 | +264 |
- #' }+ #' The image/icon path can be remote or the static path accessible by `shiny`, like the `www/` |
||
82 | +265 |
#' |
||
83 | +266 |
- #' if (interactive()) {+ #' @return A `shiny.tag` containing the element that adds the title and logo to the `shiny` app. |
||
84 | +267 |
- #' shinyApp(ui, server)+ #' @export |
||
85 | +268 |
- #' }+ build_app_title <- function( |
||
86 | +269 |
- #' @keywords internal+ title = "teal app", |
||
87 | +270 |
- #'+ favicon = "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/nest.png") { |
||
88 | -+ | |||
271 | +11x |
- NULL+ checkmate::assert_string(title, null.ok = TRUE) |
||
89 | -+ | |||
272 | +11x |
-
+ checkmate::assert_string(favicon, null.ok = TRUE) |
||
90 | -+ | |||
273 | +11x |
- #' @rdname module_tabs_with_filters+ tags$head( |
||
91 | -+ | |||
274 | +11x |
- ui_tabs_with_filters <- function(id, modules, datasets, filter = teal_slices()) {+ tags$title(title), |
||
92 | -! | +|||
275 | +11x |
- checkmate::assert_class(modules, "teal_modules")+ tags$link( |
||
93 | -! | +|||
276 | +11x |
- checkmate::assert_list(datasets, types = c("list", "FilteredData"))+ rel = "icon", |
||
94 | -! | +|||
277 | +11x |
- checkmate::assert_class(filter, "teal_slices")+ href = favicon,+ |
+ ||
278 | +11x | +
+ sizes = "any" |
||
95 | +279 |
-
+ ) |
||
96 | -! | +|||
280 | +
- ns <- NS(id)+ ) |
|||
97 | -! | +|||
281 | +
- is_module_specific <- isTRUE(attr(filter, "module_specific"))+ } |
|||
98 | +282 | |||
99 | -! | +|||
283 | +
- teal_ui <- ui_nested_tabs(ns("root"), modules = modules, datasets, is_module_specific = is_module_specific)+ #' Application ID |
|||
100 | -! | +|||
284 | +
- filter_panel_btns <- tags$li(+ #' |
|||
101 | -! | +|||
285 | +
- class = "flex-grow",+ #' Creates App ID used to match filter snapshots to application. |
|||
102 | -! | +|||
286 | +
- tags$button(+ #' |
|||
103 | -! | +|||
287 | +
- class = "btn action-button filter_hamburger", # see sidebar.css for style filter_hamburger+ #' Calculate app ID that will be used to stamp filter state snapshots. |
|||
104 | -! | +|||
288 | +
- href = "javascript:void(0)",+ #' App ID is a hash of the app's data and modules. |
|||
105 | -! | +|||
289 | +
- onclick = "toggleFilterPanel();", # see sidebar.js+ #' See "transferring snapshots" section in ?snapshot. |
|||
106 | -! | +|||
290 | +
- title = "Toggle filter panels",+ #' |
|||
107 | -! | +|||
291 | +
- icon("fas fa-bars")+ #' @param data (`teal_data` or `teal_data_module`) as accepted by `init` |
|||
108 | +292 |
- ),+ #' @param modules (`teal_modules`) object as accepted by `init` |
||
109 | -! | +|||
293 | +
- filter_manager_modal_ui(ns("filter_manager"))+ #' |
|||
110 | +294 |
- )- |
- ||
111 | -! | -
- teal_ui$children[[1]] <- tagAppendChild(teal_ui$children[[1]], filter_panel_btns)+ #' @return A single character string. |
||
112 | +295 | - - | -||
113 | -! | -
- if (!is_module_specific) {+ #' |
||
114 | +296 |
- # need to rearrange html so that filter panel is within tabset- |
- ||
115 | -! | -
- tabset_bar <- teal_ui$children[[1]]+ #' @keywords internal |
||
116 | -! | +|||
297 | +
- teal_modules <- teal_ui$children[[2]]+ create_app_id <- function(data, modules) { |
|||
117 | -! | +|||
298 | +19x |
- filter_ui <- unlist(datasets)[[1]]$ui_filter_panel(ns("filter_panel"))+ checkmate::assert_multi_class(data, c("teal_data", "teal_data_module")) |
||
118 | -! | +|||
299 | +18x |
- list(+ checkmate::assert_class(modules, "teal_modules") |
||
119 | -! | +|||
300 | +
- tabset_bar,+ |
|||
120 | -! | +|||
301 | +17x |
- tags$hr(class = "my-2"),+ data <- if (inherits(data, "teal_data")) { |
||
121 | -! | +|||
302 | +15x |
- fluidRow(+ as.list(data@env) |
||
122 | -! | +|||
303 | +17x |
- column(width = 9, teal_modules, class = "teal_primary_col"),+ } else if (inherits(data, "teal_data_module")) { |
||
123 | -! | +|||
304 | +2x |
- column(width = 3, filter_ui, class = "teal_secondary_col")+ deparse1(body(data$server)) |
||
124 | +305 |
- )+ } |
||
125 | -+ | |||
306 | +17x |
- )+ modules <- lapply(modules, defunction) |
||
126 | +307 |
- } else {- |
- ||
127 | -! | -
- teal_ui+ |
||
128 | -+ | |||
308 | +17x |
- }+ rlang::hash(list(data = data, modules = modules)) |
||
129 | +309 |
} |
||
130 | +310 | |||
131 | +311 |
- #' @rdname module_tabs_with_filters+ #' Go through list and extract bodies of encountered functions as string, recursively. |
||
132 | +312 |
- srv_tabs_with_filters <- function(id,+ #' @keywords internal |
||
133 | +313 |
- datasets,+ #' @noRd |
||
134 | +314 |
- modules,+ defunction <- function(x) { |
||
135 | -+ | |||
315 | +186x |
- reporter = teal.reporter::Reporter$new(),+ if (is.list(x)) { |
||
136 | -+ | |||
316 | +40x |
- filter = teal_slices()) {+ lapply(x, defunction) |
||
137 | -5x | +317 | +146x |
- checkmate::assert_class(modules, "teal_modules")+ } else if (is.function(x)) { |
138 | -5x | +318 | +44x |
- checkmate::assert_list(datasets, types = c("list", "FilteredData"))+ deparse1(body(x)) |
139 | -5x | +|||
319 | +
- checkmate::assert_class(reporter, "Reporter")+ } else { |
|||
140 | -3x | +320 | +102x |
- checkmate::assert_class(filter, "teal_slices")+ x |
141 | +321 |
-
+ } |
||
142 | -3x | +|||
322 | +
- moduleServer(id, function(input, output, session) {+ } |
|||
143 | -3x | +
1 | +
- logger::log_trace("srv_tabs_with_filters initializing the module.")+ # This file adds a splash screen for delayed data loading on top of teal |
||
144 | +2 | ||
145 | -3x | +||
3 | +
- is_module_specific <- isTRUE(attr(filter, "module_specific"))+ #' Add splash screen to `teal` application. |
||
146 | -3x | +||
4 | +
- manager_out <- filter_manager_modal_srv("filter_manager", filtered_data_list = datasets, filter = filter)+ #' |
||
147 | +5 |
-
+ #' @description `r lifecycle::badge("stable")` |
|
148 | -3x | +||
6 | +
- active_module <- srv_nested_tabs(+ #' |
||
149 | -3x | +||
7 | +
- id = "root",+ #' Displays custom splash screen during initial delayed data loading. |
||
150 | -3x | +||
8 | +
- datasets = datasets,+ #' |
||
151 | -3x | +||
9 | +
- modules = modules,+ #' @details |
||
152 | -3x | +||
10 | +
- reporter = reporter,+ #' This module pauses app initialization pending delayed data loading. |
||
153 | -3x | +||
11 | +
- is_module_specific = is_module_specific+ #' This is necessary because the filter panel and modules depend on the data to initialize. |
||
154 | +12 |
- )+ #' |
|
155 | +13 |
-
+ #' `teal_with_splash` follows the `shiny` module convention. |
|
156 | -3x | +||
14 | +
- if (!is_module_specific) {+ #' [`init()`] is a wrapper around this that assumes that `teal` it is |
||
157 | -3x | +||
15 | +
- active_datanames <- reactive({+ #' the top-level module and cannot be embedded. |
||
158 | -6x | +||
16 | +
- if (identical(active_module()$datanames, "all")) {+ #' |
||
159 | -! | +||
17 | +
- singleton$datanames()+ #' Note: It is no longer recommended to embed `teal` in `shiny` apps as a module. |
||
160 | +18 |
- } else {+ #' but rather use `init` to create a standalone application. |
|
161 | -5x | +||
19 | +
- include_parent_datanames(+ #' |
||
162 | -5x | +||
20 | +
- active_module()$datanames,+ #' @seealso [init()] |
||
163 | -5x | +||
21 | +
- singleton$get_join_keys()+ #' |
||
164 | +22 |
- )+ #' @param id (`character(1)`) |
|
165 | +23 |
- }+ #' module id |
|
166 | +24 |
- })+ #' @inheritParams init |
|
167 | -3x | +||
25 | +
- singleton <- unlist(datasets)[[1]]+ #' @param modules (`teal_modules`) object containing the output modules which |
||
168 | -3x | +||
26 | +
- singleton$srv_filter_panel("filter_panel", active_datanames = active_datanames)+ #' will be displayed in the `teal` application. See [modules()] and [module()] for |
||
169 | +27 |
-
+ #' more details. |
|
170 | -3x | +||
28 | +
- observeEvent(+ #' @inheritParams shiny::moduleServer |
||
171 | -3x | +||
29 | +
- eventExpr = active_datanames(),+ #' @return |
||
172 | -3x | +||
30 | +
- handlerExpr = {+ #' Returns a `reactive` expression containing a `teal_data` object when data is loaded or `NULL` when it is not. |
||
173 | -4x | +||
31 | +
- script <- if (length(active_datanames()) == 0 || is.null(active_datanames())) {+ #' @name module_teal_with_splash |
||
174 | +32 |
- # hide the filter panel and disable the burger button+ #' @examples |
|
175 | -! | +||
33 | +
- "handleNoActiveDatasets();"+ #' teal_modules <- modules(example_module()) |
||
176 | +34 |
- } else {+ #' # Shiny app with modular integration of teal |
|
177 | +35 |
- # show the filter panel and enable the burger button+ #' ui <- fluidPage( |
|
178 | -4x | +||
36 | +
- "handleActiveDatasetsPresent();"+ #' ui_teal_with_splash(id = "app1", data = teal_data()) |
||
179 | +37 |
- }+ #' ) |
|
180 | -4x | +||
38 | +
- shinyjs::runjs(script)+ #' |
||
181 | +39 |
- },+ #' server <- function(input, output, session) { |
|
182 | -3x | +||
40 | +
- ignoreNULL = FALSE+ #' srv_teal_with_splash( |
||
183 | +41 |
- )+ #' id = "app1", |
|
184 | +42 |
- }+ #' data = teal_data(iris = iris), |
|
185 | +43 |
-
+ #' modules = teal_modules |
|
186 | -3x | +||
44 | +
- showNotification("Data loaded - App fully started up")+ #' ) |
||
187 | -3x | +||
45 | +
- logger::log_trace("srv_tabs_with_filters initialized the module")+ #' } |
||
188 | +46 |
-
+ #' |
|
189 | -3x | +||
47 | +
- active_module+ #' if (interactive()) { |
||
190 | +48 |
- })+ #' shinyApp(ui, server) |
|
191 | +49 |
- }+ #' } |
1 | +50 |
- #' Generates library calls from current session info+ #' |
||
2 | +51 |
- #'+ NULL |
||
3 | +52 |
- #' Function to create multiple library calls out of current session info to ensure reproducible code works.+ |
||
4 | +53 |
- #'+ #' @export |
||
5 | +54 |
- #' @return Character vector of `library(<package>)` calls.+ #' @rdname module_teal_with_splash |
||
6 | +55 |
- #' @keywords internal+ ui_teal_with_splash <- function(id, |
||
7 | +56 |
- get_rcode_libraries <- function() {- |
- ||
8 | -6x | -
- vapply(- |
- ||
9 | -6x | -
- utils::sessionInfo()$otherPkgs,- |
- ||
10 | -6x | -
- function(x) {- |
- ||
11 | -36x | -
- paste0("library(", x$Package, ")")+ data, |
||
12 | +57 |
- },- |
- ||
13 | -6x | -
- character(1)+ title = build_app_title(), |
||
14 | +58 |
- ) %>%+ header = tags$p(), |
||
15 | +59 |
- # put it into reverse order to correctly simulate executed code+ footer = tags$p()) { |
||
16 | -6x | +60 | +7x |
- rev() %>%+ checkmate::assert_character(id, max.len = 1, any.missing = FALSE) |
17 | -6x | +61 | +7x |
- paste0(sep = "\n") %>%+ checkmate::assert_multi_class(data, c("teal_data", "teal_data_module")) |
18 | -6x | +62 | +7x |
- paste0(collapse = "")+ checkmate::assert( |
19 | -+ | |||
63 | +7x |
- }+ .var.name = "title", |
||
20 | -+ | |||
64 | +7x |
-
+ checkmate::check_string(title), |
||
21 | -+ | |||
65 | +7x |
- #' @noRd+ checkmate::check_multi_class(title, c("shiny.tag", "shiny.tag.list", "html")) |
||
22 | +66 |
- #' @keywords internal+ ) |
||
23 | -+ | |||
67 | +7x |
- get_rcode_str_install <- function() {+ checkmate::assert( |
||
24 | -10x | +68 | +7x |
- code_string <- getOption("teal.load_nest_code")+ .var.name = "header", |
25 | -10x | +69 | +7x |
- if (is.character(code_string)) {+ checkmate::check_string(header), |
26 | -2x | +70 | +7x |
- code_string+ checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html")) |
27 | +71 |
- } else {+ ) |
||
28 | -8x | +72 | +7x |
- "# Add any code to install/load your NEST environment here\n"+ checkmate::assert( |
29 | -+ | |||
73 | +7x |
- }+ .var.name = "footer", |
||
30 | -+ | |||
74 | +7x |
- }+ checkmate::check_string(footer), |
||
31 | -+ | |||
75 | +7x |
-
+ checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html")) |
||
32 | +76 |
- #' Get datasets code+ ) |
||
33 | +77 |
- #'+ |
||
34 | -+ | |||
78 | +7x |
- #' Retrieve complete code to create, verify, and filter a dataset.+ ns <- NS(id) |
||
35 | +79 |
- #'+ |
||
36 | +80 |
- #' @param datanames (`character`) names of datasets to extract code from+ # Startup splash screen for delayed loading |
||
37 | +81 |
- #' @param datasets (`FilteredData`) object+ # We use delayed loading in all cases, even when the data does not need to be fetched. |
||
38 | +82 |
- #' @param hashes named (`list`) of hashes per dataset+ # This has the benefit that when filtering the data takes a lot of time initially, the |
||
39 | +83 |
- #'+ # Shiny app does not time out. |
||
40 | -+ | |||
84 | +7x |
- #' @return Character string concatenated from the following elements:+ splash_ui <- if (inherits(data, "teal_data_module")) { |
||
41 | -+ | |||
85 | +1x |
- #' - data pre-processing code (from `data` argument in `init`)+ data$ui(ns("teal_data_module")) |
||
42 | -+ | |||
86 | +7x |
- #' - hash check of loaded objects+ } else if (inherits(data, "teal_data")) { |
||
43 | -+ | |||
87 | +6x |
- #' - filter code (if any)+ div() |
||
44 | +88 |
- #'- |
- ||
45 | -- |
- #' @keywords internal+ } |
||
46 | -+ | |||
89 | +7x |
- get_datasets_code <- function(datanames, datasets, hashes) {+ ui_teal( |
||
47 | -+ | |||
90 | +7x |
- # preprocessing code+ id = ns("teal"), |
||
48 | -4x | +91 | +7x |
- str_prepro <-+ splash_ui = div(splash_ui, uiOutput(ns("error"))), |
49 | -4x | +92 | +7x |
- teal.data:::get_code_dependency(attr(datasets, "preprocessing_code"), names = datanames, check_names = FALSE)+ title = title, |
50 | -4x | +93 | +7x |
- if (length(str_prepro) == 0) {+ header = header, |
51 | -! | +|||
94 | +7x |
- str_prepro <- "message('Preprocessing is empty')"+ footer = footer |
||
52 | +95 |
- } else {+ ) |
||
53 | -4x | +|||
96 | +
- str_prepro <- paste(str_prepro, collapse = "\n")+ } |
|||
54 | +97 |
- }+ |
||
55 | +98 |
-
+ #' @export |
||
56 | +99 |
- # hash checks+ #' @rdname module_teal_with_splash |
||
57 | -4x | +|||
100 | +
- str_hash <- vapply(datanames, function(dataname) {+ srv_teal_with_splash <- function(id, data, modules, filter = teal_slices()) { |
|||
58 | -6x | +101 | +15x |
- sprintf(+ checkmate::assert_character(id, max.len = 1, any.missing = FALSE) |
59 | -6x | +102 | +15x |
- "stopifnot(%s == %s)",+ checkmate::assert_multi_class(data, c("teal_data", "teal_data_module")) |
60 | -6x | +103 | +15x |
- deparse1(bquote(rlang::hash(.(as.name(dataname))))),+ checkmate::assert_class(modules, "teal_modules") |
61 | -6x | +104 | +15x |
- deparse1(hashes[[dataname]])+ checkmate::assert_class(filter, "teal_slices") |
62 | +105 |
- )+ |
||
63 | -4x | +106 | +15x |
- }, character(1))+ moduleServer(id, function(input, output, session) { |
64 | -4x | +107 | +15x |
- str_hash <- paste(str_hash, collapse = "\n")+ logger::log_trace("srv_teal_with_splash initializing module with data.") |
65 | +108 | |||
66 | -- |
- # filter expressions- |
- ||
67 | -4x | +109 | +15x |
- str_filter <- teal.slice::get_filter_expr(datasets, datanames)+ if (getOption("teal.show_js_log", default = FALSE)) { |
68 | -4x | +|||
110 | +! |
- if (str_filter == "") {+ shinyjs::showLog() |
||
69 | -2x | +|||
111 | +
- str_filter <- character(0)+ } |
|||
70 | +112 |
- }+ |
||
71 | +113 |
-
+ # teal_data_rv contains teal_data object |
||
72 | +114 |
- # concatenate all code+ # either passed to teal::init or returned from teal_data_module |
||
73 | -4x | +115 | +15x |
- str_code <- paste(c(str_prepro, str_hash, str_filter), collapse = "\n\n")+ teal_data_rv <- if (inherits(data, "teal_data_module")) { |
74 | -4x | +116 | +10x |
- sprintf("%s\n", str_code)+ data <- data$server(id = "teal_data_module") |
75 | -+ | |||
117 | +10x |
- }+ if (!is.reactive(data)) { |
1 | -+ | |||
118 | +1x |
- #' Get client timezone+ stop("The `teal_data_module` passed to `data` must return a reactive expression.", call. = FALSE) |
||
2 | +119 |
- #'+ } |
||
3 | -+ | |||
120 | +9x |
- #' User timezone in the browser may be different to the one on the server.+ data |
||
4 | -+ | |||
121 | +15x |
- #' This script can be run to register a `shiny` input which contains information about the timezone in the browser.+ } else if (inherits(data, "teal_data")) { |
||
5 | -+ | |||
122 | +5x |
- #'+ reactiveVal(data) |
||
6 | +123 |
- #' @param ns (`function`) namespace function passed from the `session` object in the `shiny` server.+ } |
||
7 | +124 |
- #' For `shiny` modules this will allow for proper name spacing of the registered input.+ |
||
8 | -+ | |||
125 | +14x |
- #'+ teal_data_rv_validate <- reactive({ |
||
9 | +126 |
- #' @return (`shiny`) input variable accessible with `input$tz` which is a (`character`)+ # custom module can return error |
||
10 | -+ | |||
127 | +11x |
- #' string containing the timezone of the browser/client.+ data <- tryCatch(teal_data_rv(), error = function(e) e) |
||
11 | +128 |
- #'+ |
||
12 | +129 |
- #' @keywords internal+ # there is an empty reactive cycle on init!+ |
+ ||
130 | +11x | +
+ if (inherits(data, "shiny.silent.error") && identical(data$message, "")) {+ |
+ ||
131 | +! | +
+ return(NULL) |
||
13 | +132 |
- #'+ } |
||
14 | +133 |
- get_client_timezone <- function(ns) {+ + |
+ ||
134 | ++ |
+ # to handle qenv.error |
||
15 | -18x | +135 | +11x |
- script <- sprintf(+ if (inherits(data, "qenv.error")) { |
16 | -18x | +136 | +2x |
- "Shiny.setInputValue(`%s`, Intl.DateTimeFormat().resolvedOptions().timeZone)",+ validate( |
17 | -18x | +137 | +2x |
- ns("timezone")+ need( |
18 | -+ | |||
138 | +2x |
- )+ FALSE, |
||
19 | -18x | +139 | +2x |
- shinyjs::runjs(script) # function does not return anything+ paste( |
20 | -18x | +140 | +2x |
- invisible(NULL)+ "Error when executing `teal_data_module` passed to `data`:\n ",+ |
+
141 | +2x | +
+ paste(data$message, collapse = "\n"),+ |
+ ||
142 | +2x | +
+ "\n Check your inputs or contact app developer if error persists." |
||
21 | +143 |
- }+ ) |
||
22 | +144 |
-
+ ) |
||
23 | +145 |
- #' Resolve the expected bootstrap theme+ ) |
||
24 | +146 |
- #' @noRd+ } |
||
25 | +147 |
- #' @keywords internal+ |
||
26 | +148 |
- get_teal_bs_theme <- function() {+ # to handle module non-qenv errors |
||
27 | -11x | +149 | +9x |
- bs_theme <- getOption("teal.bs_theme")+ if (inherits(data, "error")) { |
28 | -11x | +150 | +1x |
- if (is.null(bs_theme)) {+ validate( |
29 | -8x | +151 | +1x |
- NULL+ need( |
30 | -3x | +152 | +1x |
- } else if (!inherits(bs_theme, "bs_theme")) {+ FALSE, |
31 | -2x | +153 | +1x |
- warning("teal.bs_theme has to be of a bslib::bs_theme class, the default shiny bootstrap is used.")+ paste( |
32 | -2x | +154 | +1x |
- NULL+ "Error when executing `teal_data_module` passed to `data`:\n ", |
33 | -+ | |||
155 | +1x |
- } else {+ paste(data$message, collpase = "\n"), |
||
34 | +156 | 1x |
- bs_theme+ "\n Check your inputs or contact app developer if error persists." |
|
35 | +157 |
- }+ ) |
||
36 | +158 |
- }+ ) |
||
37 | +159 |
-
+ ) |
||
38 | +160 |
- #' Return parentnames along with datanames.+ } |
||
39 | +161 |
- #' @noRd+ |
||
40 | -+ | |||
162 | +8x |
- #' @keywords internal+ validate( |
||
41 | -+ | |||
163 | +8x |
- include_parent_datanames <- function(dataname, join_keys) {+ need( |
||
42 | -11x | +164 | +8x |
- parents <- character(0)+ inherits(data, "teal_data"), |
43 | -11x | +165 | +8x |
- for (i in dataname) {+ paste( |
44 | -16x | +166 | +8x |
- while (length(i) > 0) {+ "Error: `teal_data_module` passed to `data` failed to return `teal_data` object, returned", |
45 | -18x | +167 | +8x |
- parent_i <- teal.data::parent(join_keys, i)+ toString(sQuote(class(data))), |
46 | -18x | +168 | +8x |
- parents <- c(parent_i, parents)+ "instead.", |
47 | -18x | +169 | +8x |
- i <- parent_i+ "\n Check your inputs or contact app developer if error persists." |
48 | +170 |
- }+ ) |
||
49 | +171 |
- }+ ) |
||
50 | +172 | - - | -||
51 | -11x | -
- unique(c(parents, dataname))+ ) |
||
52 | +173 |
- }+ |
||
53 | -+ | |||
174 | +5x |
-
+ if (!length(teal.data::datanames(data))) { |
||
54 | -+ | |||
175 | +1x |
- #' Create a `FilteredData`+ warning("`data` object has no datanames. Default datanames are set using `teal_data`'s environment.") |
||
55 | +176 |
- #'+ } |
||
56 | +177 |
- #' Create a `FilteredData` object from a `teal_data` object.+ |
||
57 | -+ | |||
178 | +5x |
- #'+ is_modules_ok <- check_modules_datanames(modules, teal_data_datanames(data)) |
||
58 | -+ | |||
179 | +5x |
- #' @param x (`teal_data`) object+ if (!isTRUE(is_modules_ok)) { |
||
59 | -+ | |||
180 | +1x |
- #' @param datanames (`character`) vector of data set names to include; must be subset of `datanames(x)`+ validate(need(isTRUE(is_modules_ok), sprintf("%s. Contact app developer.", is_modules_ok))) |
||
60 | +181 |
- #' @return A `FilteredData` object.+ } |
||
61 | +182 |
- #' @keywords internal+ |
||
62 | -+ | |||
183 | +4x |
- teal_data_to_filtered_data <- function(x, datanames = teal_data_datanames(x)) {+ is_filter_ok <- check_filter_datanames(filter, teal_data_datanames(data)) |
||
63 | -13x | +184 | +4x |
- checkmate::assert_class(x, "teal_data")+ if (!isTRUE(is_filter_ok)) { |
64 | -13x | +185 | +1x |
- checkmate::assert_character(datanames, min.chars = 1L, any.missing = FALSE)+ showNotification( |
65 | -+ | |||
186 | +1x |
-
+ "Some filters were not applied because of incompatibility with data. Contact app developer.", |
||
66 | -13x | +187 | +1x |
- ans <- teal.slice::init_filtered_data(+ type = "warning", |
67 | -13x | +188 | +1x |
- x = sapply(datanames, function(dn) x[[dn]], simplify = FALSE),+ duration = 10+ |
+
189 | ++ |
+ ) |
||
68 | -13x | +190 | +1x |
- join_keys = teal.data::join_keys(x)+ warning(is_filter_ok) |
69 | +191 |
- )+ } |
||
70 | +192 |
- # Piggy-back entire pre-processing code so that filtering code can be appended later.+ |
||
71 | -13x | +193 | +4x |
- attr(ans, "preprocessing_code") <- teal.code::get_code(x)+ teal_data_rv() |
72 | -13x | +|||
194 | +
- attr(ans, "verification_status") <- x@verified+ })+ |
+ |||
195 | ++ | + | ||
73 | -13x | +196 | +14x |
- ans+ output$error <- renderUI({ |
74 | -+ | |||
197 | +! |
- }+ teal_data_rv_validate() |
||
75 | -+ | |||
198 | +! |
-
+ NULL |
||
76 | +199 |
- #' Template function for `TealReportCard` creation and customization+ }) |
||
77 | +200 |
- #'+ |
||
78 | +201 |
- #' This function generates a report card with a title,+ |
||
79 | -+ | |||
202 | +14x |
- #' an optional description, and the option to append the filter state list.+ res <- srv_teal(id = "teal", modules = modules, teal_data_rv = teal_data_rv_validate, filter = filter) |
||
80 | -+ | |||
203 | +14x |
- #'+ logger::log_trace("srv_teal_with_splash initialized module with data.") |
||
81 | +204 |
- #' @param title (`character(1)`) title of the card (unless overwritten by label)+ |
||
82 | -+ | |||
205 | +14x |
- #' @param label (`character(1)`) label provided by the user when adding the card+ res |
||
83 | +206 |
- #' @param description (`character(1)`) optional additional description+ }) |
||
84 | +207 |
- #' @param with_filter (`logical(1)`) flag indicating to add filter state+ } |
85 | +1 |
- #' @param filter_panel_api (`FilterPanelAPI`) object with API that allows the generation+ #' Generates library calls from current session info |
||
86 | +2 |
- #' of the filter state in the report+ #' |
||
87 | +3 |
- #'+ #' Function to create multiple library calls out of current session info to ensure reproducible code works. |
||
88 | +4 |
- #' @return (`TealReportCard`) populated with a title, description and filter state.+ #' |
||
89 | +5 |
- #'+ #' @return Character vector of `library(<package>)` calls. |
||
90 | +6 |
- #' @export+ #' @keywords internal |
||
91 | +7 |
- report_card_template <- function(title, label, description = NULL, with_filter, filter_panel_api) {+ get_rcode_libraries <- function() { |
||
92 | -2x | +8 | +6x |
- checkmate::assert_string(title)+ vapply( |
93 | -2x | +9 | +6x |
- checkmate::assert_string(label)+ utils::sessionInfo()$otherPkgs, |
94 | -2x | +10 | +6x |
- checkmate::assert_string(description, null.ok = TRUE)+ function(x) { |
95 | -2x | +11 | +36x |
- checkmate::assert_flag(with_filter)+ paste0("library(", x$Package, ")")+ |
+
12 | ++ |
+ }, |
||
96 | -2x | +13 | +6x |
- checkmate::assert_class(filter_panel_api, classes = "FilterPanelAPI")+ character(1) |
97 | +14 |
-
+ ) %>% |
||
98 | -2x | +|||
15 | +
- card <- teal::TealReportCard$new()+ # put it into reverse order to correctly simulate executed code |
|||
99 | -2x | +16 | +6x |
- title <- if (label == "") title else label+ rev() %>% |
100 | -2x | +17 | +6x |
- card$set_name(title)+ paste0(sep = "\n") %>% |
101 | -2x | +18 | +6x |
- card$append_text(title, "header2")+ paste0(collapse = "") |
102 | -1x | +|||
19 | +
- if (!is.null(description)) card$append_text(description, "header3")+ } |
|||
103 | -1x | +|||
20 | +
- if (with_filter) card$append_fs(filter_panel_api$get_filter_state())+ |
|||
104 | -2x | +|||
21 | +
- card+ #' @noRd |
|||
105 | +22 |
- }+ #' @keywords internal |
||
106 | +23 |
-
+ get_rcode_str_install <- function() {+ |
+ ||
24 | +10x | +
+ code_string <- getOption("teal.load_nest_code")+ |
+ ||
25 | +10x | +
+ if (is.character(code_string)) {+ |
+ ||
26 | +2x | +
+ code_string |
||
107 | +27 |
- #' Resolve `datanames` for the modules+ } else {+ |
+ ||
28 | +8x | +
+ "# Add any code to install/load your NEST environment here\n" |
||
108 | +29 |
- #'+ } |
||
109 | +30 |
- #' Modifies `module$datanames` to include names of the parent dataset (taken from `join_keys`).+ } |
||
110 | +31 |
- #' When `datanames` is set to `"all"` it is replaced with all available datasets names.+ |
||
111 | +32 |
- #' @param modules (`teal_modules`) object+ #' Get datasets code |
||
112 | +33 |
- #' @param datanames (`character`) names of datasets available in the `data` object+ #' |
||
113 | +34 |
- #' @param join_keys (`join_keys`) object+ #' Retrieve complete code to create, verify, and filter a dataset. |
||
114 | +35 |
- #' @return `teal_modules` with resolved `datanames`.+ #' |
||
115 | +36 |
- #' @keywords internal+ #' @param datanames (`character`) names of datasets to extract code from |
||
116 | +37 |
- resolve_modules_datanames <- function(modules, datanames, join_keys) {+ #' @param datasets (`FilteredData`) object |
||
117 | -! | +|||
38 | +
- if (inherits(modules, "teal_modules")) {+ #' @param hashes named (`list`) of hashes per dataset |
|||
118 | -! | +|||
39 | +
- modules$children <- sapply(+ #' |
|||
119 | -! | +|||
40 | +
- modules$children,+ #' @return Character string concatenated from the following elements: |
|||
120 | -! | +|||
41 | +
- resolve_modules_datanames,+ #' - data pre-processing code (from `data` argument in `init`) |
|||
121 | -! | +|||
42 | +
- simplify = FALSE,+ #' - hash check of loaded objects |
|||
122 | -! | +|||
43 | +
- datanames = datanames,+ #' - filter code (if any) |
|||
123 | -! | +|||
44 | +
- join_keys = join_keys+ #' |
|||
124 | +45 |
- )+ #' @keywords internal |
||
125 | -! | +|||
46 | +
- modules+ get_datasets_code <- function(datanames, datasets, hashes) { |
|||
126 | +47 |
- } else {+ # preprocessing code |
||
127 | -! | +|||
48 | +4x |
- modules$datanames <- if (identical(modules$datanames, "all")) {+ str_prepro <- |
||
128 | -! | +|||
49 | +4x |
- datanames+ teal.data:::get_code_dependency(attr(datasets, "preprocessing_code"), names = datanames, check_names = FALSE) |
||
129 | -! | +|||
50 | +4x |
- } else if (is.character(modules$datanames)) {+ if (length(str_prepro) == 0) { |
||
130 | +51 | ! |
- extra_datanames <- setdiff(modules$datanames, datanames)+ str_prepro <- "message('Preprocessing is empty')" |
|
131 | -! | +|||
52 | +
- if (length(extra_datanames)) {+ } else { |
|||
132 | -! | +|||
53 | +4x |
- stop(+ str_prepro <- paste(str_prepro, collapse = "\n") |
||
133 | -! | +|||
54 | +
- sprintf(+ } |
|||
134 | -! | +|||
55 | +
- "Module %s has datanames that are not available in a 'data':\n %s not in %s",+ |
|||
135 | -! | +|||
56 | +
- modules$label,+ # hash checks |
|||
136 | -! | +|||
57 | +4x |
- toString(extra_datanames),+ str_hash <- vapply(datanames, function(dataname) { |
||
137 | -! | +|||
58 | +6x |
- toString(datanames)+ sprintf(+ |
+ ||
59 | +6x | +
+ "stopifnot(%s == %s)",+ |
+ ||
60 | +6x | +
+ deparse1(bquote(rlang::hash(.(as.name(dataname))))),+ |
+ ||
61 | +6x | +
+ deparse1(hashes[[dataname]]) |
||
138 | +62 |
- )+ )+ |
+ ||
63 | +4x | +
+ }, character(1))+ |
+ ||
64 | +4x | +
+ str_hash <- paste(str_hash, collapse = "\n") |
||
139 | +65 |
- )+ |
||
140 | +66 |
- }+ # filter expressions |
||
141 | -! | +|||
67 | +4x |
- datanames_adjusted <- intersect(modules$datanames, datanames)+ str_filter <- teal.slice::get_filter_expr(datasets, datanames) |
||
142 | -! | +|||
68 | +4x |
- include_parent_datanames(dataname = datanames_adjusted, join_keys = join_keys)+ if (str_filter == "") { |
||
143 | -+ | |||
69 | +2x |
- }+ str_filter <- character(0) |
||
144 | -! | +|||
70 | +
- modules+ } |
|||
145 | +71 |
- }+ |
||
146 | +72 |
- }+ # concatenate all code+ |
+ ||
73 | +4x | +
+ str_code <- paste(c(str_prepro, str_hash, str_filter), collapse = "\n\n")+ |
+ ||
74 | +4x | +
+ sprintf("%s\n", str_code) |
||
147 | +75 |
-
+ } |
148 | +1 |
- #' Check `datanames` in modules+ #' Create `teal_module` and `teal_modules` objects. |
||
149 | +2 |
#' |
||
150 | +3 |
- #' This function ensures specified `datanames` in modules match those in the data object,+ #' @description |
||
151 | +4 |
- #' returning error messages or `TRUE` for successful validation.+ #' `r lifecycle::badge("stable")` |
||
152 | +5 |
#' |
||
153 | +6 |
- #' @param modules (`teal_modules`) object+ #' Create a nested tab structure to embed modules in a `teal` application. |
||
154 | +7 |
- #' @param datanames (`character`) names of datasets available in the `data` object+ #' |
||
155 | +8 |
- #'+ #' @details |
||
156 | +9 |
- #' @return A `character(1)` containing error message or `TRUE` if validation passes.+ #' `module()` creates an instance of a `teal_module` that can be placed in a `teal` application. |
||
157 | +10 |
- #' @keywords internal+ #' `modules()` shapes the structure of a the application by organizing `teal_module` within the navigation panel. |
||
158 | +11 |
- check_modules_datanames <- function(modules, datanames) {+ #' It wraps `teal_module` and `teal_modules` objects in a `teal_modules` object, |
||
159 | -12x | +|||
12 | +
- checkmate::assert_class(modules, "teal_modules")+ #' which results in a nested structure corresponding to the nested tabs in the final application. |
|||
160 | -12x | +|||
13 | +
- checkmate::assert_character(datanames)+ #' |
|||
161 | +14 |
-
+ #' Note that for `modules()` `label` comes after `...`, so it must be passed as a named argument, |
||
162 | -12x | +|||
15 | +
- recursive_check_datanames <- function(modules, datanames) {+ #' otherwise it will be captured by `...`. |
|||
163 | +16 |
- # check teal_modules against datanames+ #' |
||
164 | -26x | +|||
17 | +
- if (inherits(modules, "teal_modules")) {+ #' The labels `"global_filters"` and `"Report previewer"` are reserved |
|||
165 | -12x | +|||
18 | +
- sapply(modules$children, function(module) recursive_check_datanames(module, datanames = datanames))+ #' because they are used by the `mapping` argument of [teal_slices()] |
|||
166 | +19 |
- } else {+ #' and the report previewer module [reporter_previewer_module()], respectively. |
||
167 | -14x | +|||
20 | +
- extra_datanames <- setdiff(modules$datanames, c("all", datanames))+ #' |
|||
168 | -14x | +|||
21 | +
- if (length(extra_datanames)) {+ #' @param label (`character(1)`) Label shown in the navigation item for the module or module group. |
|||
169 | -2x | +|||
22 | +
- sprintf(+ #' For `modules()` defaults to `"root"`. See `Details`. |
|||
170 | -2x | +|||
23 | +
- "- Module '%s' uses datanames not available in 'data': (%s) not in (%s)",+ #' @param server (`function`) `shiny` module with following arguments: |
|||
171 | -2x | +|||
24 | +
- modules$label,+ #' - `id` - `teal` will set proper `shiny` namespace for this module (see [shiny::moduleServer()]). |
|||
172 | -2x | +|||
25 | +
- toString(dQuote(extra_datanames, q = FALSE)),+ #' - `input`, `output`, `session` - (not recommended) then [shiny::callModule()] will be used to call a module. |
|||
173 | -2x | +|||
26 | +
- toString(dQuote(datanames, q = FALSE))+ #' - `data` (optional) module will receive a `teal_data` object, a list of reactive (filtered) data specified in |
|||
174 | +27 |
- )+ #' the `filters` argument. |
||
175 | +28 |
- }+ #' - `datasets` (optional) module will receive `FilteredData`. (See [`teal.slice::FilteredData`]). |
||
176 | +29 |
- }+ #' - `reporter` (optional) module will receive `Reporter`. (See [`teal.reporter::Reporter`]). |
||
177 | +30 |
- }+ #' - `filter_panel_api` (optional) module will receive `FilterPanelAPI`. (See [`teal.slice::FilterPanelAPI`]). |
||
178 | -12x | +|||
31 | +
- check_datanames <- unlist(recursive_check_datanames(modules, datanames))+ #' - `...` (optional) `server_args` elements will be passed to the module named argument or to the `...`. |
|||
179 | -12x | +|||
32 | +
- if (length(check_datanames)) {+ #' @param ui (`function`) `shiny` UI module function with following arguments: |
|||
180 | -2x | +|||
33 | +
- paste(check_datanames, collapse = "\n")+ #' - `id` - `teal` will set proper `shiny` namespace for this module. |
|||
181 | +34 |
- } else {+ #' - `...` (optional) `ui_args` elements will be passed to the module named argument or to the `...`. |
||
182 | -10x | +|||
35 | +
- TRUE+ #' @param filters (`character`) Deprecated. Use `datanames` instead. |
|||
183 | +36 |
- }+ #' @param datanames (`character`) A vector with `datanames` that are relevant for the item. The |
||
184 | +37 |
- }+ #' filter panel will automatically update the shown filters to include only |
||
185 | +38 |
-
+ #' filters in the listed datasets. `NULL` will hide the filter panel, |
||
186 | +39 |
- #' Check `datanames` in filters+ #' and the keyword `"all"` will show filters of all datasets. `datanames` also determines |
||
187 | +40 |
- #'+ #' a subset of datasets which are appended to the `data` argument in server function. |
||
188 | +41 |
- #' This function checks whether `datanames` in filters correspond to those in `data`,+ #' @param server_args (named `list`) with additional arguments passed on to the server function. |
||
189 | +42 |
- #' returning character vector with error messages or `TRUE` if all checks pass.+ #' @param ui_args (named `list`) with additional arguments passed on to the UI function. |
||
190 | +43 |
- #'+ #' @param x (`teal_module` or `teal_modules`) Object to format/print. |
||
191 | +44 |
- #' @param filters (`teal_slices`) object+ #' @param indent (`integer(1)`) Indention level; each nested element is indented one level more. |
||
192 | +45 |
- #' @param datanames (`character`) names of datasets available in the `data` object+ #' @param ... |
||
193 | +46 |
- #'+ #' - For `modules()`: (`teal_module` or `teal_modules`) Objects to wrap into a tab. |
||
194 | +47 |
- #' @return A `character(1)` containing error message or TRUE if validation passes.+ #' - For `format()` and `print()`: Arguments passed to other methods. |
||
195 | +48 |
- #' @keywords internal+ #' |
||
196 | +49 |
- check_filter_datanames <- function(filters, datanames) {+ #' @return |
||
197 | -10x | +|||
50 | +
- checkmate::assert_class(filters, "teal_slices")+ #' `module()` returns an object of class `teal_module`. |
|||
198 | -10x | +|||
51 | +
- checkmate::assert_character(datanames)+ #' |
|||
199 | +52 |
-
+ #' `modules()` returns a `teal_modules` object which contains following fields: |
||
200 | +53 |
- # check teal_slices against datanames+ #' - `label`: taken from the `label` argument. |
||
201 | -10x | +|||
54 | +
- out <- unlist(sapply(+ #' - `children`: a list containing objects passed in `...`. List elements are named after |
|||
202 | -10x | +|||
55 | +
- filters, function(filter) {+ #' their `label` attribute converted to a valid `shiny` id. |
|||
203 | -3x | +|||
56 | +
- dataname <- shiny::isolate(filter$dataname)+ #' |
|||
204 | -3x | +|||
57 | +
- if (!dataname %in% datanames) {+ #' @name teal_modules |
|||
205 | -2x | +|||
58 | +
- sprintf(+ #' @aliases teal_module |
|||
206 | -2x | +|||
59 | +
- "- Filter '%s' refers to dataname not available in 'data':\n %s not in (%s)",+ #' |
|||
207 | -2x | +|||
60 | +
- shiny::isolate(filter$id),+ #' @examples |
|||
208 | -2x | +|||
61 | +
- dQuote(dataname, q = FALSE),- |
- |||
209 | -2x | -
- toString(dQuote(datanames, q = FALSE))+ #' library(shiny) |
||
210 | +62 |
- )+ #' |
||
211 | +63 |
- }+ #' module_1 <- module( |
||
212 | +64 |
- }+ #' label = "a module", |
||
213 | +65 |
- ))+ #' server = function(id, data) { |
||
214 | +66 |
-
+ #' moduleServer( |
||
215 | +67 |
-
+ #' id, |
||
216 | -10x | +|||
68 | +
- if (length(out)) {+ #' module = function(input, output, session) { |
|||
217 | -2x | +|||
69 | +
- paste(out, collapse = "\n")+ #' output$data <- renderDataTable(data()[["iris"]]) |
|||
218 | +70 |
- } else {+ #' } |
||
219 | -8x | +|||
71 | +
- TRUE+ #' ) |
|||
220 | +72 |
- }+ #' }, |
||
221 | +73 |
- }+ #' ui = function(id) { |
||
222 | +74 |
-
+ #' ns <- NS(id) |
||
223 | +75 |
- #' Wrapper on `teal.data::datanames`+ #' tagList(dataTableOutput(ns("data"))) |
||
224 | +76 |
- #'+ #' }, |
||
225 | +77 |
- #' Special function used in internals of `teal` to return names of datasets even if `datanames`+ #' datanames = "all" |
||
226 | +78 |
- #' has not been set.+ #' ) |
||
227 | +79 |
- #' @param data (`teal_data`)+ #' |
||
228 | +80 |
- #' @return `character`+ #' module_2 <- module( |
||
229 | +81 |
- #' @keywords internal+ #' label = "another module", |
||
230 | +82 |
- teal_data_datanames <- function(data) {+ #' server = function(id) { |
||
231 | -51x | +|||
83 | +
- checkmate::assert_class(data, "teal_data")+ #' moduleServer( |
|||
232 | -51x | +|||
84 | +
- if (length(teal.data::datanames(data))) {+ #' id, |
|||
233 | -47x | +|||
85 | +
- teal.data::datanames(data)+ #' module = function(input, output, session) { |
|||
234 | +86 |
- } else {+ #' output$text <- renderText("Another Module") |
||
235 | -4x | +|||
87 | +
- ls(teal.code::get_env(data), all.names = TRUE)+ #' } |
|||
236 | +88 |
- }+ #' ) |
||
237 | +89 |
- }+ #' }, |
||
238 | +90 |
-
+ #' ui = function(id) { |
||
239 | +91 |
- #' Function for validating the title parameter of `teal::init`+ #' ns <- NS(id) |
||
240 | +92 |
- #'+ #' tagList(textOutput(ns("text"))) |
||
241 | +93 |
- #' Checks if the input of the title from `teal::init` will create a valid title and favicon tag.+ #' }, |
||
242 | +94 |
- #' @param shiny_tag (`shiny.tag`) Object to validate for a valid title.+ #' datanames = NULL |
||
243 | +95 |
- #' @keywords internal+ #' ) |
||
244 | +96 |
- validate_app_title_tag <- function(shiny_tag) {+ #' |
||
245 | -14x | +|||
97 | +
- checkmate::assert_class(shiny_tag, "shiny.tag")+ #' modules <- modules( |
|||
246 | -14x | +|||
98 | +
- checkmate::assert_true(shiny_tag$name == "head")+ #' label = "modules", |
|||
247 | -13x | +|||
99 | +
- child_names <- vapply(shiny_tag$children, `[[`, character(1L), "name")+ #' modules( |
|||
248 | -13x | +|||
100 | +
- checkmate::assert_subset(c("title", "link"), child_names, .var.name = "child tags")+ #' label = "nested modules", |
|||
249 | -11x | +|||
101 | +
- rel_attr <- shiny_tag$children[[which(child_names == "link")]]$attribs$rel+ #' module_1 |
|||
250 | -11x | +|||
102 | +
- checkmate::assert_subset(+ #' ), |
|||
251 | -11x | +|||
103 | +
- rel_attr,+ #' module_2 |
|||
252 | -11x | +|||
104 | +
- c("icon", "shortcut icon"),+ #' ) |
|||
253 | -11x | +|||
105 | +
- .var.name = "Link tag's rel attribute",+ #' |
|||
254 | -11x | +|||
106 | +
- empty.ok = FALSE+ #' app <- init( |
|||
255 | +107 |
- )+ #' data = teal_data(iris = iris), |
||
256 | +108 |
- }+ #' modules = modules |
||
257 | +109 |
-
+ #' ) |
||
258 | +110 |
- #' Build app title with favicon+ #' |
||
259 | +111 |
- #'+ #' if (interactive()) { |
||
260 | +112 |
- #' A helper function to create the browser title along with a logo.+ #' shinyApp(app$ui, app$server) |
||
261 | +113 |
- #'+ #' } |
||
262 | +114 |
- #' @param title (`character`) The browser title for the `teal` app.+ |
||
263 | +115 |
- #' @param favicon (`character`) The path for the icon for the title.+ #' @rdname teal_modules |
||
264 | +116 |
- #' The image/icon path can be remote or the static path accessible by `shiny`, like the `www/`+ #' @export |
||
265 | +117 |
#' |
||
266 | +118 |
- #' @return A `shiny.tag` containing the element that adds the title and logo to the `shiny` app.+ module <- function(label = "module", |
||
267 | +119 |
- #' @export+ server = function(id, ...) { |
||
268 | -+ | |||
120 | +! |
- build_app_title <- function(+ moduleServer(id, function(input, output, session) {}) # nolint |
||
269 | +121 |
- title = "teal app",+ }, |
||
270 | +122 |
- favicon = "https://raw.githubusercontent.com/insightsengineering/hex-stickers/main/PNG/nest.png") {- |
- ||
271 | -11x | -
- checkmate::assert_string(title, null.ok = TRUE)- |
- ||
272 | -11x | -
- checkmate::assert_string(favicon, null.ok = TRUE)+ ui = function(id, ...) { |
||
273 | -11x | +|||
123 | +! |
- tags$head(+ tags$p(paste0("This module has no UI (id: ", id, " )")) |
||
274 | -11x | +|||
124 | +
- tags$title(title),+ }, |
|||
275 | -11x | +|||
125 | +
- tags$link(+ filters, |
|||
276 | -11x | +|||
126 | +
- rel = "icon",+ datanames = "all", |
|||
277 | -11x | +|||
127 | +
- href = favicon,+ server_args = NULL, |
|||
278 | -11x | +|||
128 | +
- sizes = "any"+ ui_args = NULL) { |
|||
279 | +129 |
- )+ # argument checking (independent) |
||
280 | +130 |
- )+ ## `label` |
||
281 | -+ | |||
131 | +143x |
- }+ checkmate::assert_string(label) |
||
282 | -+ | |||
132 | +140x |
-
+ if (label == "global_filters") { |
||
283 | -+ | |||
133 | +1x |
- #' Application ID+ stop( |
||
284 | -+ | |||
134 | +1x |
- #'+ sprintf("module(label = \"%s\", ...\n ", label), |
||
285 | -+ | |||
135 | +1x |
- #' Creates App ID used to match filter snapshots to application.+ "Label 'global_filters' is reserved in teal. Please change to something else.", |
||
286 | -+ | |||
136 | +1x |
- #'+ call. = FALSE |
||
287 | +137 |
- #' Calculate app ID that will be used to stamp filter state snapshots.+ ) |
||
288 | +138 |
- #' App ID is a hash of the app's data and modules.+ } |
||
289 | -+ | |||
139 | +139x |
- #' See "transferring snapshots" section in ?snapshot.+ if (label == "Report previewer") { |
||
290 | -+ | |||
140 | +! |
- #'+ stop( |
||
291 | -+ | |||
141 | +! |
- #' @param data (`teal_data` or `teal_data_module`) as accepted by `init`+ sprintf("module(label = \"%s\", ...\n ", label), |
||
292 | -+ | |||
142 | +! |
- #' @param modules (`teal_modules`) object as accepted by `init`+ "Label 'Report previewer' is reserved in teal. Please change to something else.", |
||
293 | -+ | |||
143 | +! |
- #'+ call. = FALSE |
||
294 | +144 |
- #' @return A single character string.+ ) |
||
295 | +145 |
- #'+ } |
||
296 | +146 |
- #' @keywords internal+ |
||
297 | +147 |
- create_app_id <- function(data, modules) {+ ## server |
||
298 | -19x | +148 | +139x |
- checkmate::assert_multi_class(data, c("teal_data", "teal_data_module"))+ checkmate::assert_function(server) |
299 | -18x | +149 | +139x |
- checkmate::assert_class(modules, "teal_modules")+ server_formals <- names(formals(server)) |
300 | -+ | |||
150 | +139x |
-
+ if (!( |
||
301 | -17x | +151 | +139x |
- data <- if (inherits(data, "teal_data")) {+ "id" %in% server_formals || |
302 | -15x | +152 | +139x |
- as.list(data@env)+ all(c("input", "output", "session") %in% server_formals)+ |
+
153 | ++ |
+ )) { |
||
303 | -17x | +154 | +2x |
- } else if (inherits(data, "teal_data_module")) {+ stop( |
304 | +155 | 2x |
- deparse1(body(data$server))+ "\nmodule() `server` argument requires a function with following arguments:", |
|
305 | -+ | |||
156 | +2x |
- }+ "\n - id - `teal` will set proper `shiny` namespace for this module.", |
||
306 | -17x | +157 | +2x |
- modules <- lapply(modules, defunction)+ "\n - input, output, session (not recommended) - then `shiny::callModule` will be used to call a module.", |
307 | -+ | |||
158 | +2x |
-
+ "\n\nFollowing arguments can be used optionaly:", |
||
308 | -17x | +159 | +2x |
- rlang::hash(list(data = data, modules = modules))+ "\n - `data` - module will receive list of reactive (filtered) data specified in the `filters` argument", |
309 | -+ | |||
160 | +2x |
- }+ "\n - `datasets` - module will receive `FilteredData`. See `help(teal.slice::FilteredData)`", |
||
310 | -+ | |||
161 | +2x |
-
+ "\n - `reporter` - module will receive `Reporter`. See `help(teal.reporter::Reporter)`", |
||
311 | -+ | |||
162 | +2x |
- #' Go through list and extract bodies of encountered functions as string, recursively.+ "\n - `filter_panel_api` - module will receive `FilterPanelAPI`. (See [teal.slice::FilterPanelAPI]).", |
||
312 | -+ | |||
163 | +2x |
- #' @keywords internal+ "\n - `...` server_args elements will be passed to the module named argument or to the `...`" |
||
313 | +164 |
- #' @noRd+ ) |
||
314 | +165 |
- defunction <- function(x) {+ } |
||
315 | -186x | +166 | +137x |
- if (is.list(x)) {+ if ("datasets" %in% server_formals) { |
316 | -40x | +167 | +2x |
- lapply(x, defunction)+ warning( |
317 | -146x | +168 | +2x |
- } else if (is.function(x)) {+ sprintf("Called from module(label = \"%s\", ...)\n ", label), |
318 | -44x | -
- deparse1(body(x))- |
- ||
319 | -+ | 169 | +2x |
- } else {+ "`datasets` argument in the server is deprecated and will be removed in the next release. ", |
320 | -102x | -
- x- |
- ||
321 | -- |
- }- |
- ||
322 | -+ | 170 | +2x |
- }+ "Please use `data` instead.", |
1 | -+ | ||
171 | +2x |
- #' Create a `teal` module for previewing a report+ call. = FALSE |
|
2 | +172 |
- #'+ ) |
|
3 | +173 |
- #' @description `r lifecycle::badge("experimental")`+ } |
|
4 | +174 |
- #'+ |
|
5 | +175 |
- #' This function wraps [teal.reporter::reporter_previewer_ui()] and+ |
|
6 | +176 |
- #' [teal.reporter::reporter_previewer_srv()] into a `teal_module` to be+ ## UI |
|
7 | -+ | ||
177 | +137x |
- #' used in `teal` applications.+ checkmate::assert_function(ui) |
|
8 | -+ | ||
178 | +137x |
- #'+ ui_formals <- names(formals(ui)) |
|
9 | -+ | ||
179 | +137x |
- #' If you are creating a `teal` application using [init()] then this+ if (!"id" %in% ui_formals) { |
|
10 | -+ | ||
180 | +1x |
- #' module will be added to your application automatically if any of your `teal_modules`+ stop( |
|
11 | -+ | ||
181 | +1x |
- #' support report generation.+ "\nmodule() `ui` argument requires a function with following arguments:", |
|
12 | -+ | ||
182 | +1x |
- #'+ "\n - id - `teal` will set proper `shiny` namespace for this module.", |
|
13 | -+ | ||
183 | +1x |
- #' @inheritParams teal_modules+ "\n\nFollowing arguments can be used optionally:", |
|
14 | -+ | ||
184 | +1x |
- #' @param server_args (named `list`)- |
- |
15 | -- |
- #' Arguments passed to [teal.reporter::reporter_previewer_srv()].- |
- |
16 | -- |
- #'- |
- |
17 | -- |
- #' @return- |
- |
18 | -- |
- #' `teal_module` (extended with `teal_module_previewer` class) containing the `teal.reporter` previewer functionality.- |
- |
19 | -- |
- #'- |
- |
20 | -- |
- #' @export- |
- |
21 | -- |
- #'- |
- |
22 | -- |
- reporter_previewer_module <- function(label = "Report previewer", server_args = list()) {- |
- |
23 | -4x | -
- checkmate::assert_string(label)- |
- |
24 | -2x | -
- checkmate::assert_list(server_args, names = "named")- |
- |
25 | -2x | -
- checkmate::assert_true(all(names(server_args) %in% names(formals(teal.reporter::reporter_previewer_srv))))- |
- |
26 | -- | - - | -|
27 | -2x | -
- logger::log_info("Initializing reporter_previewer_module")- |
- |
28 | -- | - - | -|
29 | -2x | -
- srv <- function(id, reporter, ...) {- |
- |
30 | -! | -
- teal.reporter::reporter_previewer_srv(id, reporter, ...)- |
- |
31 | -- |
- }- |
- |
32 | -- | - - | -|
33 | -2x | -
- ui <- function(id, ...) {- |
- |
34 | -! | -
- teal.reporter::reporter_previewer_ui(id, ...)- |
- |
35 | -- |
- }- |
- |
36 | -- | - - | -|
37 | -2x | -
- module <- module(- |
- |
38 | -2x | -
- label = "temporary label",- |
- |
39 | -2x | -
- server = srv, ui = ui,- |
- |
40 | -2x | -
- server_args = server_args, ui_args = list(), datanames = NULL- |
- |
41 | -- |
- )- |
- |
42 | -- |
- # Module is created with a placeholder label and the label is changed later.- |
- |
43 | -- |
- # This is to prevent another module being labeled "Report previewer".- |
- |
44 | -2x | -
- class(module) <- c("teal_module_previewer", class(module))- |
- |
45 | -2x | -
- module$label <- label- |
- |
46 | -2x | -
- module- |
- |
47 | -- |
- }- |
-
1 | -- |
- # This file adds a splash screen for delayed data loading on top of teal- |
-
2 | -- | - - | -
3 | -- |
- #' Add splash screen to `teal` application.- |
-
4 | -- |
- #'- |
-
5 | -- |
- #' @description `r lifecycle::badge("stable")`- |
-
6 | -- |
- #'- |
-
7 | -- |
- #' Displays custom splash screen during initial delayed data loading.- |
-
8 | -- |
- #'- |
-
9 | -- |
- #' @details- |
-
10 | -- |
- #' This module pauses app initialization pending delayed data loading.- |
-
11 | -- |
- #' This is necessary because the filter panel and modules depend on the data to initialize.- |
-
12 | -- |
- #'- |
-
13 | -- |
- #' `teal_with_splash` follows the `shiny` module convention.- |
-
14 | -- |
- #' [`init()`] is a wrapper around this that assumes that `teal` it is- |
-
15 | -- |
- #' the top-level module and cannot be embedded.- |
-
16 | -- |
- #'- |
-
17 | -- |
- #' Note: It is no longer recommended to embed `teal` in `shiny` apps as a module.- |
-
18 | -- |
- #' but rather use `init` to create a standalone application.- |
-
19 | -- |
- #'- |
-
20 | -- |
- #' @seealso [init()]- |
-
21 | -- |
- #'- |
-
22 | -- |
- #' @param id (`character(1)`)- |
-
23 | -- |
- #' module id- |
-
24 | -- |
- #' @inheritParams init- |
-
25 | -- |
- #' @param modules (`teal_modules`) object containing the output modules which- |
-
26 | -- |
- #' will be displayed in the `teal` application. See [modules()] and [module()] for- |
-
27 | -- |
- #' more details.- |
-
28 | -- |
- #' @inheritParams shiny::moduleServer- |
-
29 | -- |
- #' @return- |
-
30 | -- |
- #' Returns a `reactive` expression containing a `teal_data` object when data is loaded or `NULL` when it is not.- |
-
31 | -- |
- #' @name module_teal_with_splash- |
-
32 | -- |
- #' @examples- |
-
33 | -- |
- #' teal_modules <- modules(example_module())- |
-
34 | -- |
- #' # Shiny app with modular integration of teal- |
-
35 | -- |
- #' ui <- fluidPage(- |
-
36 | -- |
- #' ui_teal_with_splash(id = "app1", data = teal_data())- |
-
37 | -- |
- #' )- |
-
38 | -- |
- #'- |
-
39 | -- |
- #' server <- function(input, output, session) {- |
-
40 | -- |
- #' srv_teal_with_splash(- |
-
41 | -- |
- #' id = "app1",- |
-
42 | -- |
- #' data = teal_data(iris = iris),- |
-
43 | -- |
- #' modules = teal_modules- |
-
44 | -- |
- #' )- |
-
45 | -- |
- #' }- |
-
46 | -- |
- #'- |
-
47 | -- |
- #' if (interactive()) {- |
-
48 | -- |
- #' shinyApp(ui, server)- |
-
49 | -- |
- #' }- |
-
50 | -- |
- #'- |
-
51 | -- |
- NULL- |
-
52 | -- | - - | -
53 | -- |
- #' @export- |
-
54 | -- |
- #' @rdname module_teal_with_splash- |
-
55 | -- |
- ui_teal_with_splash <- function(id,- |
-
56 | -- |
- data,- |
-
57 | -- |
- title = build_app_title(),- |
-
58 | -- |
- header = tags$p(),- |
-
59 | -- |
- footer = tags$p()) {- |
-
60 | -7x | -
- checkmate::assert_character(id, max.len = 1, any.missing = FALSE)- |
-
61 | -7x | -
- checkmate::assert_multi_class(data, c("teal_data", "teal_data_module"))- |
-
62 | -7x | -
- checkmate::assert(- |
-
63 | -7x | -
- .var.name = "title",- |
-
64 | -7x | -
- checkmate::check_string(title),- |
-
65 | -7x | -
- checkmate::check_multi_class(title, c("shiny.tag", "shiny.tag.list", "html"))- |
-
66 | -- |
- )- |
-
67 | -7x | -
- checkmate::assert(- |
-
68 | -7x | -
- .var.name = "header",- |
-
69 | -7x | -
- checkmate::check_string(header),- |
-
70 | -7x | -
- checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html"))- |
-
71 | -- |
- )- |
-
72 | -7x | -
- checkmate::assert(- |
-
73 | -7x | -
- .var.name = "footer",- |
-
74 | -7x | -
- checkmate::check_string(footer),- |
-
75 | -7x | -
- checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html"))- |
-
76 | -- |
- )- |
-
77 | -- | - - | -
78 | -7x | -
- ns <- NS(id)- |
-
79 | -- | - - | -
80 | -- |
- # Startup splash screen for delayed loading- |
-
81 | -- |
- # We use delayed loading in all cases, even when the data does not need to be fetched.- |
-
82 | -- |
- # This has the benefit that when filtering the data takes a lot of time initially, the- |
-
83 | -- |
- # Shiny app does not time out.- |
-
84 | -7x | -
- splash_ui <- if (inherits(data, "teal_data_module")) {- |
-
85 | -1x | -
- data$ui(ns("teal_data_module"))- |
-
86 | -7x | -
- } else if (inherits(data, "teal_data")) {- |
-
87 | -6x | -
- div()- |
-
88 | -- |
- }- |
-
89 | -7x | -
- ui_teal(- |
-
90 | -7x | -
- id = ns("teal"),- |
-
91 | -7x | -
- splash_ui = div(splash_ui, uiOutput(ns("error"))),- |
-
92 | -7x | -
- title = title,- |
-
93 | -7x | -
- header = header,- |
-
94 | -7x | -
- footer = footer- |
-
95 | -- |
- )- |
-
96 | -- |
- }- |
-
97 | -- | - - | -
98 | -- |
- #' @export- |
-
99 | -- |
- #' @rdname module_teal_with_splash- |
-
100 | -- |
- srv_teal_with_splash <- function(id, data, modules, filter = teal_slices()) {- |
-
101 | -15x | -
- checkmate::assert_character(id, max.len = 1, any.missing = FALSE)- |
-
102 | -15x | -
- checkmate::assert_multi_class(data, c("teal_data", "teal_data_module"))- |
-
103 | -15x | -
- checkmate::assert_class(modules, "teal_modules")- |
-
104 | -15x | -
- checkmate::assert_class(filter, "teal_slices")- |
-
105 | -- | - - | -
106 | -15x | -
- moduleServer(id, function(input, output, session) {- |
-
107 | -15x | -
- logger::log_trace("srv_teal_with_splash initializing module with data.")- |
-
108 | -- | - - | -
109 | -15x | -
- if (getOption("teal.show_js_log", default = FALSE)) {- |
-
110 | -! | -
- shinyjs::showLog()- |
-
111 | -- |
- }- |
-
112 | -- | - - | -
113 | -- |
- # teal_data_rv contains teal_data object- |
-
114 | -- |
- # either passed to teal::init or returned from teal_data_module- |
-
115 | -15x | -
- teal_data_rv <- if (inherits(data, "teal_data_module")) {- |
-
116 | -10x | -
- data <- data$server(id = "teal_data_module")- |
-
117 | -10x | -
- if (!is.reactive(data)) {- |
-
118 | -1x | -
- stop("The `teal_data_module` passed to `data` must return a reactive expression.", call. = FALSE)- |
-
119 | -- |
- }- |
-
120 | -9x | -
- data- |
-
121 | -15x | -
- } else if (inherits(data, "teal_data")) {- |
-
122 | -5x | -
- reactiveVal(data)- |
-
123 | -- |
- }- |
-
124 | -- | - - | -
125 | -14x | -
- teal_data_rv_validate <- reactive({- |
-
126 | -- |
- # custom module can return error- |
-
127 | -11x | -
- data <- tryCatch(teal_data_rv(), error = function(e) e)- |
-
128 | -- | - - | -
129 | -- |
- # there is an empty reactive cycle on init!- |
-
130 | -11x | -
- if (inherits(data, "shiny.silent.error") && identical(data$message, "")) {- |
-
131 | -! | -
- return(NULL)- |
-
132 | -- |
- }- |
-
133 | -- | - - | -
134 | -- |
- # to handle qenv.error- |
-
135 | -11x | -
- if (inherits(data, "qenv.error")) {- |
-
136 | -2x | -
- validate(- |
-
137 | -2x | -
- need(- |
-
138 | -2x | -
- FALSE,- |
-
139 | -2x | -
- paste(- |
-
140 | -2x | -
- "Error when executing `teal_data_module` passed to `data`:\n ",- |
-
141 | -2x | -
- paste(data$message, collapse = "\n"),- |
-
142 | -2x | -
- "\n Check your inputs or contact app developer if error persists."- |
-
143 | -- |
- )- |
-
144 | -- |
- )- |
-
145 | -- |
- )- |
-
146 | -- |
- }- |
-
147 | -- | - - | -
148 | -- |
- # to handle module non-qenv errors- |
-
149 | -9x | -
- if (inherits(data, "error")) {- |
-
150 | -1x | -
- validate(- |
-
151 | -1x | -
- need(- |
-
152 | -1x | -
- FALSE,- |
-
153 | -1x | -
- paste(- |
-
154 | -1x | -
- "Error when executing `teal_data_module` passed to `data`:\n ",- |
-
155 | -1x | -
- paste(data$message, collpase = "\n"),- |
-
156 | -1x | -
- "\n Check your inputs or contact app developer if error persists."- |
-
157 | -- |
- )- |
-
158 | -- |
- )- |
-
159 | -- |
- )- |
-
160 | -- |
- }- |
-
161 | -- | - - | -
162 | -8x | -
- validate(- |
-
163 | -8x | -
- need(- |
-
164 | -8x | -
- inherits(data, "teal_data"),- |
-
165 | -8x | -
- paste(- |
-
166 | -8x | -
- "Error: `teal_data_module` passed to `data` failed to return `teal_data` object, returned",- |
-
167 | -8x | -
- toString(sQuote(class(data))),- |
-
168 | -8x | -
- "instead.",- |
-
169 | -8x | -
- "\n Check your inputs or contact app developer if error persists."- |
-
170 | -- |
- )- |
-
171 | -- |
- )- |
-
172 | -- |
- )- |
-
173 | -- | - - | -
174 | -5x | -
- if (!length(teal.data::datanames(data))) {- |
-
175 | -1x | -
- warning("`data` object has no datanames. Default datanames are set using `teal_data`'s environment.")- |
-
176 | -- |
- }- |
-
177 | -- | - - | -
178 | -5x | -
- is_modules_ok <- check_modules_datanames(modules, teal_data_datanames(data))- |
-
179 | -5x | -
- if (!isTRUE(is_modules_ok)) {- |
-
180 | -1x | -
- validate(need(isTRUE(is_modules_ok), sprintf("%s. Contact app developer.", is_modules_ok)))- |
-
181 | -- |
- }- |
-
182 | -- | - - | -
183 | -4x | -
- is_filter_ok <- check_filter_datanames(filter, teal_data_datanames(data))- |
-
184 | -4x | -
- if (!isTRUE(is_filter_ok)) {- |
-
185 | -1x | -
- showNotification(- |
-
186 | -1x | -
- "Some filters were not applied because of incompatibility with data. Contact app developer.",- |
-
187 | -1x | -
- type = "warning",- |
-
188 | -1x | -
- duration = 10- |
-
189 | -- |
- )- |
-
190 | -1x | -
- warning(is_filter_ok)- |
-
191 | -- |
- }- |
-
192 | -- | - - | -
193 | -4x | -
- teal_data_rv()- |
-
194 | -- |
- })- |
-
195 | -- | - - | -
196 | -14x | -
- output$error <- renderUI({- |
-
197 | -! | -
- teal_data_rv_validate()- |
-
198 | -! | -
- NULL- |
-
199 | -- |
- })- |
-
200 | -- | - - | -
201 | -- | - - | -
202 | -14x | -
- res <- srv_teal(id = "teal", modules = modules, teal_data_rv = teal_data_rv_validate, filter = filter)- |
-
203 | -14x | -
- logger::log_trace("srv_teal_with_splash initialized module with data.")- |
-
204 | -- | - - | -
205 | -14x | -
- res- |
-
206 | -- |
- })- |
-
207 | -- |
- }- |
-
1 | -- |
- #' Validate that dataset has a minimum number of observations- |
- ||
2 | -- |
- #'- |
- ||
3 | -- |
- #' `r lifecycle::badge("stable")`- |
- ||
4 | -- |
- #'- |
- ||
5 | -- |
- #' This function is a wrapper for `shiny::validate`.- |
- ||
6 | -- |
- #'- |
- ||
7 | -- |
- #' @param x (`data.frame`)- |
- ||
8 | -- |
- #' @param min_nrow (`numeric(1)`) Minimum allowed number of rows in `x`.- |
- ||
9 | -- |
- #' @param complete (`logical(1)`) Flag specifying whether to check only complete cases. Defaults to `FALSE`.- |
- ||
10 | -- |
- #' @param allow_inf (`logical(1)`) Flag specifying whether to allow infinite values. Defaults to `TRUE`.- |
- ||
11 | -- |
- #' @param msg (`character(1)`) Additional message to display alongside the default message.- |
- ||
12 | -- |
- #'- |
- ||
13 | -- |
- #' @export- |
- ||
14 | -- |
- #'- |
- ||
15 | -- |
- #' @examples- |
- ||
16 | -- |
- #' library(teal)- |
- ||
17 | -- |
- #' ui <- fluidPage(- |
- ||
18 | -- |
- #' sliderInput("len", "Max Length of Sepal",- |
- ||
19 | -- |
- #' min = 4.3, max = 7.9, value = 5- |
- ||
20 | -- |
- #' ),- |
- ||
21 | -- |
- #' plotOutput("plot")- |
- ||
22 | -- |
- #' )- |
- ||
23 | -- |
- #'- |
- ||
24 | -- |
- #' server <- function(input, output) {- |
- ||
25 | -- |
- #' output$plot <- renderPlot({- |
- ||
26 | -- |
- #' iris_df <- iris[iris$Sepal.Length <= input$len, ]- |
- ||
27 | -- |
- #' validate_has_data(- |
- ||
28 | -- |
- #' iris_df,- |
- ||
29 | -- |
- #' min_nrow = 10,- |
- ||
30 | -- |
- #' complete = FALSE,- |
- ||
31 | -- |
- #' msg = "Please adjust Max Length of Sepal"- |
- ||
32 | -- |
- #' )+ "\n - `...` ui_args elements will be passed to the module argument of the same name or to the `...`" |
||
33 | +185 |
- #'+ ) |
||
34 | +186 |
- #' hist(iris_df$Sepal.Length, breaks = 5)+ } |
||
35 | -+ | |||
187 | +136x |
- #' })+ if (any(c("data", "datasets") %in% ui_formals)) { |
||
36 | -+ | |||
188 | +2x |
- #' }+ stop( |
||
37 | -+ | |||
189 | +2x |
- #' if (interactive()) {+ sprintf("Called from module(label = \"%s\", ...)\n ", label), |
||
38 | -+ | |||
190 | +2x |
- #' shinyApp(ui, server)+ "UI with `data` or `datasets` argument is no longer accepted.\n ", |
||
39 | -+ | |||
191 | +2x |
- #' }+ "If some UI inputs depend on data, please move the logic to your server instead.\n ", |
||
40 | -+ | |||
192 | +2x |
- #'+ "Possible solutions are renderUI() or updateXyzInput() functions." |
||
41 | +193 |
- validate_has_data <- function(x,+ ) |
||
42 | +194 |
- min_nrow = NULL,+ } |
||
43 | +195 |
- complete = FALSE,+ |
||
44 | +196 |
- allow_inf = TRUE,+ |
||
45 | +197 |
- msg = NULL) {- |
- ||
46 | -17x | -
- checkmate::assert_string(msg, null.ok = TRUE)- |
- ||
47 | -15x | -
- checkmate::assert_data_frame(x)+ ## `filters` |
||
48 | -15x | +198 | +134x |
- if (!is.null(min_nrow)) {+ if (!missing(filters)) { |
49 | -15x | +|||
199 | +! |
- if (complete) {+ datanames <- filters |
||
50 | -5x | +|||
200 | +! |
- complete_index <- stats::complete.cases(x)+ msg <- |
||
51 | -5x | +|||
201 | +! |
- validate(need(+ "The `filters` argument is deprecated and will be removed in the next release. Please use `datanames` instead." |
||
52 | -5x | +|||
202 | +! |
- sum(complete_index) > 0 && nrow(x[complete_index, , drop = FALSE]) >= min_nrow,+ logger::log_warn(msg) |
||
53 | -5x | +|||
203 | +! |
- paste(c(paste("Number of complete cases is less than:", min_nrow), msg), collapse = "\n")+ warning(msg) |
||
54 | +204 |
- ))+ } |
||
55 | +205 |
- } else {+ |
||
56 | -10x | +|||
206 | +
- validate(need(+ ## `datanames` (also including deprecated `filters`) |
|||
57 | -10x | +|||
207 | +
- nrow(x) >= min_nrow,+ # please note a race condition between datanames set when filters is not missing and data arg in server function |
|||
58 | -10x | +208 | +134x |
- paste(+ if (!is.element("data", server_formals) && !is.null(datanames)) { |
59 | -10x | +209 | +50x |
- c(paste("Minimum number of records not met: >=", min_nrow, "records required."), msg),+ message(sprintf("module \"%s\" server function takes no data so \"datanames\" will be ignored", label)) |
60 | -10x | +210 | +50x |
- collapse = "\n"+ datanames <- NULL |
61 | +211 |
- )+ } |
||
62 | -+ | |||
212 | +134x |
- ))+ checkmate::assert_character(datanames, min.len = 1, null.ok = TRUE, any.missing = FALSE) |
||
63 | +213 |
- }+ |
||
64 | +214 |
-
+ ## `server_args` |
||
65 | -10x | +215 | +133x |
- if (!allow_inf) {+ checkmate::assert_list(server_args, null.ok = TRUE, names = "named") |
66 | -6x | +216 | +131x |
- validate(need(+ srv_extra_args <- setdiff(names(server_args), server_formals) |
67 | -6x | +217 | +131x |
- all(vapply(x, function(col) !is.numeric(col) || !any(is.infinite(col)), logical(1))),+ if (length(srv_extra_args) > 0 && !"..." %in% server_formals) { |
68 | -6x | -
- "Dataframe contains Inf values which is not allowed."- |
- ||
69 | -- |
- ))- |
- ||
70 | -- |
- }- |
- ||
71 | -+ | 218 | +1x |
- }+ stop( |
72 | -+ | |||
219 | +1x |
- }+ "\nFollowing `server_args` elements have no equivalent in the formals of the server:\n", |
- ||
73 | -+ | |||
220 | +1x |
-
+ paste(paste(" -", srv_extra_args), collapse = "\n"), |
||
74 | -+ | |||
221 | +1x |
- #' Validate that dataset has unique rows for key variables+ "\n\nUpdate the server arguments by including above or add `...`" |
||
75 | +222 |
- #'+ ) |
||
76 | +223 |
- #' `r lifecycle::badge("stable")`+ } |
||
77 | +224 |
- #'+ |
||
78 | +225 |
- #' This function is a wrapper for `shiny::validate`.+ ## `ui_args` |
||
79 | -+ | |||
226 | +130x |
- #'+ checkmate::assert_list(ui_args, null.ok = TRUE, names = "named") |
||
80 | -+ | |||
227 | +128x |
- #' @param x (`data.frame`)+ ui_extra_args <- setdiff(names(ui_args), ui_formals) |
||
81 | -+ | |||
228 | +128x |
- #' @param key (`character`) Vector of ID variables from `x` that identify unique records.+ if (length(ui_extra_args) > 0 && !"..." %in% ui_formals) { |
||
82 | -+ | |||
229 | +1x |
- #'+ stop( |
||
83 | -+ | |||
230 | +1x |
- #' @export+ "\nFollowing `ui_args` elements have no equivalent in the formals of UI:\n", |
||
84 | -+ | |||
231 | +1x |
- #'+ paste(paste(" -", ui_extra_args), collapse = "\n"), |
||
85 | -+ | |||
232 | +1x |
- #' @examples+ "\n\nUpdate the UI arguments by including above or add `...`" |
||
86 | +233 |
- #' iris$id <- rep(1:50, times = 3)+ ) |
||
87 | +234 |
- #' ui <- fluidPage(+ } |
||
88 | +235 |
- #' selectInput(+ |
||
89 | -+ | |||
236 | +127x |
- #' inputId = "species",+ structure( |
||
90 | -+ | |||
237 | +127x |
- #' label = "Select species",+ list( |
||
91 | -+ | |||
238 | +127x |
- #' choices = c("setosa", "versicolor", "virginica"),+ label = label, |
||
92 | -+ | |||
239 | +127x |
- #' selected = "setosa",+ server = server, ui = ui, datanames = unique(datanames), |
||
93 | -+ | |||
240 | +127x |
- #' multiple = TRUE+ server_args = server_args, ui_args = ui_args |
||
94 | +241 |
- #' ),+ ), |
||
95 | -+ | |||
242 | +127x |
- #' plotOutput("plot")+ class = "teal_module" |
||
96 | +243 |
- #' )+ ) |
||
97 | +244 |
- #' server <- function(input, output) {+ } |
||
98 | +245 |
- #' output$plot <- renderPlot({+ |
||
99 | +246 |
- #' iris_f <- iris[iris$Species %in% input$species, ]+ #' @rdname teal_modules |
||
100 | +247 |
- #' validate_one_row_per_id(iris_f, key = c("id"))+ #' @export |
||
101 | +248 |
#' |
||
102 | +249 |
- #' hist(iris_f$Sepal.Length, breaks = 5)+ modules <- function(..., label = "root") { |
||
103 | -+ | |||
250 | +99x |
- #' })+ checkmate::assert_string(label) |
||
104 | -+ | |||
251 | +97x |
- #' }+ submodules <- list(...) |
||
105 | -+ | |||
252 | +97x |
- #' if (interactive()) {+ if (any(vapply(submodules, is.character, FUN.VALUE = logical(1)))) { |
||
106 | -+ | |||
253 | +2x |
- #' shinyApp(ui, server)+ stop( |
||
107 | -+ | |||
254 | +2x |
- #' }+ "The only character argument to modules() must be 'label' and it must be named, ", |
||
108 | -+ | |||
255 | +2x |
- #'+ "change modules('lab', ...) to modules(label = 'lab', ...)" |
||
109 | +256 |
- validate_one_row_per_id <- function(x, key = c("USUBJID", "STUDYID")) {- |
- ||
110 | -! | -
- validate(need(!any(duplicated(x[key])), paste("Found more than one row per id.")))+ ) |
||
111 | +257 |
- }+ } |
||
112 | +258 | |||
259 | +95x | +
+ checkmate::assert_list(submodules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules"))+ |
+ ||
113 | +260 |
- #' Validates that vector includes all expected values+ # name them so we can more easily access the children |
||
114 | +261 |
- #'+ # beware however that the label of the submodules should not be changed as it must be kept synced |
||
115 | -+ | |||
262 | +92x |
- #' `r lifecycle::badge("stable")`+ labels <- vapply(submodules, function(submodule) submodule$label, character(1)) |
||
116 | -+ | |||
263 | +92x |
- #'+ names(submodules) <- make.unique(gsub("[^[:alnum:]]+", "_", labels), sep = "_") |
||
117 | -+ | |||
264 | +92x |
- #' This function is a wrapper for `shiny::validate`.+ structure( |
||
118 | -+ | |||
265 | +92x |
- #'+ list( |
||
119 | -+ | |||
266 | +92x |
- #' @param x Vector of values to test.+ label = label, |
||
120 | -+ | |||
267 | +92x |
- #' @param choices Vector to test against.+ children = submodules |
||
121 | +268 |
- #' @param msg (`character(1)`) Error message to display if some elements of `x` are not elements of `choices`.+ ), |
||
122 | -+ | |||
269 | +92x |
- #'+ class = "teal_modules" |
||
123 | +270 |
- #' @export+ ) |
||
124 | +271 |
- #'+ } |
||
125 | +272 |
- #' @examples+ |
||
126 | +273 |
- #' ui <- fluidPage(+ # printing methods ---- |
||
127 | +274 |
- #' selectInput(+ |
||
128 | +275 |
- #' "species",+ #' @rdname teal_modules |
||
129 | +276 |
- #' "Select species",+ #' @export |
||
130 | +277 |
- #' choices = c("setosa", "versicolor", "virginica", "unknown species"),+ format.teal_module <- function(x, indent = 0, ...) { # nolint |
||
131 | -+ | |||
278 | +3x |
- #' selected = "setosa",+ paste0(paste(rep(" ", indent), collapse = ""), "+ ", x$label, "\n", collapse = "") |
||
132 | +279 |
- #' multiple = FALSE+ } |
||
133 | +280 |
- #' ),+ |
||
134 | +281 |
- #' verbatimTextOutput("summary")+ |
||
135 | +282 |
- #' )+ #' @rdname teal_modules |
||
136 | +283 |
- #'+ #' @export |
||
137 | +284 |
- #' server <- function(input, output) {+ print.teal_module <- function(x, ...) { |
||
138 | -+ | |||
285 | +! |
- #' output$summary <- renderPrint({+ cat(format(x, ...)) |
||
139 | -+ | |||
286 | +! |
- #' validate_in(input$species, iris$Species, "Species does not exist.")+ invisible(x) |
||
140 | +287 |
- #' nrow(iris[iris$Species == input$species, ])+ } |
||
141 | +288 |
- #' })+ |
||
142 | +289 |
- #' }+ |
||
143 | +290 |
- #' if (interactive()) {+ #' @rdname teal_modules |
||
144 | +291 |
- #' shinyApp(ui, server)+ #' @export |
||
145 | +292 |
- #' }+ format.teal_modules <- function(x, indent = 0, ...) { # nolint |
||
146 | -+ | |||
293 | +1x |
- #'+ paste( |
||
147 | -+ | |||
294 | +1x |
- validate_in <- function(x, choices, msg) {+ c( |
||
148 | -! | +|||
295 | +1x |
- validate(need(length(x) > 0 && length(choices) > 0 && all(x %in% choices), msg))+ paste0(rep(" ", indent), "+ ", x$label, "\n"), |
||
149 | -+ | |||
296 | +1x |
- }+ unlist(lapply(x$children, format, indent = indent + 1, ...)) |
||
150 | +297 |
-
+ ), |
||
151 | -+ | |||
298 | +1x |
- #' Validates that vector has length greater than 0+ collapse = "" |
||
152 | +299 |
- #'+ ) |
||
153 | +300 |
- #' `r lifecycle::badge("stable")`+ } |
||
154 | +301 |
- #'+ |
||
155 | +302 |
- #' This function is a wrapper for `shiny::validate`.+ |
||
156 | +303 |
- #'+ #' @rdname teal_modules |
||
157 | +304 |
- #' @param x vector+ #' @export |
||
158 | +305 |
- #' @param msg message to display+ print.teal_modules <- print.teal_module |
||
159 | +306 |
- #'+ |
||
160 | +307 |
- #' @export+ |
||
161 | +308 |
- #'+ # utilities ---- |
||
162 | +309 |
- #' @examples+ ## subset or modify modules ---- |
||
163 | +310 |
- #' data <- data.frame(+ |
||
164 | +311 |
- #' id = c(1:10, 11:20, 1:10),+ #' Append a `teal_module` to `children` of a `teal_modules` object |
||
165 | +312 |
- #' strata = rep(c("A", "B"), each = 15)+ #' @keywords internal |
||
166 | +313 |
- #' )+ #' @param modules (`teal_modules`) |
||
167 | +314 |
- #' ui <- fluidPage(+ #' @param module (`teal_module`) object to be appended onto the children of `modules` |
||
168 | +315 |
- #' selectInput("ref1", "Select strata1 to compare",+ #' @return A `teal_modules` object with `module` appended. |
||
169 | +316 |
- #' choices = c("A", "B", "C"), selected = "A"+ append_module <- function(modules, module) { |
||
170 | -+ | |||
317 | +8x |
- #' ),+ checkmate::assert_class(modules, "teal_modules") |
||
171 | -+ | |||
318 | +6x |
- #' selectInput("ref2", "Select strata2 to compare",+ checkmate::assert_class(module, "teal_module") |
||
172 | -+ | |||
319 | +4x |
- #' choices = c("A", "B", "C"), selected = "B"+ modules$children <- c(modules$children, list(module)) |
||
173 | -+ | |||
320 | +4x |
- #' ),+ labels <- vapply(modules$children, function(submodule) submodule$label, character(1)) |
||
174 | -+ | |||
321 | +4x |
- #' verbatimTextOutput("arm_summary")+ names(modules$children) <- make.unique(gsub("[^[:alnum:]]", "_", tolower(labels)), sep = "_") |
||
175 | -+ | |||
322 | +4x |
- #' )+ modules |
||
176 | +323 |
- #'+ } |
||
177 | +324 |
- #' server <- function(input, output) {+ |
||
178 | +325 |
- #' output$arm_summary <- renderText({+ #' Extract/Remove module(s) of specific class |
||
179 | +326 |
- #' sample_1 <- data$id[data$strata == input$ref1]+ #' |
||
180 | +327 |
- #' sample_2 <- data$id[data$strata == input$ref2]+ #' Given a `teal_module` or a `teal_modules`, return the elements of the structure according to `class`. |
||
181 | +328 |
#' |
||
182 | +329 |
- #' validate_has_elements(sample_1, "No subjects in strata1.")+ #' @param modules (`teal_modules`) |
||
183 | +330 |
- #' validate_has_elements(sample_2, "No subjects in strata2.")+ #' @param class The class name of `teal_module` to be extracted or dropped. |
||
184 | +331 |
- #'+ #' @keywords internal |
||
185 | +332 |
- #' paste0(+ #' @return |
||
186 | +333 |
- #' "Number of samples in: strata1=", length(sample_1),+ #' - For `extract_module`, a `teal_module` of class `class` or `teal_modules` containing modules of class `class`. |
||
187 | +334 |
- #' " comparions strata2=", length(sample_2)+ #' - For `drop_module`, the opposite, which is all `teal_modules` of class other than `class`. |
||
188 | +335 |
- #' )+ #' @rdname module_management |
||
189 | +336 |
- #' })+ extract_module <- function(modules, class) { |
||
190 | -+ | |||
337 | +20x |
- #' }+ if (inherits(modules, class)) { |
||
191 | -+ | |||
338 | +! |
- #' if (interactive()) {+ modules |
||
192 | -+ | |||
339 | +20x |
- #' shinyApp(ui, server)+ } else if (inherits(modules, "teal_module")) { |
||
193 | -+ | |||
340 | +11x |
- #' }+ NULL |
||
194 | -+ | |||
341 | +9x |
- validate_has_elements <- function(x, msg) {+ } else if (inherits(modules, "teal_modules")) { |
||
195 | -! | +|||
342 | +9x |
- validate(need(length(x) > 0, msg))+ Filter(function(x) length(x) > 0L, lapply(modules$children, extract_module, class)) |
||
196 | +343 |
- }+ } |
||
197 | +344 |
-
+ } |
||
198 | +345 |
- #' Validates no intersection between two vectors+ |
||
199 | +346 |
- #'+ #' @keywords internal |
||
200 | +347 |
- #' `r lifecycle::badge("stable")`+ #' @return `teal_modules` |
||
201 | +348 |
- #'+ #' @rdname module_management |
||
202 | +349 |
- #' This function is a wrapper for `shiny::validate`.+ drop_module <- function(modules, class) { |
||
203 | -+ | |||
350 | +! |
- #'+ if (inherits(modules, class)) { |
||
204 | -+ | |||
351 | +! |
- #' @param x vector+ NULL |
||
205 | -+ | |||
352 | +! |
- #' @param y vector+ } else if (inherits(modules, "teal_module")) { |
||
206 | -+ | |||
353 | +! |
- #' @param msg (`character(1)`) message to display if `x` and `y` intersect+ modules |
||
207 | -+ | |||
354 | +! |
- #'+ } else if (inherits(modules, "teal_modules")) {+ |
+ ||
355 | +! | +
+ do.call(+ |
+ ||
356 | +! | +
+ "modules", |
||
208 | -+ | |||
357 | +! |
- #' @export+ c(Filter(function(x) length(x) > 0L, lapply(modules$children, drop_module, class)), label = modules$label) |
||
209 | +358 |
- #'+ ) |
||
210 | +359 |
- #' @examples+ } |
||
211 | +360 |
- #' data <- data.frame(+ } |
||
212 | +361 |
- #' id = c(1:10, 11:20, 1:10),+ |
||
213 | +362 |
- #' strata = rep(c("A", "B", "C"), each = 10)+ ## read modules ---- |
||
214 | +363 |
- #' )+ |
||
215 | +364 |
- #'+ #' Does the object make use of the `arg` |
||
216 | +365 |
- #' ui <- fluidPage(+ #' |
||
217 | +366 |
- #' selectInput("ref1", "Select strata1 to compare",+ #' @param modules (`teal_module` or `teal_modules`) object |
||
218 | +367 |
- #' choices = c("A", "B", "C"),+ #' @param arg (`character(1)`) names of the arguments to be checked against formals of `teal` modules. |
||
219 | +368 |
- #' selected = "A"+ #' @return `logical` whether the object makes use of `arg`. |
||
220 | +369 |
- #' ),+ #' @rdname is_arg_used |
||
221 | +370 |
- #' selectInput("ref2", "Select strata2 to compare",+ #' @keywords internal |
||
222 | +371 |
- #' choices = c("A", "B", "C"),+ is_arg_used <- function(modules, arg) { |
||
223 | -+ | |||
372 | +286x |
- #' selected = "B"+ checkmate::assert_string(arg) |
||
224 | -+ | |||
373 | +283x |
- #' ),+ if (inherits(modules, "teal_modules")) { |
||
225 | -+ | |||
374 | +29x |
- #' verbatimTextOutput("summary")+ any(unlist(lapply(modules$children, is_arg_used, arg))) |
||
226 | -+ | |||
375 | +254x |
- #' )+ } else if (inherits(modules, "teal_module")) { |
||
227 | -+ | |||
376 | +43x |
- #'+ is_arg_used(modules$server, arg) || is_arg_used(modules$ui, arg) |
||
228 | -+ | |||
377 | +211x |
- #' server <- function(input, output) {+ } else if (is.function(modules)) { |
||
229 | -+ | |||
378 | +209x |
- #' output$summary <- renderText({+ isTRUE(arg %in% names(formals(modules))) |
||
230 | +379 |
- #' sample_1 <- data$id[data$strata == input$ref1]+ } else { |
||
231 | -+ | |||
380 | +2x |
- #' sample_2 <- data$id[data$strata == input$ref2]+ stop("is_arg_used function not implemented for this object") |
||
232 | +381 |
- #'+ } |
||
233 | +382 |
- #' validate_no_intersection(+ } |
||
234 | +383 |
- #' sample_1, sample_2,+ |
||
235 | +384 |
- #' "subjects within strata1 and strata2 cannot overlap"+ |
||
236 | +385 |
- #' )+ #' Get module depth |
||
237 | +386 |
- #' paste0(+ #' |
||
238 | +387 |
- #' "Number of subject in: reference treatment=", length(sample_1),+ #' Depth starts at 0, so a single `teal.module` has depth 0. |
||
239 | +388 |
- #' " comparions treatment=", length(sample_2)+ #' Nesting it increases overall depth by 1. |
||
240 | +389 |
- #' )+ #' |
||
241 | +390 |
- #' })+ #' @inheritParams init |
||
242 | +391 |
- #' }+ #' @param depth optional, integer determining current depth level |
||
243 | +392 |
- #' if (interactive()) {+ #' |
||
244 | +393 |
- #' shinyApp(ui, server)+ #' @return Depth level for given module. |
||
245 | +394 |
- #' }+ #' @keywords internal |
||
246 | +395 |
- #'+ modules_depth <- function(modules, depth = 0L) { |
||
247 | -+ | |||
396 | +12x |
- validate_no_intersection <- function(x, y, msg) {+ checkmate::assert_multi_class(modules, c("teal_module", "teal_modules")) |
||
248 | -! | +|||
397 | +12x |
- validate(need(length(intersect(x, y)) == 0, msg))+ checkmate::assert_int(depth, lower = 0) |
||
249 | -+ | |||
398 | +11x |
- }+ if (inherits(modules, "teal_modules")) { |
||
250 | -+ | |||
399 | +4x |
-
+ max(vapply(modules$children, modules_depth, integer(1), depth = depth + 1L)) |
||
251 | +400 |
-
+ } else { |
||
252 | -+ | |||
401 | +7x |
- #' Validates that dataset contains specific variable+ depth |
||
253 | +402 |
- #'+ } |
||
254 | +403 |
- #' `r lifecycle::badge("stable")`+ } |
||
255 | +404 |
- #'+ |
||
256 | +405 |
- #' This function is a wrapper for `shiny::validate`.+ #' Retrieve labels from `teal_modules` |
||
257 | +406 |
#' |
||
258 | +407 |
- #' @param data (`data.frame`)+ #' @param modules (`teal_modules`) |
||
259 | +408 |
- #' @param varname (`character(1)`) name of variable to check for in `data`+ #' @return A `list` containing the labels of the modules. If the modules are nested, |
||
260 | +409 |
- #' @param msg (`character(1)`) message to display if `data` does not include `varname`+ #' the function returns a nested `list` of labels. |
||
261 | +410 |
- #'+ #' @keywords internal |
||
262 | +411 |
- #' @export+ module_labels <- function(modules) { |
||
263 | -+ | |||
412 | +! |
- #'+ if (inherits(modules, "teal_modules")) { |
||
264 | -+ | |||
413 | +! |
- #' @examples+ lapply(modules$children, module_labels) |
||
265 | +414 |
- #' data <- data.frame(+ } else { |
||
266 | -+ | |||
415 | +! |
- #' one = rep("a", length.out = 20),+ modules$label |
||
267 | +416 |
- #' two = rep(c("a", "b"), length.out = 20)+ } |
||
268 | +417 |
- #' )+ } |
269 | +1 |
- #' ui <- fluidPage(+ #' Create a UI of nested tabs of `teal_modules` |
|
270 | +2 |
- #' selectInput(+ #' |
|
271 | +3 |
- #' "var",+ #' @section `ui_nested_tabs`: |
|
272 | +4 |
- #' "Select variable",+ #' Each `teal_modules` is translated to a `tabsetPanel` and each |
|
273 | +5 |
- #' choices = c("one", "two", "three", "four"),+ #' of its children is another tab-module called recursively. The UI of a |
|
274 | +6 |
- #' selected = "one"+ #' `teal_module` is obtained by calling its UI function. |
|
275 | +7 |
- #' ),+ #' |
|
276 | +8 |
- #' verbatimTextOutput("summary")+ #' The `datasets` argument is required to resolve the `teal` arguments in an |
|
277 | +9 |
- #' )+ #' isolated context (with respect to reactivity). |
|
278 | +10 |
#' |
|
279 | +11 |
- #' server <- function(input, output) {+ #' @section `srv_nested_tabs`: |
|
280 | +12 |
- #' output$summary <- renderText({+ #' This module recursively calls all elements of `modules` and returns currently active one. |
|
281 | +13 |
- #' validate_has_variable(data, input$var)+ #' - `teal_module` returns self as a active module. |
|
282 | +14 |
- #' paste0("Selected treatment variables: ", paste(input$var, collapse = ", "))+ #' - `teal_modules` also returns module active within self which is determined by the `input$active_tab`. |
|
283 | +15 |
- #' })+ #' |
|
284 | +16 |
- #' }+ #' @name module_nested_tabs |
|
285 | +17 |
- #' if (interactive()) {+ #' |
|
286 | +18 |
- #' shinyApp(ui, server)+ #' @inheritParams module_tabs_with_filters |
|
287 | +19 |
- #' }+ #' |
|
288 | +20 |
- validate_has_variable <- function(data, varname, msg) {+ #' @param depth (`integer(1)`) |
|
289 | -! | +||
21 | +
- if (length(varname) != 0) {+ #' number which helps to determine depth of the modules nesting. |
||
290 | -! | +||
22 | +
- has_vars <- varname %in% names(data)+ #' @param is_module_specific (`logical(1)`) |
||
291 | +23 |
-
+ #' flag determining if the filter panel is global or module-specific. |
|
292 | -! | +||
24 | +
- if (!all(has_vars)) {+ #' When set to `TRUE`, a filter panel is called inside of each module tab. |
||
293 | -! | +||
25 | +
- if (missing(msg)) {+ #' |
||
294 | -! | +||
26 | +
- msg <- sprintf(+ #' @return |
||
295 | -! | +||
27 | +
- "%s does not have the required variables: %s.",+ #' Depending on the class of `modules`, `ui_nested_tabs` returns: |
||
296 | -! | +||
28 | +
- deparse(substitute(data)),+ #' - `teal_module`: instantiated UI of the module. |
||
297 | -! | +||
29 | +
- toString(varname[!has_vars])+ #' - `teal_modules`: `tabsetPanel` with each tab corresponding to recursively |
||
298 | +30 |
- )+ #' calling this function on it. |
|
299 | +31 |
- }+ #' |
|
300 | -! | +||
32 | +
- validate(need(FALSE, msg))+ #' `srv_nested_tabs` returns a reactive which returns the active module that corresponds to the selected tab. |
||
301 | +33 |
- }+ #' |
|
302 | +34 |
- }+ #' @keywords internal |
|
303 | +35 |
- }+ NULL |
|
304 | +36 | ||
305 | +37 |
- #' Validate that variables has expected number of levels+ #' @rdname module_nested_tabs |
|
306 | +38 |
- #'+ ui_nested_tabs <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) { |
|
307 | -+ | ||
39 | +! | +
+ checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"))+ |
+ |
40 | +! |
- #' `r lifecycle::badge("stable")`+ checkmate::assert_count(depth) |
|
308 | -+ | ||
41 | +! |
- #'+ UseMethod("ui_nested_tabs", modules) |
|
309 | +42 |
- #' If the number of levels of `x` is less than `min_levels`+ } |
|
310 | +43 |
- #' or greater than `max_levels` the validation will fail.+ |
|
311 | +44 |
- #' This function is a wrapper for `shiny::validate`.+ #' @rdname module_nested_tabs |
|
312 | +45 |
- #'+ #' @export |
|
313 | +46 |
- #' @param x variable name. If `x` is not a factor, the unique values+ ui_nested_tabs.default <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) { |
|
314 | -+ | ||
47 | +! |
- #' are treated as levels.+ stop("Modules class not supported: ", paste(class(modules), collapse = " ")) |
|
315 | +48 |
- #' @param min_levels cutoff for minimum number of levels of `x`+ } |
|
316 | +49 |
- #' @param max_levels cutoff for maximum number of levels of `x`+ |
|
317 | +50 |
- #' @param var_name name of variable being validated for use in+ #' @rdname module_nested_tabs |
|
318 | +51 |
- #' validation message+ #' @export |
|
319 | +52 |
- #'+ ui_nested_tabs.teal_modules <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) { |
|
320 | -+ | ||
53 | +! |
- #' @export+ checkmate::assert_list(datasets, types = c("list", "FilteredData")) |
|
321 | -+ | ||
54 | +! |
- #' @examples+ ns <- NS(id) |
|
322 | -+ | ||
55 | +! |
- #' data <- data.frame(+ do.call( |
|
323 | -+ | ||
56 | +! |
- #' one = rep("a", length.out = 20),+ tabsetPanel, |
|
324 | -+ | ||
57 | +! |
- #' two = rep(c("a", "b"), length.out = 20),+ c( |
|
325 | +58 |
- #' three = rep(c("a", "b", "c"), length.out = 20),+ # by giving an id, we can reactively respond to tab changes |
|
326 | -+ | ||
59 | +! |
- #' four = rep(c("a", "b", "c", "d"), length.out = 20),+ list( |
|
327 | -+ | ||
60 | +! |
- #' stringsAsFactors = TRUE+ id = ns("active_tab"), |
|
328 | -+ | ||
61 | +! |
- #' )+ type = if (modules$label == "root") "pills" else "tabs" |
|
329 | +62 |
- #' ui <- fluidPage(+ ), |
|
330 | -+ | ||
63 | +! |
- #' selectInput(+ lapply( |
|
331 | -+ | ||
64 | +! |
- #' "var",+ names(modules$children), |
|
332 | -+ | ||
65 | +! |
- #' "Select variable",+ function(module_id) { |
|
333 | -+ | ||
66 | +! |
- #' choices = c("one", "two", "three", "four"),+ module_label <- modules$children[[module_id]]$label |
|
334 | -+ | ||
67 | +! |
- #' selected = "one"+ tabPanel( |
|
335 | -+ | ||
68 | +! |
- #' ),+ title = module_label, |
|
336 | -+ | ||
69 | +! |
- #' verbatimTextOutput("summary")+ value = module_id, # when clicked this tab value changes input$<tabset panel id> |
|
337 | -+ | ||
70 | +! |
- #' )+ ui_nested_tabs( |
|
338 | -+ | ||
71 | +! |
- #'+ id = ns(module_id), |
|
339 | -+ | ||
72 | +! |
- #' server <- function(input, output) {+ modules = modules$children[[module_id]], |
|
340 | -+ | ||
73 | +! |
- #' output$summary <- renderText({+ datasets = datasets[[module_label]], |
|
341 | -+ | ||
74 | +! |
- #' validate_n_levels(data[[input$var]], min_levels = 2, max_levels = 15, var_name = input$var)+ depth = depth + 1L, |
|
342 | -+ | ||
75 | +! |
- #' paste0(+ is_module_specific = is_module_specific |
|
343 | +76 |
- #' "Levels of selected treatment variable: ",+ ) |
|
344 | +77 |
- #' paste(levels(data[[input$var]]),+ ) |
|
345 | +78 |
- #' collapse = ", "+ } |
|
346 | +79 |
- #' )+ ) |
|
347 | +80 |
- #' )+ ) |
|
348 | +81 |
- #' })+ ) |
|
349 | +82 |
- #' }+ } |
|
350 | +83 |
- #' if (interactive()) {+ |
|
351 | +84 |
- #' shinyApp(ui, server)+ #' @rdname module_nested_tabs |
|
352 | +85 |
- #' }+ #' @export |
|
353 | +86 |
- validate_n_levels <- function(x, min_levels = 1, max_levels = 12, var_name) {+ ui_nested_tabs.teal_module <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) { |
|
354 | +87 | ! |
- x_levels <- if (is.factor(x)) {+ checkmate::assert_class(datasets, classes = "FilteredData") |
355 | +88 | ! |
- levels(x)+ ns <- NS(id) |
356 | +89 |
- } else {+ |
|
357 | +90 | ! |
- unique(x)+ args <- c(list(id = ns("module")), modules$ui_args) |
358 | +91 |
- }+ |
|
359 | -+ | ||
92 | +! |
-
+ teal_ui <- tags$div( |
|
360 | +93 | ! |
- if (!is.null(min_levels) && !(is.null(max_levels))) {+ id = id, |
361 | +94 | ! |
- validate(need(+ class = "teal_module", |
362 | +95 | ! |
- length(x_levels) >= min_levels && length(x_levels) <= max_levels,+ uiOutput(ns("data_reactive"), inline = TRUE), |
363 | +96 | ! |
- sprintf(+ tagList( |
364 | +97 | ! |
- "%s variable needs minimum %s level(s) and maximum %s level(s).",+ if (depth >= 2L) div(style = "mt-6"), |
365 | +98 | ! |
- var_name, min_levels, max_levels+ do.call(modules$ui, args) |
366 | +99 |
- )+ ) |
|
367 | +100 |
- ))+ ) |
|
368 | -! | +||
101 | +
- } else if (!is.null(min_levels)) {+ |
||
369 | +102 | ! |
- validate(need(+ if (!is.null(modules$datanames) && is_module_specific) { |
370 | +103 | ! |
- length(x_levels) >= min_levels,+ fluidRow( |
371 | +104 | ! |
- sprintf("%s variable needs minimum %s levels(s)", var_name, min_levels)- |
-
372 | -- |
- ))+ column(width = 9, teal_ui, class = "teal_primary_col"), |
|
373 | +105 | ! |
- } else if (!is.null(max_levels)) {+ column( |
374 | +106 | ! |
- validate(need(+ width = 3, |
375 | +107 | ! |
- length(x_levels) <= max_levels,+ datasets$ui_filter_panel(ns("module_filter_panel")), |
376 | +108 | ! |
- sprintf("%s variable needs maximum %s level(s)", var_name, max_levels)- |
-
377 | -- |
- ))- |
- |
378 | -- |
- }- |
- |
379 | -- |
- }+ class = "teal_secondary_col" |
1 | +109 |
- #' Create `teal_module` and `teal_modules` objects.+ ) |
||
2 | +110 |
- #'+ ) |
||
3 | +111 |
- #' @description+ } else { |
||
4 | -+ | |||
112 | +! |
- #' `r lifecycle::badge("stable")`+ teal_ui |
||
5 | +113 |
- #'+ } |
||
6 | +114 |
- #' Create a nested tab structure to embed modules in a `teal` application.+ } |
||
7 | +115 |
- #'+ |
||
8 | +116 |
- #' @details+ #' @rdname module_nested_tabs |
||
9 | +117 |
- #' `module()` creates an instance of a `teal_module` that can be placed in a `teal` application.+ srv_nested_tabs <- function(id, datasets, modules, is_module_specific = FALSE, |
||
10 | +118 |
- #' `modules()` shapes the structure of a the application by organizing `teal_module` within the navigation panel.+ reporter = teal.reporter::Reporter$new()) { |
||
11 | -+ | |||
119 | +50x |
- #' It wraps `teal_module` and `teal_modules` objects in a `teal_modules` object,+ checkmate::assert_multi_class(modules, c("teal_modules", "teal_module")) |
||
12 | -+ | |||
120 | +50x |
- #' which results in a nested structure corresponding to the nested tabs in the final application.+ checkmate::assert_class(reporter, "Reporter") |
||
13 | -+ | |||
121 | +49x |
- #'+ UseMethod("srv_nested_tabs", modules) |
||
14 | +122 |
- #' Note that for `modules()` `label` comes after `...`, so it must be passed as a named argument,+ } |
||
15 | +123 |
- #' otherwise it will be captured by `...`.+ |
||
16 | +124 |
- #'+ #' @rdname module_nested_tabs |
||
17 | +125 |
- #' The labels `"global_filters"` and `"Report previewer"` are reserved+ #' @export |
||
18 | +126 |
- #' because they are used by the `mapping` argument of [teal_slices()]+ srv_nested_tabs.default <- function(id, datasets, modules, is_module_specific = FALSE, |
||
19 | +127 |
- #' and the report previewer module [reporter_previewer_module()], respectively.+ reporter = teal.reporter::Reporter$new()) { |
||
20 | -+ | |||
128 | +! |
- #'+ stop("Modules class not supported: ", paste(class(modules), collapse = " ")) |
||
21 | +129 |
- #' @param label (`character(1)`) Label shown in the navigation item for the module or module group.+ } |
||
22 | +130 |
- #' For `modules()` defaults to `"root"`. See `Details`.+ |
||
23 | +131 |
- #' @param server (`function`) `shiny` module with following arguments:+ #' @rdname module_nested_tabs |
||
24 | +132 |
- #' - `id` - `teal` will set proper `shiny` namespace for this module (see [shiny::moduleServer()]).+ #' @export |
||
25 | +133 |
- #' - `input`, `output`, `session` - (not recommended) then [shiny::callModule()] will be used to call a module.+ srv_nested_tabs.teal_modules <- function(id, datasets, modules, is_module_specific = FALSE, |
||
26 | +134 |
- #' - `data` (optional) module will receive a `teal_data` object, a list of reactive (filtered) data specified in+ reporter = teal.reporter::Reporter$new()) { |
||
27 | -+ | |||
135 | +22x |
- #' the `filters` argument.+ checkmate::assert_list(datasets, types = c("list", "FilteredData")) |
||
28 | +136 |
- #' - `datasets` (optional) module will receive `FilteredData`. (See [`teal.slice::FilteredData`]).+ |
||
29 | -+ | |||
137 | +22x |
- #' - `reporter` (optional) module will receive `Reporter`. (See [`teal.reporter::Reporter`]).+ moduleServer(id = id, module = function(input, output, session) { |
||
30 | -+ | |||
138 | +22x |
- #' - `filter_panel_api` (optional) module will receive `FilterPanelAPI`. (See [`teal.slice::FilterPanelAPI`]).+ logger::log_trace("srv_nested_tabs.teal_modules initializing the module { deparse1(modules$label) }.") |
||
31 | +139 |
- #' - `...` (optional) `server_args` elements will be passed to the module named argument or to the `...`.+ |
||
32 | -+ | |||
140 | +22x |
- #' @param ui (`function`) `shiny` UI module function with following arguments:+ labels <- vapply(modules$children, `[[`, character(1), "label") |
||
33 | -+ | |||
141 | +22x |
- #' - `id` - `teal` will set proper `shiny` namespace for this module.+ modules_reactive <- sapply( |
||
34 | -+ | |||
142 | +22x |
- #' - `...` (optional) `ui_args` elements will be passed to the module named argument or to the `...`.+ names(modules$children), |
||
35 | -+ | |||
143 | +22x |
- #' @param filters (`character`) Deprecated. Use `datanames` instead.+ function(module_id) { |
||
36 | -+ | |||
144 | +33x |
- #' @param datanames (`character`) A vector with `datanames` that are relevant for the item. The+ srv_nested_tabs( |
||
37 | -+ | |||
145 | +33x |
- #' filter panel will automatically update the shown filters to include only+ id = module_id, |
||
38 | -+ | |||
146 | +33x |
- #' filters in the listed datasets. `NULL` will hide the filter panel,+ datasets = datasets[[labels[module_id]]], |
||
39 | -+ | |||
147 | +33x |
- #' and the keyword `"all"` will show filters of all datasets. `datanames` also determines+ modules = modules$children[[module_id]], |
||
40 | -+ | |||
148 | +33x |
- #' a subset of datasets which are appended to the `data` argument in server function.+ is_module_specific = is_module_specific, |
||
41 | -+ | |||
149 | +33x |
- #' @param server_args (named `list`) with additional arguments passed on to the server function.+ reporter = reporter |
||
42 | +150 |
- #' @param ui_args (named `list`) with additional arguments passed on to the UI function.+ ) |
||
43 | +151 |
- #' @param x (`teal_module` or `teal_modules`) Object to format/print.+ }, |
||
44 | -+ | |||
152 | +22x |
- #' @param indent (`integer(1)`) Indention level; each nested element is indented one level more.+ simplify = FALSE |
||
45 | +153 |
- #' @param ...+ ) |
||
46 | +154 |
- #' - For `modules()`: (`teal_module` or `teal_modules`) Objects to wrap into a tab.+ |
||
47 | +155 |
- #' - For `format()` and `print()`: Arguments passed to other methods.+ # when not ready input$active_tab would return NULL - this would fail next reactive |
||
48 | -+ | |||
156 | +22x |
- #'+ input_validated <- eventReactive(input$active_tab, input$active_tab, ignoreNULL = TRUE) |
||
49 | -+ | |||
157 | +22x |
- #' @return+ get_active_module <- reactive({ |
||
50 | -+ | |||
158 | +12x |
- #' `module()` returns an object of class `teal_module`.+ if (length(modules$children) == 1L) { |
||
51 | +159 |
- #'+ # single tab is active by default |
||
52 | -+ | |||
160 | +1x |
- #' `modules()` returns a `teal_modules` object which contains following fields:+ modules_reactive[[1]]() |
||
53 | +161 |
- #' - `label`: taken from the `label` argument.+ } else { |
||
54 | +162 |
- #' - `children`: a list containing objects passed in `...`. List elements are named after+ # switch to active tab |
||
55 | -+ | |||
163 | +11x |
- #' their `label` attribute converted to a valid `shiny` id.+ modules_reactive[[input_validated()]]() |
||
56 | +164 |
- #'+ } |
||
57 | +165 |
- #' @name teal_modules+ }) |
||
58 | +166 |
- #' @aliases teal_module+ |
||
59 | -+ | |||
167 | +22x |
- #'+ get_active_module |
||
60 | +168 |
- #' @examples+ }) |
||
61 | +169 |
- #' library(shiny)+ } |
||
62 | +170 |
- #'+ |
||
63 | +171 |
- #' module_1 <- module(+ #' @rdname module_nested_tabs |
||
64 | +172 |
- #' label = "a module",+ #' @export |
||
65 | +173 |
- #' server = function(id, data) {+ srv_nested_tabs.teal_module <- function(id, datasets, modules, is_module_specific = TRUE, |
||
66 | +174 |
- #' moduleServer(+ reporter = teal.reporter::Reporter$new()) { |
||
67 | -+ | |||
175 | +27x |
- #' id,+ checkmate::assert_class(datasets, "FilteredData") |
||
68 | -+ | |||
176 | +27x |
- #' module = function(input, output, session) {+ logger::log_trace("srv_nested_tabs.teal_module initializing the module: { deparse1(modules$label) }.") |
||
69 | +177 |
- #' output$data <- renderDataTable(data()[["iris"]])+ |
||
70 | -+ | |||
178 | +27x |
- #' }+ moduleServer(id = id, module = function(input, output, session) { |
||
71 | -+ | |||
179 | +27x |
- #' )+ if (!is.null(modules$datanames) && is_module_specific) { |
||
72 | -+ | |||
180 | +! |
- #' },+ datasets$srv_filter_panel("module_filter_panel") |
||
73 | +181 |
- #' ui = function(id) {+ } |
||
74 | +182 |
- #' ns <- NS(id)+ |
||
75 | +183 |
- #' tagList(dataTableOutput(ns("data")))+ # Create two triggers to limit reactivity between filter-panel and modules. |
||
76 | +184 |
- #' },+ # We want to recalculate only visible modules |
||
77 | +185 |
- #' datanames = "all"+ # - trigger the data when the tab is selected |
||
78 | +186 |
- #' )+ # - trigger module to be called when the tab is selected for the first time |
||
79 | -+ | |||
187 | +27x |
- #'+ trigger_data <- reactiveVal(1L) |
||
80 | -+ | |||
188 | +27x |
- #' module_2 <- module(+ trigger_module <- reactiveVal(NULL) |
||
81 | -+ | |||
189 | +27x |
- #' label = "another module",+ output$data_reactive <- renderUI({ |
||
82 | -+ | |||
190 | +17x |
- #' server = function(id) {+ lapply(datasets$datanames(), function(x) { |
||
83 | -+ | |||
191 | +21x |
- #' moduleServer(+ datasets$get_data(x, filtered = TRUE) |
||
84 | +192 |
- #' id,+ }) |
||
85 | -+ | |||
193 | +17x |
- #' module = function(input, output, session) {+ isolate(trigger_data(trigger_data() + 1)) |
||
86 | -+ | |||
194 | +17x |
- #' output$text <- renderText("Another Module")+ isolate(trigger_module(TRUE)) |
||
87 | +195 |
- #' }+ |
||
88 | -+ | |||
196 | +17x |
- #' )+ NULL |
||
89 | +197 |
- #' },+ }) |
||
90 | +198 |
- #' ui = function(id) {+ |
||
91 | +199 |
- #' ns <- NS(id)+ # collect arguments to run teal_module |
||
92 | -+ | |||
200 | +27x |
- #' tagList(textOutput(ns("text")))+ args <- c(list(id = "module"), modules$server_args) |
||
93 | -+ | |||
201 | +27x |
- #' },+ if (is_arg_used(modules$server, "reporter")) { |
||
94 | -+ | |||
202 | +! |
- #' datanames = NULL+ args <- c(args, list(reporter = reporter)) |
||
95 | +203 |
- #' )+ } |
||
96 | +204 |
- #'+ |
||
97 | -+ | |||
205 | +27x |
- #' modules <- modules(+ if (is_arg_used(modules$server, "datasets")) { |
||
98 | -+ | |||
206 | +1x |
- #' label = "modules",+ args <- c(args, datasets = datasets) |
||
99 | +207 |
- #' modules(+ } |
||
100 | +208 |
- #' label = "nested modules",+ |
||
101 | -+ | |||
209 | +27x |
- #' module_1+ if (is_arg_used(modules$server, "data")) { |
||
102 | -+ | |||
210 | +7x |
- #' ),+ data <- eventReactive(trigger_data(), .datasets_to_data(modules, datasets)) |
||
103 | -+ | |||
211 | +7x |
- #' module_2+ args <- c(args, data = list(data)) |
||
104 | +212 |
- #' )+ } |
||
105 | +213 |
- #'+ |
||
106 | -+ | |||
214 | +27x |
- #' app <- init(+ if (is_arg_used(modules$server, "filter_panel_api")) { |
||
107 | -+ | |||
215 | +2x |
- #' data = teal_data(iris = iris),+ filter_panel_api <- teal.slice::FilterPanelAPI$new(datasets) |
||
108 | -+ | |||
216 | +2x |
- #' modules = modules+ args <- c(args, filter_panel_api = filter_panel_api) |
||
109 | +217 |
- #' )+ } |
||
110 | +218 |
- #'+ |
||
111 | +219 |
- #' if (interactive()) {+ # observe the trigger_module above to induce the module once the renderUI is triggered |
||
112 | -+ | |||
220 | +27x |
- #' shinyApp(app$ui, app$server)+ observeEvent( |
||
113 | -+ | |||
221 | +27x |
- #' }+ ignoreNULL = TRUE, |
||
114 | -+ | |||
222 | +27x |
-
+ once = TRUE, |
||
115 | -+ | |||
223 | +27x |
- #' @rdname teal_modules+ eventExpr = trigger_module(), |
||
116 | -+ | |||
224 | +27x |
- #' @export+ handlerExpr = { |
||
117 | -+ | |||
225 | +17x |
- #'+ module_output <- if (is_arg_used(modules$server, "id")) { |
||
118 | -+ | |||
226 | +17x |
- module <- function(label = "module",+ do.call(modules$server, args) |
||
119 | +227 |
- server = function(id, ...) {+ } else { |
||
120 | +228 | ! |
- moduleServer(id, function(input, output, session) {}) # nolint+ do.call(callModule, c(args, list(module = modules$server))) |
|
121 | +229 |
- },+ } |
||
122 | +230 |
- ui = function(id, ...) {+ } |
||
123 | -! | +|||
231 | +
- tags$p(paste0("This module has no UI (id: ", id, " )"))+ ) |
|||
124 | +232 |
- },+ |
||
125 | -+ | |||
233 | +27x |
- filters,+ reactive(modules) |
||
126 | +234 |
- datanames = "all",+ }) |
||
127 | +235 |
- server_args = NULL,+ } |
||
128 | +236 |
- ui_args = NULL) {+ |
||
129 | +237 |
- # argument checking (independent)+ #' Convert `FilteredData` to reactive list of datasets of the `teal_data` type. |
||
130 | +238 |
- ## `label`+ #' |
||
131 | -143x | +|||
239 | +
- checkmate::assert_string(label)+ #' Converts `FilteredData` object to `teal_data` object containing datasets needed for a specific module. |
|||
132 | -140x | +|||
240 | +
- if (label == "global_filters") {+ #' Please note that if a module needs a dataset which has a parent, then the parent will also be returned. |
|||
133 | -1x | +|||
241 | +
- stop(+ #' A hash per `dataset` is calculated internally and returned in the code. |
|||
134 | -1x | +|||
242 | +
- sprintf("module(label = \"%s\", ...\n ", label),+ #' |
|||
135 | -1x | +|||
243 | +
- "Label 'global_filters' is reserved in teal. Please change to something else.",+ #' @param module (`teal_module`) module where needed filters are taken from |
|||
136 | -1x | +|||
244 | +
- call. = FALSE+ #' @param datasets (`FilteredData`) object where needed data are taken from |
|||
137 | +245 |
- )+ #' |
||
138 | +246 |
- }+ #' @return A `teal_data` object. |
||
139 | -139x | +|||
247 | +
- if (label == "Report previewer") {+ #' |
|||
140 | -! | +|||
248 | +
- stop(+ #' @keywords internal |
|||
141 | -! | +|||
249 | +
- sprintf("module(label = \"%s\", ...\n ", label),+ .datasets_to_data <- function(module, datasets) { |
|||
142 | -! | +|||
250 | +4x |
- "Label 'Report previewer' is reserved in teal. Please change to something else.",+ checkmate::assert_class(module, "teal_module") |
||
143 | -! | +|||
251 | +4x |
- call. = FALSE+ checkmate::assert_class(datasets, "FilteredData") |
||
144 | +252 |
- )+ |
||
145 | -+ | |||
253 | +4x |
- }+ datanames <- if (is.null(module$datanames) || identical(module$datanames, "all")) { |
||
146 | -+ | |||
254 | +1x |
-
+ datasets$datanames() |
||
147 | +255 |
- ## server+ } else { |
||
148 | -139x | +256 | +3x |
- checkmate::assert_function(server)+ include_parent_datanames( |
149 | -139x | +257 | +3x |
- server_formals <- names(formals(server))+ module$datanames, |
150 | -139x | +258 | +3x |
- if (!(+ datasets$get_join_keys() |
151 | -139x | +|||
259 | +
- "id" %in% server_formals ||+ ) |
|||
152 | -139x | +|||
260 | +
- all(c("input", "output", "session") %in% server_formals)+ } |
|||
153 | +261 |
- )) {+ |
||
154 | -2x | +|||
262 | +
- stop(+ # list of reactive filtered data |
|||
155 | -2x | +263 | +4x |
- "\nmodule() `server` argument requires a function with following arguments:",+ data <- sapply(datanames, function(x) datasets$get_data(x, filtered = TRUE), simplify = FALSE) |
156 | -2x | +|||
264 | +
- "\n - id - `teal` will set proper `shiny` namespace for this module.",+ |
|||
157 | -2x | +265 | +4x |
- "\n - input, output, session (not recommended) - then `shiny::callModule` will be used to call a module.",+ hashes <- calculate_hashes(datanames, datasets) |
158 | -2x | +|||
266 | +
- "\n\nFollowing arguments can be used optionaly:",+ |
|||
159 | -2x | +267 | +4x |
- "\n - `data` - module will receive list of reactive (filtered) data specified in the `filters` argument",+ code <- c( |
160 | -2x | +268 | +4x |
- "\n - `datasets` - module will receive `FilteredData`. See `help(teal.slice::FilteredData)`",+ get_rcode_str_install(), |
161 | -2x | +269 | +4x |
- "\n - `reporter` - module will receive `Reporter`. See `help(teal.reporter::Reporter)`",+ get_rcode_libraries(), |
162 | -2x | +270 | +4x |
- "\n - `filter_panel_api` - module will receive `FilterPanelAPI`. (See [teal.slice::FilterPanelAPI]).",+ get_datasets_code(datanames, datasets, hashes) |
163 | -2x | +|||
271 | +
- "\n - `...` server_args elements will be passed to the module named argument or to the `...`"+ ) |
|||
164 | +272 |
- )+ |
||
165 | +273 |
- }+ |
||
166 | -137x | +274 | +4x |
- if ("datasets" %in% server_formals) {+ data <- do.call( |
167 | -2x | +275 | +4x |
- warning(+ teal.data::teal_data, |
168 | -2x | +276 | +4x |
- sprintf("Called from module(label = \"%s\", ...)\n ", label),+ args = c(data, code = list(code), join_keys = list(datasets$get_join_keys()[datanames])) |
169 | -2x | +|||
277 | +
- "`datasets` argument in the server is deprecated and will be removed in the next release. ",+ ) |
|||
170 | -2x | +|||
278 | +
- "Please use `data` instead.",+ |
|||
171 | -2x | +279 | +4x |
- call. = FALSE+ data@verified <- attr(datasets, "verification_status") |
172 | -+ | |||
280 | +4x |
- )+ data |
||
173 | +281 |
- }+ } |
||
174 | +282 | |||
175 | +283 |
-
+ #' Get the hash of a dataset |
||
176 | +284 |
- ## UI- |
- ||
177 | -137x | -
- checkmate::assert_function(ui)- |
- ||
178 | -137x | -
- ui_formals <- names(formals(ui))- |
- ||
179 | -137x | -
- if (!"id" %in% ui_formals) {- |
- ||
180 | -1x | -
- stop(+ #' |
||
181 | -1x | +|||
285 | +
- "\nmodule() `ui` argument requires a function with following arguments:",+ #' @param datanames (`character`) names of datasets |
|||
182 | -1x | +|||
286 | +
- "\n - id - `teal` will set proper `shiny` namespace for this module.",+ #' @param datasets (`FilteredData`) object holding the data |
|||
183 | -1x | +|||
287 | +
- "\n\nFollowing arguments can be used optionally:",+ #' |
|||
184 | -1x | +|||
288 | +
- "\n - `...` ui_args elements will be passed to the module argument of the same name or to the `...`"+ #' @return A list of hashes per dataset. |
|||
185 | +289 |
- )+ #' @keywords internal |
||
186 | +290 |
- }+ #' |
||
187 | -136x | +|||
291 | +
- if (any(c("data", "datasets") %in% ui_formals)) {+ calculate_hashes <- function(datanames, datasets) { |
|||
188 | -2x | +292 | +7x |
- stop(+ sapply(datanames, function(x) rlang::hash(datasets$get_data(x, filtered = FALSE)), simplify = FALSE) |
189 | -2x | +|||
293 | +
- sprintf("Called from module(label = \"%s\", ...)\n ", label),+ } |
|||
190 | -2x | +
1 | +
- "UI with `data` or `datasets` argument is no longer accepted.\n ",+ #' Send input validation messages to output. |
|||
191 | -2x | +|||
2 | +
- "If some UI inputs depend on data, please move the logic to your server instead.\n ",+ #' |
|||
192 | -2x | +|||
3 | +
- "Possible solutions are renderUI() or updateXyzInput() functions."+ #' Captures messages from `InputValidator` objects and collates them |
|||
193 | +4 |
- )+ #' into one message passed to `validate`. |
||
194 | +5 |
- }+ #' |
||
195 | +6 |
-
+ #' `shiny::validate` is used to withhold rendering of an output element until |
||
196 | +7 |
-
+ #' certain conditions are met and to print a validation message in place |
||
197 | +8 |
- ## `filters`+ #' of the output element. |
||
198 | -134x | +|||
9 | +
- if (!missing(filters)) {+ #' `shinyvalidate::InputValidator` allows to validate input elements |
|||
199 | -! | +|||
10 | +
- datanames <- filters+ #' and to display specific messages in their respective input widgets. |
|||
200 | -! | +|||
11 | +
- msg <-+ #' `validate_inputs` provides a hybrid solution. |
|||
201 | -! | +|||
12 | +
- "The `filters` argument is deprecated and will be removed in the next release. Please use `datanames` instead."+ #' Given an `InputValidator` object, messages corresponding to inputs that fail validation |
|||
202 | -! | +|||
13 | +
- logger::log_warn(msg)+ #' are extracted and placed in one validation message that is passed to a `validate`/`need` call. |
|||
203 | -! | +|||
14 | +
- warning(msg)+ #' This way the input `validator` messages are repeated in the output. |
|||
204 | +15 |
- }+ #' |
||
205 | +16 |
-
+ #' The `...` argument accepts any number of `InputValidator` objects |
||
206 | +17 |
- ## `datanames` (also including deprecated `filters`)+ #' or a nested list of such objects. |
||
207 | +18 |
- # please note a race condition between datanames set when filters is not missing and data arg in server function+ #' If `validators` are passed directly, all their messages are printed together |
||
208 | -134x | +|||
19 | +
- if (!is.element("data", server_formals) && !is.null(datanames)) {+ #' under one (optional) header message specified by `header`. If a list is passed, |
|||
209 | -50x | +|||
20 | +
- message(sprintf("module \"%s\" server function takes no data so \"datanames\" will be ignored", label))+ #' messages are grouped by `validator`. The list's names are used as headers |
|||
210 | -50x | +|||
21 | +
- datanames <- NULL+ #' for their respective message groups. |
|||
211 | +22 |
- }+ #' If neither of the nested list elements is named, a header message is taken from `header`. |
||
212 | -134x | +|||
23 | +
- checkmate::assert_character(datanames, min.len = 1, null.ok = TRUE, any.missing = FALSE)+ #' |
|||
213 | +24 |
-
+ #' @param ... either any number of `InputValidator` objects |
||
214 | +25 |
- ## `server_args`+ #' or an optionally named, possibly nested `list` of `InputValidator` |
||
215 | -133x | +|||
26 | +
- checkmate::assert_list(server_args, null.ok = TRUE, names = "named")+ #' objects, see `Details` |
|||
216 | -131x | +|||
27 | +
- srv_extra_args <- setdiff(names(server_args), server_formals)+ #' @param header (`character(1)`) generic validation message; set to NULL to omit |
|||
217 | -131x | +|||
28 | +
- if (length(srv_extra_args) > 0 && !"..." %in% server_formals) {+ #' |
|||
218 | -1x | +|||
29 | +
- stop(+ #' @return |
|||
219 | -1x | +|||
30 | +
- "\nFollowing `server_args` elements have no equivalent in the formals of the server:\n",+ #' Returns NULL if the final validation call passes and a `shiny.silent.error` if it fails. |
|||
220 | -1x | +|||
31 | +
- paste(paste(" -", srv_extra_args), collapse = "\n"),+ #' |
|||
221 | -1x | +|||
32 | +
- "\n\nUpdate the server arguments by including above or add `...`"+ #' @seealso [`shinyvalidate::InputValidator`], [`shiny::validate`] |
|||
222 | +33 |
- )+ #' |
||
223 | +34 |
- }+ #' @examples |
||
224 | +35 |
-
+ #' library(shiny) |
||
225 | +36 |
- ## `ui_args`+ #' library(shinyvalidate) |
||
226 | -130x | +|||
37 | +
- checkmate::assert_list(ui_args, null.ok = TRUE, names = "named")+ #' |
|||
227 | -128x | +|||
38 | +
- ui_extra_args <- setdiff(names(ui_args), ui_formals)+ #' ui <- fluidPage( |
|||
228 | -128x | +|||
39 | +
- if (length(ui_extra_args) > 0 && !"..." %in% ui_formals) {+ #' selectInput("method", "validation method", c("sequential", "combined", "grouped")), |
|||
229 | -1x | +|||
40 | +
- stop(+ #' sidebarLayout( |
|||
230 | -1x | +|||
41 | +
- "\nFollowing `ui_args` elements have no equivalent in the formals of UI:\n",+ #' sidebarPanel( |
|||
231 | -1x | +|||
42 | +
- paste(paste(" -", ui_extra_args), collapse = "\n"),+ #' selectInput("letter", "select a letter:", c(letters[1:3], LETTERS[4:6])), |
|||
232 | -1x | +|||
43 | +
- "\n\nUpdate the UI arguments by including above or add `...`"+ #' selectInput("number", "select a number:", 1:6), |
|||
233 | +44 |
- )+ #' br(), |
||
234 | +45 |
- }+ #' selectInput("color", "select a color:", |
||
235 | +46 |
-
+ #' c("black", "indianred2", "springgreen2", "cornflowerblue"), |
||
236 | -127x | +|||
47 | +
- structure(+ #' multiple = TRUE |
|||
237 | -127x | +|||
48 | +
- list(+ #' ), |
|||
238 | -127x | +|||
49 | +
- label = label,+ #' sliderInput("size", "select point size:", |
|||
239 | -127x | +|||
50 | +
- server = server, ui = ui, datanames = unique(datanames),+ #' min = 0.1, max = 4, value = 0.25 |
|||
240 | -127x | +|||
51 | +
- server_args = server_args, ui_args = ui_args+ #' ) |
|||
241 | +52 |
- ),+ #' ), |
||
242 | -127x | +|||
53 | +
- class = "teal_module"+ #' mainPanel(plotOutput("plot")) |
|||
243 | +54 |
- )+ #' ) |
||
244 | +55 |
- }+ #' ) |
||
245 | +56 |
-
+ #' |
||
246 | +57 |
- #' @rdname teal_modules+ #' server <- function(input, output) { |
||
247 | +58 |
- #' @export+ #' # set up input validation |
||
248 | +59 |
- #'+ #' iv <- InputValidator$new() |
||
249 | +60 |
- modules <- function(..., label = "root") {+ #' iv$add_rule("letter", sv_in_set(LETTERS, "choose a capital letter")) |
||
250 | -99x | +|||
61 | +
- checkmate::assert_string(label)+ #' iv$add_rule("number", ~ if (as.integer(.) %% 2L == 1L) "choose an even number") |
|||
251 | -97x | +|||
62 | +
- submodules <- list(...)+ #' iv$enable() |
|||
252 | -97x | +|||
63 | +
- if (any(vapply(submodules, is.character, FUN.VALUE = logical(1)))) {+ #' # more input validation |
|||
253 | -2x | +|||
64 | +
- stop(+ #' iv_par <- InputValidator$new() |
|||
254 | -2x | +|||
65 | +
- "The only character argument to modules() must be 'label' and it must be named, ",+ #' iv_par$add_rule("color", sv_required(message = "choose a color")) |
|||
255 | -2x | +|||
66 | +
- "change modules('lab', ...) to modules(label = 'lab', ...)"+ #' iv_par$add_rule("color", ~ if (length(.) > 1L) "choose only one color") |
|||
256 | +67 |
- )+ #' iv_par$add_rule( |
||
257 | +68 |
- }+ #' "size", |
||
258 | +69 |
-
+ #' sv_between( |
||
259 | -95x | +|||
70 | +
- checkmate::assert_list(submodules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules"))+ #' left = 0.5, right = 3, |
|||
260 | +71 |
- # name them so we can more easily access the children+ #' message_fmt = "choose a value between {left} and {right}" |
||
261 | +72 |
- # beware however that the label of the submodules should not be changed as it must be kept synced+ #' ) |
||
262 | -92x | +|||
73 | +
- labels <- vapply(submodules, function(submodule) submodule$label, character(1))+ #' ) |
|||
263 | -92x | +|||
74 | +
- names(submodules) <- make.unique(gsub("[^[:alnum:]]+", "_", labels), sep = "_")+ #' iv_par$enable() |
|||
264 | -92x | +|||
75 | +
- structure(+ #' |
|||
265 | -92x | +|||
76 | +
- list(+ #' output$plot <- renderPlot({ |
|||
266 | -92x | +|||
77 | +
- label = label,+ #' # validate output |
|||
267 | -92x | +|||
78 | +
- children = submodules+ #' switch(input[["method"]], |
|||
268 | +79 |
- ),+ #' "sequential" = { |
||
269 | -92x | +|||
80 | +
- class = "teal_modules"+ #' validate_inputs(iv) |
|||
270 | +81 |
- )+ #' validate_inputs(iv_par, header = "Set proper graphical parameters") |
||
271 | +82 |
- }+ #' }, |
||
272 | +83 |
-
+ #' "combined" = validate_inputs(iv, iv_par), |
||
273 | +84 |
- # printing methods ----+ #' "grouped" = validate_inputs(list( |
||
274 | +85 |
-
+ #' "Some inputs require attention" = iv, |
||
275 | +86 |
- #' @rdname teal_modules+ #' "Set proper graphical parameters" = iv_par |
||
276 | +87 |
- #' @export+ #' )) |
||
277 | +88 |
- format.teal_module <- function(x, indent = 0, ...) { # nolint+ #' ) |
||
278 | -3x | +|||
89 | +
- paste0(paste(rep(" ", indent), collapse = ""), "+ ", x$label, "\n", collapse = "")+ #' |
|||
279 | +90 |
- }+ #' plot(eruptions ~ waiting, faithful, |
||
280 | +91 |
-
+ #' las = 1, pch = 16, |
||
281 | +92 |
-
+ #' col = input[["color"]], cex = input[["size"]] |
||
282 | +93 |
- #' @rdname teal_modules+ #' ) |
||
283 | +94 |
- #' @export+ #' }) |
||
284 | +95 |
- print.teal_module <- function(x, ...) {+ #' } |
||
285 | -! | +|||
96 | +
- cat(format(x, ...))+ #' |
|||
286 | -! | +|||
97 | +
- invisible(x)+ #' if (interactive()) { |
|||
287 | +98 |
- }+ #' shinyApp(ui, server) |
||
288 | +99 |
-
+ #' } |
||
289 | +100 |
-
+ #' |
||
290 | +101 |
- #' @rdname teal_modules+ #' @export |
||
291 | +102 |
- #' @export+ #' |
||
292 | +103 |
- format.teal_modules <- function(x, indent = 0, ...) { # nolint+ validate_inputs <- function(..., header = "Some inputs require attention") { |
||
293 | -1x | +104 | +36x |
- paste(+ dots <- list(...) |
294 | -1x | +105 | +2x |
- c(+ if (!is_validators(dots)) stop("validate_inputs accepts validators or a list thereof") |
295 | -1x | +|||
106 | +
- paste0(rep(" ", indent), "+ ", x$label, "\n"),+ |
|||
296 | -1x | +107 | +34x |
- unlist(lapply(x$children, format, indent = indent + 1, ...))+ messages <- extract_validator(dots, header) |
297 | -+ | |||
108 | +34x |
- ),+ failings <- if (!any_names(dots)) { |
||
298 | -1x | +109 | +29x |
- collapse = ""+ add_header(messages, header) |
299 | +110 |
- )+ } else { |
||
300 | -+ | |||
111 | +5x |
- }+ unlist(messages) |
||
301 | +112 |
-
+ } |
||
302 | +113 | |||
303 | -+ | |||
114 | +34x |
- #' @rdname teal_modules+ shiny::validate(shiny::need(is.null(failings), failings)) |
||
304 | +115 |
- #' @export+ } |
||
305 | +116 |
- print.teal_modules <- print.teal_module+ |
||
306 | +117 |
-
+ ### internal functions |
||
307 | +118 | |||
308 | +119 |
- # utilities ----+ #' @noRd |
||
309 | +120 |
- ## subset or modify modules ----+ #' @keywords internal |
||
310 | +121 |
-
+ # recursive object type test |
||
311 | +122 |
- #' Append a `teal_module` to `children` of a `teal_modules` object+ # returns logical of length 1 |
||
312 | +123 |
- #' @keywords internal+ is_validators <- function(x) { |
||
313 | -+ | |||
124 | +118x |
- #' @param modules (`teal_modules`)+ all(if (is.list(x)) unlist(lapply(x, is_validators)) else inherits(x, "InputValidator")) |
||
314 | +125 |
- #' @param module (`teal_module`) object to be appended onto the children of `modules`+ } |
||
315 | +126 |
- #' @return A `teal_modules` object with `module` appended.+ |
||
316 | +127 |
- append_module <- function(modules, module) {+ #' @noRd |
||
317 | -8x | +|||
128 | +
- checkmate::assert_class(modules, "teal_modules")+ #' @keywords internal |
|||
318 | -6x | +|||
129 | +
- checkmate::assert_class(module, "teal_module")+ # test if an InputValidator object is enabled |
|||
319 | -4x | +|||
130 | +
- modules$children <- c(modules$children, list(module))+ # returns logical of length 1 |
|||
320 | -4x | +|||
131 | +
- labels <- vapply(modules$children, function(submodule) submodule$label, character(1))+ # official method requested at https://github.com/rstudio/shinyvalidate/issues/64 |
|||
321 | -4x | +|||
132 | +
- names(modules$children) <- make.unique(gsub("[^[:alnum:]]", "_", tolower(labels)), sep = "_")+ validator_enabled <- function(x) { |
|||
322 | -4x | +133 | +49x |
- modules+ x$.__enclos_env__$private$enabled |
323 | +134 |
} |
||
324 | +135 | |||
325 | +136 |
- #' Extract/Remove module(s) of specific class+ #' Recursively extract messages from validator list |
||
326 | +137 |
- #'+ #' @return A character vector or a list of character vectors, possibly nested and named. |
||
327 | +138 |
- #' Given a `teal_module` or a `teal_modules`, return the elements of the structure according to `class`.+ #' @noRd |
||
328 | +139 |
- #'+ #' @keywords internal |
||
329 | +140 |
- #' @param modules (`teal_modules`)+ extract_validator <- function(iv, header) {+ |
+ ||
141 | +113x | +
+ if (inherits(iv, "InputValidator")) {+ |
+ ||
142 | +49x | +
+ add_header(gather_messages(iv), header) |
||
330 | +143 |
- #' @param class The class name of `teal_module` to be extracted or dropped.+ } else {+ |
+ ||
144 | +58x | +
+ if (is.null(names(iv))) names(iv) <- rep("", length(iv))+ |
+ ||
145 | +64x | +
+ mapply(extract_validator, iv = iv, header = names(iv), SIMPLIFY = FALSE) |
||
331 | +146 |
- #' @keywords internal+ } |
||
332 | +147 |
- #' @return+ } |
||
333 | +148 |
- #' - For `extract_module`, a `teal_module` of class `class` or `teal_modules` containing modules of class `class`.+ |
||
334 | +149 |
- #' - For `drop_module`, the opposite, which is all `teal_modules` of class other than `class`.+ #' Collate failing messages from validator. |
||
335 | +150 |
- #' @rdname module_management+ #' @return `list` |
||
336 | +151 |
- extract_module <- function(modules, class) {+ #' @noRd |
||
337 | -20x | +|||
152 | +
- if (inherits(modules, class)) {+ #' @keywords internal |
|||
338 | -! | +|||
153 | +
- modules+ gather_messages <- function(iv) { |
|||
339 | -20x | +154 | +49x |
- } else if (inherits(modules, "teal_module")) {+ if (validator_enabled(iv)) { |
340 | -11x | +155 | +46x |
- NULL+ status <- iv$validate() |
341 | -9x | +156 | +46x |
- } else if (inherits(modules, "teal_modules")) {+ failing_inputs <- Filter(Negate(is.null), status) |
342 | -9x | +157 | +46x |
- Filter(function(x) length(x) > 0L, lapply(modules$children, extract_module, class))+ unique(lapply(failing_inputs, function(x) x[["message"]])) |
343 | +158 |
- }+ } else { |
||
344 | -+ | |||
159 | +3x |
- }+ warning("Validator is disabled and will be omitted.") |
||
345 | -+ | |||
160 | +3x |
-
+ list() |
||
346 | +161 |
- #' @keywords internal+ } |
||
347 | +162 |
- #' @return `teal_modules`+ } |
||
348 | +163 |
- #' @rdname module_management+ |
||
349 | +164 |
- drop_module <- function(modules, class) {- |
- ||
350 | -! | -
- if (inherits(modules, class)) {- |
- ||
351 | -! | -
- NULL+ #' Add optional header to failing messages |
||
352 | -! | +|||
165 | +
- } else if (inherits(modules, "teal_module")) {+ #' @noRd |
|||
353 | -! | +|||
166 | +
- modules+ #' @keywords internal |
|||
354 | -! | +|||
167 | +
- } else if (inherits(modules, "teal_modules")) {+ add_header <- function(messages, header = "") { |
|||
355 | -! | +|||
168 | +78x |
- do.call(+ ans <- unlist(messages) |
||
356 | -! | +|||
169 | +78x |
- "modules",+ if (length(ans) != 0L && isTRUE(nchar(header) > 0L)) { |
||
357 | -! | +|||
170 | +31x |
- c(Filter(function(x) length(x) > 0L, lapply(modules$children, drop_module, class)), label = modules$label)+ ans <- c(paste0(header, "\n"), ans, "\n") |
||
358 | +171 |
- )+ } |
||
359 | -+ | |||
172 | +78x |
- }+ ans |
||
360 | +173 |
} |
||
361 | +174 | |||
362 | +175 |
- ## read modules ----+ #' Recursively check if the object contains a named list |
||
363 | +176 |
-
+ #' @noRd |
||
364 | +177 |
- #' Does the object make use of the `arg`+ #' @keywords internal |
||
365 | +178 |
- #'+ any_names <- function(x) { |
||
366 | -+ | |||
179 | +103x |
- #' @param modules (`teal_module` or `teal_modules`) object+ any( |
||
367 | -+ | |||
180 | +103x |
- #' @param arg (`character(1)`) names of the arguments to be checked against formals of `teal` modules.+ if (is.list(x)) { |
||
368 | -+ | |||
181 | +58x |
- #' @return `logical` whether the object makes use of `arg`.+ if (!is.null(names(x)) && any(names(x) != "")) TRUE else unlist(lapply(x, any_names)) |
||
369 | +182 |
- #' @rdname is_arg_used+ } else {+ |
+ ||
183 | +40x | +
+ FALSE |
||
370 | +184 |
- #' @keywords internal+ } |
||
371 | +185 |
- is_arg_used <- function(modules, arg) {+ ) |
||
372 | -286x | +|||
186 | +
- checkmate::assert_string(arg)+ } |
|||
373 | -283x | +
1 | +
- if (inherits(modules, "teal_modules")) {+ # This module is the main teal module that puts everything together. |
|||
374 | -29x | +|||
2 | +
- any(unlist(lapply(modules$children, is_arg_used, arg)))+ |
|||
375 | -254x | +|||
3 | +
- } else if (inherits(modules, "teal_module")) {+ #' `teal` main app module |
|||
376 | -43x | +|||
4 | +
- is_arg_used(modules$server, arg) || is_arg_used(modules$ui, arg)+ #' |
|||
377 | -211x | +|||
5 | +
- } else if (is.function(modules)) {+ #' This is the main `teal` app that puts everything together. |
|||
378 | -209x | +|||
6 | +
- isTRUE(arg %in% names(formals(modules)))+ #' |
|||
379 | +7 |
- } else {+ #' It displays the splash UI which is used to fetch the data, possibly |
||
380 | -2x | +|||
8 | +
- stop("is_arg_used function not implemented for this object")+ #' prompting for a password input to fetch the data. Once the data is ready, |
|||
381 | +9 |
- }+ #' the splash screen is replaced by the actual `teal` UI that is tabsetted and |
||
382 | +10 |
- }+ #' has a filter panel with `datanames` that are relevant for the current tab. |
||
383 | +11 |
-
+ #' Nested tabs are possible, but we limit it to two nesting levels for reasons |
||
384 | +12 |
-
+ #' of clarity of the UI. |
||
385 | +13 |
- #' Get module depth+ #' |
||
386 | +14 |
- #'+ #' The splash screen functionality can also be used |
||
387 | +15 |
- #' Depth starts at 0, so a single `teal.module` has depth 0.+ #' for non-delayed data which takes time to load into memory, avoiding |
||
388 | +16 |
- #' Nesting it increases overall depth by 1.+ #' `shiny` session timeouts. |
||
389 | +17 |
#' |
||
390 | +18 |
- #' @inheritParams init+ #' Server evaluates the `teal_data_rv` (delayed data mechanism) and creates the |
||
391 | +19 |
- #' @param depth optional, integer determining current depth level+ #' `datasets` object that is shared across modules. |
||
392 | +20 |
- #'+ #' Once it is ready and non-`NULL`, the splash screen is replaced by the |
||
393 | +21 |
- #' @return Depth level for given module.+ #' main `teal` UI that depends on the data. |
||
394 | +22 |
- #' @examples+ #' The currently active tab is tracked and the right filter panel |
||
395 | +23 |
- #' # use non-exported function from teal+ #' updates the displayed datasets to filter for according to the active `datanames` |
||
396 | +24 |
- #' modules_depth <- getFromNamespace("modules_depth", "teal")+ #' of the tab. |
||
397 | +25 |
#' |
||
398 | +26 |
- #' mods <- modules(+ #' It is written as a `shiny` module so it can be added into other apps as well. |
||
399 | +27 |
- #' label = "d1",+ #' |
||
400 | +28 |
- #' modules(+ #' @name module_teal |
||
401 | +29 |
- #' label = "d2",+ #' |
||
402 | +30 |
- #' modules(+ #' @inheritParams module_teal_with_splash |
||
403 | +31 |
- #' label = "d3",+ #' |
||
404 | +32 |
- #' module(label = "aaa1"), module(label = "aaa2"), module(label = "aaa3")+ #' @param splash_ui (`shiny.tag`) UI to display initially, |
||
405 | +33 |
- #' ),+ #' can be a splash screen or a `shiny` module UI. For the latter, see |
||
406 | +34 |
- #' module(label = "bbb")+ #' [init()] about how to call the corresponding server function. |
||
407 | +35 |
- #' ),+ #' |
||
408 | +36 |
- #' module(label = "ccc")+ #' @param teal_data_rv (`reactive`) |
||
409 | +37 |
- #' )+ #' returns the `teal_data`, only evaluated once, `NULL` value is ignored |
||
410 | +38 |
- #' stopifnot(modules_depth(mods) == 3L)+ #' |
||
411 | +39 |
- #'+ #' @return |
||
412 | +40 | ++ |
+ #' Returns a `reactive` expression which returns the currently active module.+ |
+ |
41 |
- #' mods <- modules(+ #' |
|||
413 | +42 |
- #' label = "a",+ #' @keywords internal |
||
414 | +43 |
- #' modules(+ #' |
||
415 | +44 |
- #' label = "b1", module(label = "c")+ NULL |
||
416 | +45 |
- #' ),+ |
||
417 | +46 |
- #' module(label = "b2")+ #' @rdname module_teal |
||
418 | +47 |
- #' )+ ui_teal <- function(id, |
||
419 | +48 |
- #' stopifnot(modules_depth(mods) == 2L)+ splash_ui = tags$h2("Starting the Teal App"), |
||
420 | +49 |
- #' @keywords internal+ title = build_app_title(), |
||
421 | +50 |
- modules_depth <- function(modules, depth = 0L) {+ header = tags$p(), |
||
422 | -12x | +|||
51 | +
- checkmate::assert_multi_class(modules, c("teal_module", "teal_modules"))+ footer = tags$p()) { |
|||
423 | -12x | +52 | +7x |
- checkmate::assert_int(depth, lower = 0)+ checkmate::assert_character(id, max.len = 1, any.missing = FALSE) |
424 | -11x | +|||
53 | +
- if (inherits(modules, "teal_modules")) {+ |
|||
425 | -4x | +54 | +7x |
- max(vapply(modules$children, modules_depth, integer(1), depth = depth + 1L))+ checkmate::assert_multi_class(splash_ui, c("shiny.tag", "shiny.tag.list", "html")) |
426 | +55 |
- } else {+ |
||
427 | +56 | 7x |
- depth+ if (is.character(title)) { |
|
428 | -+ | |||
57 | +! |
- }+ title <- build_app_title(title) |
||
429 | +58 |
- }+ } else { |
||
430 | -+ | |||
59 | +7x |
-
+ validate_app_title_tag(title) |
||
431 | +60 |
- #' Retrieve labels from `teal_modules`+ } |
||
432 | +61 |
- #'+ |
||
433 | -+ | |||
62 | +7x |
- #' @param modules (`teal_modules`)+ checkmate::assert( |
||
434 | -+ | |||
63 | +7x |
- #' @return A `list` containing the labels of the modules. If the modules are nested,+ .var.name = "header", |
||
435 | -+ | |||
64 | +7x |
- #' the function returns a nested `list` of labels.+ checkmate::check_string(header), |
||
436 | -+ | |||
65 | +7x |
- #' @keywords internal+ checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html")) |
||
437 | +66 |
- module_labels <- function(modules) {+ ) |
||
438 | -! | +|||
67 | +7x |
- if (inherits(modules, "teal_modules")) {+ if (checkmate::test_string(header)) { |
||
439 | +68 | ! |
- lapply(modules$children, module_labels)+ header <- tags$p(header) |
|
440 | +69 |
- } else {+ } |
||
441 | -! | +|||
70 | +
- modules$label+ |
|||
442 | -+ | |||
71 | +7x |
- }+ checkmate::assert( |
||
443 | -+ | |||
72 | +7x |
- }+ .var.name = "footer", |
1 | -+ | ||
73 | +7x |
- #' Create a UI of nested tabs of `teal_modules`+ checkmate::check_string(footer), |
|
2 | -+ | ||
74 | +7x |
- #'+ checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html")) |
|
3 | +75 |
- #' @section `ui_nested_tabs`:+ ) |
|
4 | -+ | ||
76 | +7x |
- #' Each `teal_modules` is translated to a `tabsetPanel` and each+ if (checkmate::test_string(footer)) { |
|
5 | -+ | ||
77 | +! |
- #' of its children is another tab-module called recursively. The UI of a+ footer <- tags$p(footer) |
|
6 | +78 |
- #' `teal_module` is obtained by calling its UI function.+ } |
|
7 | +79 |
- #'+ |
|
8 | -+ | ||
80 | +7x |
- #' The `datasets` argument is required to resolve the `teal` arguments in an+ ns <- NS(id) |
|
9 | +81 |
- #' isolated context (with respect to reactivity).+ |
|
10 | +82 |
- #'+ # Once the data is loaded, we will remove this element and add the real teal UI instead |
|
11 | -+ | ||
83 | +7x |
- #' @section `srv_nested_tabs`:+ splash_ui <- div( |
|
12 | +84 |
- #' This module recursively calls all elements of `modules` and returns currently active one.+ # id so we can remove the splash screen once ready, which is the first child of this container |
|
13 | -+ | ||
85 | +7x |
- #' - `teal_module` returns self as a active module.+ id = ns("main_ui_container"), |
|
14 | +86 |
- #' - `teal_modules` also returns module active within self which is determined by the `input$active_tab`.+ # we put it into a div, so it can easily be removed as a whole, also when it is a tagList (and not |
|
15 | +87 |
- #'+ # just the first item of the tagList) |
|
16 | -+ | ||
88 | +7x |
- #' @name module_nested_tabs+ div(splash_ui) |
|
17 | +89 |
- #'+ ) |
|
18 | +90 |
- #' @inheritParams module_tabs_with_filters+ |
|
19 | +91 |
- #'+ # show busy icon when `shiny` session is busy computing stuff |
|
20 | +92 |
- #' @param depth (`integer(1)`)+ # based on https://stackoverflow.com/questions/17325521/r-shiny-display-loading-message-while-function-is-running/22475216#22475216 #nolint |
|
21 | -+ | ||
93 | +7x |
- #' number which helps to determine depth of the modules nesting.+ shiny_busy_message_panel <- conditionalPanel( |
|
22 | -+ | ||
94 | +7x |
- #' @param is_module_specific (`logical(1)`)+ condition = "(($('html').hasClass('shiny-busy')) && (document.getElementById('shiny-notification-panel') == null))", # nolint |
|
23 | -+ | ||
95 | +7x |
- #' flag determining if the filter panel is global or module-specific.+ div( |
|
24 | -+ | ||
96 | +7x |
- #' When set to `TRUE`, a filter panel is called inside of each module tab.+ icon("arrows-rotate", "spin fa-spin"), |
|
25 | -+ | ||
97 | +7x |
- #'+ "Computing ...", |
|
26 | +98 |
- #' @return+ # CSS defined in `custom.css` |
|
27 | -+ | ||
99 | +7x |
- #' Depending on the class of `modules`, `ui_nested_tabs` returns:+ class = "shinybusymessage" |
|
28 | +100 |
- #' - `teal_module`: instantiated UI of the module.+ ) |
|
29 | +101 |
- #' - `teal_modules`: `tabsetPanel` with each tab corresponding to recursively+ ) |
|
30 | +102 |
- #' calling this function on it.+ |
|
31 | -+ | ||
103 | +7x |
- #'+ fluidPage( |
|
32 | -+ | ||
104 | +7x |
- #' `srv_nested_tabs` returns a reactive which returns the active module that corresponds to the selected tab.+ title = title, |
|
33 | -+ | ||
105 | +7x |
- #'+ theme = get_teal_bs_theme(), |
|
34 | -+ | ||
106 | +7x |
- #' @examples+ include_teal_css_js(), |
|
35 | -+ | ||
107 | +7x |
- #' # use non-exported function from teal+ tags$header(header), |
|
36 | -+ | ||
108 | +7x |
- #' include_teal_css_js <- getFromNamespace("include_teal_css_js", "teal")+ tags$hr(class = "my-2"), |
|
37 | -+ | ||
109 | +7x |
- #' teal_data_to_filtered_data <- getFromNamespace("teal_data_to_filtered_data", "teal")+ shiny_busy_message_panel, |
|
38 | -+ | ||
110 | +7x |
- #' ui_nested_tabs <- getFromNamespace("ui_nested_tabs", "teal")+ splash_ui, |
|
39 | -+ | ||
111 | +7x |
- #' srv_nested_tabs <- getFromNamespace("srv_nested_tabs", "teal")+ tags$hr(), |
|
40 | -+ | ||
112 | +7x |
- #'+ tags$footer( |
|
41 | -+ | ||
113 | +7x |
- #' # create `teal_data`+ div( |
|
42 | -+ | ||
114 | +7x |
- #' data <- teal_data(iris = iris, mtcars = mtcars)+ footer, |
|
43 | -+ | ||
115 | +7x |
- #' datanames <- datanames(data)+ teal.widgets::verbatim_popup_ui(ns("sessionInfo"), "Session Info", type = "link"), |
|
44 | -+ | ||
116 | +7x |
- #'+ textOutput(ns("identifier")) |
|
45 | +117 |
- #' # creates a hierarchy of `teal_modules` from which a `teal` app can be created.+ ) |
|
46 | +118 |
- #' mods <- modules(+ ) |
|
47 | +119 |
- #' label = "d1",+ ) |
|
48 | +120 |
- #' modules(+ } |
|
49 | +121 |
- #' label = "d2",+ |
|
50 | +122 |
- #' modules(+ |
|
51 | +123 |
- #' label = "d3",+ #' @rdname module_teal |
|
52 | +124 |
- #' example_module(label = "aaa1", datanames = datanames),+ srv_teal <- function(id, modules, teal_data_rv, filter = teal_slices()) { |
|
53 | -+ | ||
125 | +19x |
- #' example_module(label = "aaa2", datanames = datanames)+ stopifnot(is.reactive(teal_data_rv)) |
|
54 | -+ | ||
126 | +18x |
- #' ),+ moduleServer(id, function(input, output, session) { |
|
55 | -+ | ||
127 | +18x |
- #' example_module(label = "bbb", datanames = datanames)+ logger::log_trace("srv_teal initializing the module.") |
|
56 | +128 |
- #' ),+ |
|
57 | -+ | ||
129 | +18x |
- #' example_module(label = "ccc", datanames = datanames)+ output$identifier <- renderText( |
|
58 | -+ | ||
130 | +18x |
- #' )+ paste0("Pid:", Sys.getpid(), " Token:", substr(session$token, 25, 32)) |
|
59 | +131 |
- #'+ ) |
|
60 | +132 |
- #' # creates nested list aligned with the module hierarchy created above,+ |
|
61 | -+ | ||
133 | +18x |
- #' # each leaf holding the same `FilteredData` object.+ teal.widgets::verbatim_popup_srv( |
|
62 | -+ | ||
134 | +18x |
- #' datasets <- teal_data_to_filtered_data(data)+ "sessionInfo", |
|
63 | -+ | ||
135 | +18x |
- #' datasets <- list(+ verbatim_content = utils::capture.output(utils::sessionInfo()), |
|
64 | -+ | ||
136 | +18x |
- #' "d2" = list(+ title = "SessionInfo" |
|
65 | +137 |
- #' "d3" = list(+ ) |
|
66 | +138 |
- #' "aaa1" = datasets,+ |
|
67 | +139 |
- #' "aaa2" = datasets+ # `JavaScript` code |
|
68 | -+ | ||
140 | +18x |
- #' ),+ run_js_files(files = "init.js") |
|
69 | +141 |
- #' "bbb" = datasets+ |
|
70 | +142 |
- #' ),+ # set timezone in shiny app |
|
71 | +143 |
- #' "ccc" = datasets+ # timezone is set in the early beginning so it will be available also |
|
72 | +144 |
- #' )+ # for `DDL` and all shiny modules |
|
73 | -+ | ||
145 | +18x |
- #'+ get_client_timezone(session$ns) |
|
74 | -+ | ||
146 | +18x |
- #' ui <- function() {+ observeEvent( |
|
75 | -+ | ||
147 | +18x |
- #' tagList(+ eventExpr = input$timezone, |
|
76 | -+ | ||
148 | +18x |
- #' include_teal_css_js(),+ once = TRUE, |
|
77 | -+ | ||
149 | +18x |
- #' textOutput("info"),+ handlerExpr = { |
|
78 | -+ | ||
150 | +! |
- #' fluidPage( # needed for nice tabs+ session$userData$timezone <- input$timezone |
|
79 | -+ | ||
151 | +! |
- #' ui_nested_tabs("dummy", modules = mods, datasets = datasets)+ logger::log_trace("srv_teal@1 Timezone set to client's timezone: { input$timezone }.") |
|
80 | +152 |
- #' )+ } |
|
81 | +153 |
- #' )+ ) |
|
82 | +154 |
- #' }+ |
|
83 | -+ | ||
155 | +18x |
- #' server <- function(input, output, session) {+ reporter <- teal.reporter::Reporter$new() |
|
84 | -+ | ||
156 | +18x |
- #' active_module <- srv_nested_tabs(+ if (is_arg_used(modules, "reporter") && length(extract_module(modules, "teal_module_previewer")) == 0) { |
|
85 | -+ | ||
157 | +! |
- #' "dummy",+ modules <- append_module(modules, reporter_previewer_module()) |
|
86 | +158 |
- #' datasets = datasets,+ } |
|
87 | +159 |
- #' modules = mods+ |
|
88 | -+ | ||
160 | +18x |
- #' )+ env <- environment() |
|
89 | -+ | ||
161 | +18x |
- #' output$info <- renderText({+ datasets_reactive <- eventReactive(teal_data_rv(), { |
|
90 | -+ | ||
162 | +4x |
- #' paste0("The currently active tab name is ", active_module()$label)+ env$progress <- shiny::Progress$new(session) |
|
91 | -+ | ||
163 | +4x |
- #' })+ env$progress$set(0.25, message = "Setting data") |
|
92 | +164 |
- #' }+ |
|
93 | +165 |
- #' if (interactive()) {+ # create a list of data following structure of the nested modules list structure. |
|
94 | +166 |
- #' shinyApp(ui, server)+ # Because it's easier to unpack modules and datasets when they follow the same nested structure. |
|
95 | -+ | ||
167 | +4x |
- #' }+ datasets_singleton <- teal_data_to_filtered_data(teal_data_rv()) |
|
96 | +168 |
- #'+ |
|
97 | +169 |
- #' @keywords internal+ # Singleton starts with only global filters active. |
|
98 | -+ | ||
170 | +4x |
- NULL+ filter_global <- Filter(function(x) x$id %in% attr(filter, "mapping")$global_filters, filter) |
|
99 | -+ | ||
171 | +4x |
-
+ datasets_singleton$set_filter_state(filter_global) |
|
100 | +172 |
- #' @rdname module_nested_tabs+ |
|
101 | -+ | ||
173 | +4x |
- ui_nested_tabs <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) {+ module_datasets <- function(modules) { |
|
102 | -! | +||
174 | +18x |
- checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"))+ if (inherits(modules, "teal_modules")) { |
|
103 | -! | +||
175 | +7x |
- checkmate::assert_count(depth)+ datasets <- lapply(modules$children, module_datasets) |
|
104 | -! | +||
176 | +7x |
- UseMethod("ui_nested_tabs", modules)+ labels <- vapply(modules$children, `[[`, character(1), "label") |
|
105 | -+ | ||
177 | +7x |
- }+ names(datasets) <- labels |
|
106 | -+ | ||
178 | +7x |
-
+ datasets |
|
107 | -+ | ||
179 | +11x |
- #' @rdname module_nested_tabs+ } else if (isTRUE(attr(filter, "module_specific"))) { |
|
108 | +180 |
- #' @export+ # we should create FilteredData even if modules$datanames is null |
|
109 | +181 |
- ui_nested_tabs.default <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) {+ # null controls a display of filter panel but data should be still passed |
|
110 | -! | +||
182 | +3x |
- stop("Modules class not supported: ", paste(class(modules), collapse = " "))+ datanames <- if (is.null(modules$datanames) || modules$datanames == "all") { |
|
111 | -+ | ||
183 | +3x |
- }+ include_parent_datanames( |
|
112 | -+ | ||
184 | +3x |
-
+ teal_data_datanames(teal_data_rv()), |
|
113 | -+ | ||
185 | +3x |
- #' @rdname module_nested_tabs+ teal.data::join_keys(teal_data_rv()) |
|
114 | +186 |
- #' @export+ ) |
|
115 | +187 |
- ui_nested_tabs.teal_modules <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) {+ } else { |
|
116 | +188 | ! |
- checkmate::assert_list(datasets, types = c("list", "FilteredData"))+ modules$datanames |
117 | -! | +||
189 | +
- ns <- NS(id)+ } |
||
118 | -! | +||
190 | +
- do.call(+ # todo: subset teal_data to datanames |
||
119 | -! | +||
191 | +3x |
- tabsetPanel,+ datasets_module <- teal_data_to_filtered_data(teal_data_rv(), datanames = datanames) |
|
120 | -! | +||
192 | +
- c(+ |
||
121 | +193 |
- # by giving an id, we can reactively respond to tab changes+ # set initial filters |
|
122 | -! | +||
194 | +
- list(+ # - filtering filters for this module |
||
123 | -! | +||
195 | +3x |
- id = ns("active_tab"),+ slices <- Filter(x = filter, f = function(x) { |
|
124 | +196 | ! |
- type = if (modules$label == "root") "pills" else "tabs"- |
-
125 | -- |
- ),+ x$id %in% unique(unlist(attr(filter, "mapping")[c(modules$label, "global_filters")])) && |
|
126 | +197 | ! |
- lapply(+ x$dataname %in% datanames |
127 | -! | +||
198 | +
- names(modules$children),+ }) |
||
128 | -! | +||
199 | +3x |
- function(module_id) {+ include_varnames <- attr(slices, "include_varnames")[names(attr(slices, "include_varnames")) %in% datanames] |
|
129 | -! | +||
200 | +3x |
- module_label <- modules$children[[module_id]]$label+ exclude_varnames <- attr(slices, "exclude_varnames")[names(attr(slices, "exclude_varnames")) %in% datanames] |
|
130 | -! | +||
201 | +3x |
- tabPanel(+ slices$include_varnames <- include_varnames |
|
131 | -! | +||
202 | +3x |
- title = module_label,+ slices$exclude_varnames <- exclude_varnames |
|
132 | -! | +||
203 | +3x |
- value = module_id, # when clicked this tab value changes input$<tabset panel id>+ datasets_module$set_filter_state(slices) |
|
133 | -! | +||
204 | +3x |
- ui_nested_tabs(+ datasets_module |
|
134 | -! | +||
205 | +
- id = ns(module_id),+ } else { |
||
135 | -! | +||
206 | +8x |
- modules = modules$children[[module_id]],+ datasets_singleton |
|
136 | -! | +||
207 | +
- datasets = datasets[[module_label]],+ } |
||
137 | -! | +||
208 | +
- depth = depth + 1L,+ } |
||
138 | -! | +||
209 | +4x |
- is_module_specific = is_module_specific+ module_datasets(modules) |
|
139 | +210 |
- )+ }) |
|
140 | +211 |
- )+ |
|
141 | +212 |
- }+ # Replace splash / welcome screen once data is loaded ---- |
|
142 | +213 |
- )+ # ignoreNULL to not trigger at the beginning when data is NULL |
|
143 | +214 |
- )+ # just handle it once because data obtained through delayed loading should |
|
144 | +215 |
- )+ # usually not change afterwards |
|
145 | +216 |
- }+ # if restored from bookmarked state, `filter` is ignored |
|
146 | +217 | ||
147 | -+ | ||
218 | +18x |
- #' @rdname module_nested_tabs+ observeEvent(datasets_reactive(), once = TRUE, { |
|
148 | -+ | ||
219 | +! |
- #' @export+ logger::log_trace("srv_teal@5 setting main ui after data was pulled") |
|
149 | -+ | ||
220 | +! |
- ui_nested_tabs.teal_module <- function(id, modules, datasets, depth = 0L, is_module_specific = FALSE) {+ on.exit(env$progress$close()) |
|
150 | +221 | ! |
- checkmate::assert_class(datasets, classes = "FilteredData")+ env$progress$set(0.5, message = "Setting up main UI") |
151 | +222 | ! |
- ns <- NS(id)+ datasets <- datasets_reactive() |
152 | +223 | ||
224 | ++ |
+ # main_ui_container contains splash screen first and we remove it and replace it by the real UI+ |
+ |
153 | +225 | ! |
- args <- c(list(id = ns("module")), modules$ui_args)+ removeUI(sprintf("#%s > div:nth-child(1)", session$ns("main_ui_container"))) |
154 | -+ | ||
226 | +! |
-
+ insertUI( |
|
155 | +227 | ! |
- teal_ui <- tags$div(+ selector = paste0("#", session$ns("main_ui_container")), |
156 | +228 | ! |
- id = id,+ where = "beforeEnd",+ |
+
229 | ++ |
+ # we put it into a div, so it can easily be removed as a whole, also when it is a tagList (and not+ |
+ |
230 | ++ |
+ # just the first item of the tagList) |
|
157 | +231 | ! |
- class = "teal_module",+ ui = div(ui_tabs_with_filters( |
158 | +232 | ! |
- uiOutput(ns("data_reactive"), inline = TRUE),+ session$ns("main_ui"), |
159 | +233 | ! |
- tagList(+ modules = modules, |
160 | +234 | ! |
- if (depth >= 2L) div(style = "mt-6"),+ datasets = datasets, |
161 | +235 | ! |
- do.call(modules$ui, args)+ filter = filter |
162 | +236 |
- )+ )), |
|
163 | +237 |
- )+ # needed so that the UI inputs are available and can be immediately updated, otherwise, updating may not |
|
164 | +238 |
-
+ # have any effect as they are ignored when not present |
|
165 | +239 | ! |
- if (!is.null(modules$datanames) && is_module_specific) {+ immediate = TRUE+ |
+
240 | ++ |
+ )+ |
+ |
241 | ++ | + + | +|
242 | ++ |
+ # must make sure that this is only executed once as modules assume their observers are only+ |
+ |
243 | ++ |
+ # registered once (calling server functions twice would trigger observers twice each time) |
|
166 | +244 | ! |
- fluidRow(+ srv_tabs_with_filters( |
167 | +245 | ! |
- column(width = 9, teal_ui, class = "teal_primary_col"),+ id = "main_ui", |
168 | +246 | ! |
- column(+ datasets = datasets, |
169 | +247 | ! |
- width = 3,+ modules = modules, |
170 | +248 | ! |
- datasets$ui_filter_panel(ns("module_filter_panel")),+ reporter = reporter, |
171 | +249 | ! |
- class = "teal_secondary_col"+ filter = filter |
172 | +250 |
) |
|
173 | +251 |
- )+ }) |
|
174 | +252 |
- } else {+ }) |
|
175 | -! | +||
253 | +
- teal_ui+ } |
176 | +1 |
- }+ #' Filter settings for `teal` applications |
||
177 | +2 |
- }+ #' |
||
178 | +3 |
-
+ #' Specify initial filter states and filtering settings for a `teal` app. |
||
179 | +4 |
- #' @rdname module_nested_tabs+ #' |
||
180 | +5 |
- srv_nested_tabs <- function(id, datasets, modules, is_module_specific = FALSE,+ #' Produces a `teal_slices` object. |
||
181 | +6 |
- reporter = teal.reporter::Reporter$new()) {+ #' The `teal_slice` components will specify filter states that will be active when the app starts. |
||
182 | -50x | +|||
7 | +
- checkmate::assert_multi_class(modules, c("teal_modules", "teal_module"))+ #' Attributes (created with the named arguments) will configure the way the app applies filters. |
|||
183 | -50x | +|||
8 | +
- checkmate::assert_class(reporter, "Reporter")+ #' See argument descriptions for details. |
|||
184 | -49x | +|||
9 | +
- UseMethod("srv_nested_tabs", modules)+ #' |
|||
185 | +10 |
- }+ #' @inheritParams teal.slice::teal_slices |
||
186 | +11 |
-
+ #' |
||
187 | +12 |
- #' @rdname module_nested_tabs+ #' @param module_specific optional (`logical(1)`) |
||
188 | +13 |
- #' @export+ #' - `FALSE` (default) when one filter panel applied to all modules. |
||
189 | +14 |
- srv_nested_tabs.default <- function(id, datasets, modules, is_module_specific = FALSE,+ #' All filters will be shared by all modules. |
||
190 | +15 |
- reporter = teal.reporter::Reporter$new()) {+ #' - `TRUE` when filter panel module-specific. |
||
191 | -! | +|||
16 | +
- stop("Modules class not supported: ", paste(class(modules), collapse = " "))+ #' Modules can have different set of filters specified - see `mapping` argument. |
|||
192 | +17 |
- }+ #' @param mapping `r lifecycle::badge("experimental")` |
||
193 | +18 |
-
+ #' _This is a new feature. Do kindly share your opinions on |
||
194 | +19 |
- #' @rdname module_nested_tabs+ #' [`teal`'s GitHub repository](https://github.com/insightsengineering/teal/)._ |
||
195 | +20 |
- #' @export+ #' |
||
196 | +21 |
- srv_nested_tabs.teal_modules <- function(id, datasets, modules, is_module_specific = FALSE,+ #' (named `list`) specifies which filters will be active in which modules on app start. |
||
197 | +22 |
- reporter = teal.reporter::Reporter$new()) {+ #' Elements should contain character vector of `teal_slice` `id`s (see [`teal.slice::teal_slice`]). |
||
198 | -22x | +|||
23 | +
- checkmate::assert_list(datasets, types = c("list", "FilteredData"))+ #' Names of the list should correspond to `teal_module` `label` set in [module()] function. |
|||
199 | +24 |
-
+ #' - `id`s listed under `"global_filters` will be active in all modules. |
||
200 | -22x | +|||
25 | +
- moduleServer(id = id, module = function(input, output, session) {+ #' - If missing, all filters will be applied to all modules. |
|||
201 | -22x | +|||
26 | +
- logger::log_trace("srv_nested_tabs.teal_modules initializing the module { deparse1(modules$label) }.")+ #' - If empty list, all filters will be available to all modules but will start inactive. |
|||
202 | +27 |
-
+ #' - If `module_specific` is `FALSE`, only `global_filters` will be active on start. |
||
203 | -22x | +|||
28 | +
- labels <- vapply(modules$children, `[[`, character(1), "label")+ #' @param app_id (`character(1)`) |
|||
204 | -22x | +|||
29 | +
- modules_reactive <- sapply(+ #' For internal use only, do not set manually. |
|||
205 | -22x | +|||
30 | +
- names(modules$children),+ #' Added by `init` so that a `teal_slices` can be matched to the app in which it was used. |
|||
206 | -22x | +|||
31 | +
- function(module_id) {+ #' Used for verifying snapshots uploaded from file. See `snapshot`. |
|||
207 | -33x | +|||
32 | +
- srv_nested_tabs(+ #' |
|||
208 | -33x | +|||
33 | +
- id = module_id,+ #' @param x (`list`) of lists to convert to `teal_slices` |
|||
209 | -33x | +|||
34 | +
- datasets = datasets[[labels[module_id]]],+ #' |
|||
210 | -33x | +|||
35 | +
- modules = modules$children[[module_id]],+ #' @return |
|||
211 | -33x | +|||
36 | +
- is_module_specific = is_module_specific,+ #' A `teal_slices` object. |
|||
212 | -33x | +|||
37 | +
- reporter = reporter+ #' |
|||
213 | +38 |
- )+ #' @seealso [`teal.slice::teal_slices`], [`teal.slice::teal_slice`], [slices_store()] |
||
214 | +39 |
- },+ #' |
||
215 | -22x | +|||
40 | +
- simplify = FALSE+ #' @examples |
|||
216 | +41 |
- )+ #' filter <- teal_slices( |
||
217 | +42 |
-
+ #' teal_slice(dataname = "iris", varname = "Species", id = "species"), |
||
218 | +43 |
- # when not ready input$active_tab would return NULL - this would fail next reactive+ #' teal_slice(dataname = "iris", varname = "Sepal.Length", id = "sepal_length"), |
||
219 | -22x | +|||
44 | +
- input_validated <- eventReactive(input$active_tab, input$active_tab, ignoreNULL = TRUE)+ #' teal_slice( |
|||
220 | -22x | +|||
45 | +
- get_active_module <- reactive({+ #' dataname = "iris", id = "long_petals", title = "Long petals", expr = "Petal.Length > 5" |
|||
221 | -12x | +|||
46 | +
- if (length(modules$children) == 1L) {+ #' ), |
|||
222 | +47 |
- # single tab is active by default+ #' teal_slice(dataname = "mtcars", varname = "mpg", id = "mtcars_mpg"), |
||
223 | -1x | +|||
48 | +
- modules_reactive[[1]]()+ #' mapping = list( |
|||
224 | +49 |
- } else {+ #' module1 = c("species", "sepal_length"), |
||
225 | +50 |
- # switch to active tab+ #' module2 = c("mtcars_mpg"), |
||
226 | -11x | +|||
51 | +
- modules_reactive[[input_validated()]]()+ #' global_filters = "long_petals" |
|||
227 | +52 |
- }+ #' ) |
||
228 | +53 |
- })+ #' ) |
||
229 | +54 |
-
+ #' |
||
230 | -22x | +|||
55 | +
- get_active_module+ #' app <- init( |
|||
231 | +56 |
- })+ #' data = teal_data(iris = iris, mtcars = mtcars), |
||
232 | +57 |
- }+ #' modules = list( |
||
233 | +58 |
-
+ #' module("module1"), |
||
234 | +59 |
- #' @rdname module_nested_tabs+ #' module("module2") |
||
235 | +60 |
- #' @export+ #' ), |
||
236 | +61 |
- srv_nested_tabs.teal_module <- function(id, datasets, modules, is_module_specific = TRUE,+ #' filter = filter |
||
237 | +62 |
- reporter = teal.reporter::Reporter$new()) {+ #' ) |
||
238 | -27x | +|||
63 | +
- checkmate::assert_class(datasets, "FilteredData")+ #' |
|||
239 | -27x | +|||
64 | +
- logger::log_trace("srv_nested_tabs.teal_module initializing the module: { deparse1(modules$label) }.")+ #' if (interactive()) { |
|||
240 | +65 |
-
+ #' shinyApp(app$ui, app$server) |
||
241 | -27x | +|||
66 | +
- moduleServer(id = id, module = function(input, output, session) {+ #' } |
|||
242 | -27x | +|||
67 | +
- if (!is.null(modules$datanames) && is_module_specific) {+ #' |
|||
243 | -! | +|||
68 | +
- datasets$srv_filter_panel("module_filter_panel")+ #' @export |
|||
244 | +69 |
- }+ teal_slices <- function(..., |
||
245 | +70 |
-
+ exclude_varnames = NULL, |
||
246 | +71 |
- # Create two triggers to limit reactivity between filter-panel and modules.+ include_varnames = NULL, |
||
247 | +72 |
- # We want to recalculate only visible modules+ count_type = NULL, |
||
248 | +73 |
- # - trigger the data when the tab is selected+ allow_add = TRUE, |
||
249 | +74 |
- # - trigger module to be called when the tab is selected for the first time+ module_specific = FALSE, |
||
250 | -27x | +|||
75 | +
- trigger_data <- reactiveVal(1L)+ mapping, |
|||
251 | -27x | +|||
76 | +
- trigger_module <- reactiveVal(NULL)+ app_id = NULL) { |
|||
252 | -27x | +77 | +78x |
- output$data_reactive <- renderUI({+ shiny::isolate({ |
253 | -17x | +78 | +78x |
- lapply(datasets$datanames(), function(x) {+ checkmate::assert_flag(allow_add) |
254 | -21x | -
- datasets$get_data(x, filtered = TRUE)- |
- ||
255 | -+ | 79 | +78x |
- })+ checkmate::assert_flag(module_specific) |
256 | -17x | +80 | +32x |
- isolate(trigger_data(trigger_data() + 1))+ if (!missing(mapping)) checkmate::assert_list(mapping, types = c("character", "NULL"), names = "named") |
257 | -17x | +81 | +75x |
- isolate(trigger_module(TRUE))+ checkmate::assert_string(app_id, null.ok = TRUE) |
258 | +82 | |||
259 | -17x | +83 | +75x |
- NULL+ slices <- list(...) |
260 | -+ | |||
84 | +75x |
- })+ all_slice_id <- vapply(slices, `[[`, character(1L), "id") |
||
261 | +85 | |||
262 | -- |
- # collect arguments to run teal_module- |
- ||
263 | -27x | +86 | +75x |
- args <- c(list(id = "module"), modules$server_args)+ if (missing(mapping)) { |
264 | -27x | -
- if (is_arg_used(modules$server, "reporter")) {- |
- ||
265 | -! | +87 | +46x |
- args <- c(args, list(reporter = reporter))+ mapping <- list(global_filters = all_slice_id) |
266 | +88 |
} |
||
267 | -- | - - | -||
268 | -27x | +89 | +75x |
- if (is_arg_used(modules$server, "datasets")) {+ if (!module_specific) { |
269 | -1x | +90 | +71x |
- args <- c(args, datasets = datasets)+ mapping[setdiff(names(mapping), "global_filters")] <- NULL |
270 | +91 |
} |
||
271 | +92 | |||
272 | -27x | +93 | +75x |
- if (is_arg_used(modules$server, "data")) {+ failed_slice_id <- setdiff(unlist(mapping), all_slice_id) |
273 | -7x | +94 | +75x |
- data <- eventReactive(trigger_data(), .datasets_to_data(modules, datasets))+ if (length(failed_slice_id)) { |
274 | -7x | -
- args <- c(args, data = list(data))- |
- ||
275 | -- |
- }- |
- ||
276 | -+ | 95 | +1x |
-
+ stop(sprintf( |
277 | -27x | +96 | +1x |
- if (is_arg_used(modules$server, "filter_panel_api")) {+ "Filters in mapping don't match any available filter.\n %s not in %s", |
278 | -2x | +97 | +1x |
- filter_panel_api <- teal.slice::FilterPanelAPI$new(datasets)+ toString(failed_slice_id), |
279 | -2x | +98 | +1x |
- args <- c(args, filter_panel_api = filter_panel_api)+ toString(all_slice_id) |
280 | +99 |
- }+ )) |
||
281 | +100 |
-
+ } |
||
282 | +101 |
- # observe the trigger_module above to induce the module once the renderUI is triggered- |
- ||
283 | -27x | -
- observeEvent(+ |
||
284 | -27x | +102 | +74x |
- ignoreNULL = TRUE,+ tss <- teal.slice::teal_slices( |
285 | -27x | +|||
103 | +
- once = TRUE,+ ..., |
|||
286 | -27x | +104 | +74x |
- eventExpr = trigger_module(),+ exclude_varnames = exclude_varnames, |
287 | -27x | +105 | +74x |
- handlerExpr = {+ include_varnames = include_varnames, |
288 | -17x | +106 | +74x |
- module_output <- if (is_arg_used(modules$server, "id")) {+ count_type = count_type, |
289 | -17x | +107 | +74x |
- do.call(modules$server, args)+ allow_add = allow_add |
290 | +108 |
- } else {- |
- ||
291 | -! | -
- do.call(callModule, c(args, list(module = modules$server)))+ ) |
||
292 | -+ | |||
109 | +74x |
- }+ attr(tss, "mapping") <- mapping |
||
293 | -+ | |||
110 | +74x |
- }+ attr(tss, "module_specific") <- module_specific |
||
294 | -+ | |||
111 | +74x |
- )+ attr(tss, "app_id") <- app_id |
||
295 | -+ | |||
112 | +74x |
-
+ class(tss) <- c("modules_teal_slices", class(tss)) |
||
296 | -27x | +113 | +74x |
- reactive(modules)+ tss |
297 | +114 |
}) |
||
298 | +115 |
} |
||
299 | +116 | |||
300 | +117 |
- #' Convert `FilteredData` to reactive list of datasets of the `teal_data` type.+ |
||
301 | +118 |
- #'+ #' @rdname teal_slices |
||
302 | +119 |
- #' Converts `FilteredData` object to `teal_data` object containing datasets needed for a specific module.+ #' @export |
||
303 | +120 |
- #' Please note that if a module needs a dataset which has a parent, then the parent will also be returned.+ #' @keywords internal |
||
304 | +121 |
- #' A hash per `dataset` is calculated internally and returned in the code.+ #' |
||
305 | +122 |
- #'+ as.teal_slices <- function(x) { # nolint |
||
306 | -+ | |||
123 | +10x |
- #' @param module (`teal_module`) module where needed filters are taken from+ checkmate::assert_list(x) |
||
307 | -+ | |||
124 | +10x |
- #' @param datasets (`FilteredData`) object where needed data are taken from+ lapply(x, checkmate::assert_list, names = "named", .var.name = "list element") |
||
308 | +125 |
- #'+ |
||
309 | -+ | |||
126 | +10x |
- #' @return A `teal_data` object.+ attrs <- attributes(unclass(x)) |
||
310 | -+ | |||
127 | +10x |
- #'+ ans <- lapply(x, function(x) if (is.teal_slice(x)) x else as.teal_slice(x)) |
||
311 | -+ | |||
128 | +10x |
- #' @keywords internal+ do.call(teal_slices, c(ans, attrs)) |
||
312 | +129 |
- .datasets_to_data <- function(module, datasets) {- |
- ||
313 | -4x | -
- checkmate::assert_class(module, "teal_module")+ } |
||
314 | -4x | +|||
130 | +
- checkmate::assert_class(datasets, "FilteredData")+ |
|||
315 | +131 | |||
316 | -4x | +|||
132 | +
- datanames <- if (is.null(module$datanames) || identical(module$datanames, "all")) {+ #' @rdname teal_slices |
|||
317 | -1x | +|||
133 | +
- datasets$datanames()+ #' @export |
|||
318 | +134 |
- } else {+ #' @keywords internal |
||
319 | -3x | +|||
135 | +
- include_parent_datanames(+ #' |
|||
320 | -3x | +|||
136 | +
- module$datanames,+ c.teal_slices <- function(...) { |
|||
321 | -3x | +|||
137 | +! |
- datasets$get_join_keys()+ x <- list(...) |
||
322 | -+ | |||
138 | +! |
- )+ checkmate::assert_true(all(vapply(x, is.teal_slices, logical(1L))), .var.name = "all arguments are teal_slices") |
||
323 | +139 |
- }+ |
||
324 | -+ | |||
140 | +! |
-
+ all_attributes <- lapply(x, attributes) |
||
325 | -+ | |||
141 | +! |
- # list of reactive filtered data+ all_attributes <- coalesce_r(all_attributes) |
||
326 | -4x | +|||
142 | +! |
- data <- sapply(datanames, function(x) datasets$get_data(x, filtered = TRUE), simplify = FALSE)+ all_attributes <- all_attributes[names(all_attributes) != "class"] |
||
327 | +143 | |||
328 | -4x | +|||
144 | +! |
- hashes <- calculate_hashes(datanames, datasets)+ do.call( |
||
329 | -+ | |||
145 | +! |
-
+ teal_slices, |
||
330 | -4x | +|||
146 | +! |
- code <- c(+ c( |
||
331 | -4x | +|||
147 | +! |
- get_rcode_str_install(),+ unique(unlist(x, recursive = FALSE)), |
||
332 | -4x | +|||
148 | +! |
- get_rcode_libraries(),+ all_attributes |
||
333 | -4x | +|||
149 | +
- get_datasets_code(datanames, datasets, hashes)+ ) |
|||
334 | +150 |
) |
||
335 | +151 |
-
+ } |
||
336 | +152 | |||
337 | -4x | -
- data <- do.call(- |
- ||
338 | -4x | +|||
153 | +
- teal.data::teal_data,+ |
|||
339 | -4x | +|||
154 | +
- args = c(data, code = list(code), join_keys = list(datasets$get_join_keys()[datanames]))+ #' Deep copy `teal_slices` |
|||
340 | +155 |
- )+ #' |
||
341 | +156 |
-
+ #' it's important to create a new copy of `teal_slices` when |
||
342 | -4x | +|||
157 | +
- data@verified <- attr(datasets, "verification_status")+ #' starting a new `shiny` session. Otherwise, object will be shared |
|||
343 | -4x | +|||
158 | +
- data+ #' by multiple users as it is created in global environment before |
|||
344 | +159 |
- }+ #' `shiny` session starts. |
||
345 | +160 |
-
+ #' @param filter (`teal_slices`) |
||
346 | +161 |
- #' Get the hash of a dataset+ #' @return `teal_slices` |
||
347 | +162 |
- #'+ #' @keywords internal |
||
348 | +163 |
- #' @param datanames (`character`) names of datasets+ deep_copy_filter <- function(filter) { |
||
349 | -+ | |||
164 | +1x |
- #' @param datasets (`FilteredData`) object holding the data+ checkmate::assert_class(filter, "teal_slices") |
||
350 | -+ | |||
165 | +1x |
- #'+ shiny::isolate({ |
||
351 | -+ | |||
166 | +1x |
- #' @return A list of hashes per dataset.+ filter_copy <- lapply(filter, function(slice) { |
||
352 | -+ | |||
167 | +2x |
- #' @keywords internal+ teal.slice::as.teal_slice(as.list(slice)) |
||
353 | +168 |
- #'+ }) |
||
354 | -+ | |||
169 | +1x |
- calculate_hashes <- function(datanames, datasets) {+ attributes(filter_copy) <- attributes(filter) |
||
355 | -7x | +170 | +1x |
- sapply(datanames, function(x) rlang::hash(datasets$get_data(x, filtered = FALSE)), simplify = FALSE)+ filter_copy |
356 | +171 | ++ |
+ })+ |
+ |
172 |
}@@ -22952,14 +20938,14 @@ teal coverage - 68.02% |
1 |
- #' Create a `tdata` object+ #' Store and restore `teal_slices` object |
||
3 |
- #' @description `r lifecycle::badge("deprecated")`+ #' Functions that write a `teal_slices` object to a file in the `JSON` format, |
||
4 |
- #'+ #' and also restore the object from disk. |
||
5 |
- #' Create a new object called `tdata` which contains `data`, a `reactive` list of `data.frames`+ #' |
||
6 |
- #' (or `MultiAssayExperiment`), with attributes:+ #' Date and date time objects are stored in the following formats: |
||
7 |
- #' - `code` (`reactive`) containing code used to generate the data+ #' |
||
8 |
- #' - join_keys (`join_keys`) containing the relationships between the data+ #' - `Date` class is converted to the `"ISO8601"` standard (`YYYY-MM-DD`). |
||
9 |
- #' - metadata (named `list`) containing any metadata associated with the data frames+ #' - `POSIX*t` classes are converted to character by using |
||
10 |
- #'+ #' `format.POSIX*t(usetz = TRUE, tz = "UTC")` (`YYYY-MM-DD HH:MM:SS UTC`, where |
||
11 |
- #' @name tdata+ #' `UTC` is the `Coordinated Universal Time` timezone short-code). |
||
12 |
- #' @param data (named `list`) A list of `data.frame` or `MultiAssayExperiment` objects,+ #' |
||
13 |
- #' which optionally can be `reactive`.+ #' This format is assumed during `slices_restore`. All `POSIX*t` objects in |
||
14 |
- #' Inside this object all of these items will be made `reactive`.+ #' `selected` or `choices` fields of `teal_slice` objects are always printed in |
||
15 |
- #' @param code (`character` or `reactive` which evaluates to a `character`) containing+ #' `UTC` timezone as well. |
||
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+ #' @param tss (`teal_slices`) object to be stored. |
||
18 |
- #' object `code` will be made reactive+ #' @param file (`character(1)`) file path where `teal_slices` object will be |
||
19 |
- #' @param join_keys (`teal.data::join_keys`) object containing relationships between the+ #' saved and restored. The file extension should be `".json"`. |
||
20 |
- #' datasets.+ #' |
||
21 |
- #' @param metadata (named `list`) each element contains a list of metadata about the named `data.frame`+ #' @return `slices_store` returns `NULL`, invisibly. |
||
22 |
- #' Each element of these list should be atomic and length one.+ #' |
||
23 |
- #' @return A `tdata` object.+ #' @seealso [teal_slices()] |
||
25 |
- #' @seealso `as_tdata`+ #' @keywords internal |
||
27 |
- #' @examples+ slices_store <- function(tss, file) { |
||
28 | -+ | 9x |
- #'+ checkmate::assert_class(tss, "teal_slices") |
29 | -+ | 9x |
- #' data <- new_tdata(+ checkmate::assert_path_for_output(file, overwrite = TRUE, extension = "json") |
30 |
- #' data = list(iris = iris, mtcars = reactive(mtcars), dd = data.frame(x = 1:10)),+ |
||
31 | -+ | 9x |
- #' code = "iris <- iris+ cat(format(tss, trim_lines = FALSE), "\n", file = file) |
32 |
- #' mtcars <- mtcars+ } |
||
33 |
- #' dd <- data.frame(x = 1:10)",+ |
||
34 |
- #' metadata = list(dd = list(author = "NEST"), iris = list(version = 1))+ #' @rdname slices_store |
||
35 |
- #' )+ #' @return `slices_restore` returns a `teal_slices` object restored from the file. |
||
36 |
- #'+ #' @keywords internal |
||
37 |
- #' # Extract a data.frame+ slices_restore <- function(file) { |
||
38 | -+ | 9x |
- #' isolate(data[["iris"]]())+ checkmate::assert_file_exists(file, access = "r", extension = "json") |
39 |
- #'+ |
||
40 | -+ | 9x |
- #' # Get code+ tss_json <- jsonlite::fromJSON(file, simplifyDataFrame = FALSE) |
41 | -+ | 9x |
- #' isolate(get_code_tdata(data))+ tss_json$slices <- |
42 | -+ | 9x |
- #'+ lapply(tss_json$slices, function(slice) { |
43 | -+ | 9x |
- #' # Get metadata+ for (field in c("selected", "choices")) { |
44 | -+ | 18x |
- #' get_metadata(data, "iris")+ if (!is.null(slice[[field]])) { |
45 | -+ | 12x |
- #'+ if (length(slice[[field]]) > 0) { |
46 | -+ | 9x |
- #' @export+ date_partial_regex <- "^[0-9]{4}-[0-9]{2}-[0-9]{2}" |
47 | -+ | 9x |
- new_tdata <- function(data, code = "", join_keys = NULL, metadata = NULL) {+ time_stamp_regex <- paste0(date_partial_regex, "\\s[0-9]{2}:[0-9]{2}:[0-9]{2}\\sUTC$") |
48 | -34x | +
- lifecycle::deprecate_soft(+ |
|
49 | -34x | +9x |
- when = "0.99.0",+ slice[[field]] <- |
50 | -34x | +9x |
- what = "tdata()",+ if (all(grepl(paste0(date_partial_regex, "$"), slice[[field]]))) { |
51 | -34x | +3x |
- details = paste(+ as.Date(slice[[field]]) |
52 | -34x | +9x |
- "tdata is deprecated and will be removed in the next release. Use `teal_data` instead.\n",+ } else if (all(grepl(time_stamp_regex, slice[[field]]))) { |
53 | -34x | +3x |
- "Please follow migration instructions https://github.com/insightsengineering/teal/discussions/987."+ as.POSIXct(slice[[field]], tz = "UTC") |
54 |
- )+ } else { |
||
55 | -+ | 3x |
- )+ slice[[field]] |
56 | -34x | +
- checkmate::assert_list(+ } |
|
57 | -34x | +
- data,+ } else { |
|
58 | -34x | +3x |
- any.missing = FALSE, names = "unique",+ slice[[field]] <- character(0) |
59 | -34x | +
- types = c("data.frame", "reactive", "MultiAssayExperiment")+ }+ |
+ |
60 | ++ |
+ }+ |
+ |
61 | ++ |
+ }+ |
+ |
62 | +9x | +
+ slice+ |
+ |
63 | ++ |
+ })+ |
+ |
64 | ++ | + + | +|
65 | +9x | +
+ tss_elements <- lapply(tss_json$slices, as.teal_slice)+ |
+ |
66 | ++ | + + | +|
67 | +9x | +
+ do.call(teal_slices, c(tss_elements, tss_json$attributes))+ |
+ |
68 | ++ |
+ }+ |
+
1 | ++ |
+ #' Create a `tdata` object+ |
+ ||
2 | ++ |
+ #'+ |
+ ||
3 | ++ |
+ #' @description `r lifecycle::badge("deprecated")`+ |
+ ||
4 | ++ |
+ #' |
||
60 | +5 |
- )+ #' Create a new object called `tdata` which contains `data`, a `reactive` list of `data.frames` |
||
61 | -30x | +|||
6 | +
- checkmate::assert_class(join_keys, "join_keys", null.ok = TRUE)+ #' (or `MultiAssayExperiment`), with attributes: |
|||
62 | -29x | +|||
7 | +
- checkmate::assert_multi_class(code, c("character", "reactive"))+ #' - `code` (`reactive`) containing code used to generate the data |
|||
63 | +8 |
-
+ #' - join_keys (`join_keys`) containing the relationships between the data |
||
64 | -28x | +|||
9 | +
- checkmate::assert_list(metadata, names = "unique", null.ok = TRUE)+ #' - metadata (named `list`) containing any metadata associated with the data frames |
|||
65 | -26x | +|||
10 | +
- checkmate::assert_subset(names(metadata), names(data))+ #' |
|||
66 | +11 |
-
+ #' @name tdata |
||
67 | -25x | +|||
12 | +
- if (is.reactive(code)) {+ #' @param data (named `list`) A list of `data.frame` or `MultiAssayExperiment` objects, |
|||
68 | -9x | +|||
13 | +
- isolate(checkmate::assert_class(code(), "character", .var.name = "code"))+ #' which optionally can be `reactive`. |
|||
69 | +14 |
- }+ #' Inside this object all of these items will be made `reactive`. |
||
70 | +15 |
-
+ #' @param code (`character` or `reactive` which evaluates to a `character`) containing |
||
71 | +16 |
- # create reactive data.frames+ #' the code used to generate the data. This should be `reactive` if the code is changing |
||
72 | -24x | +|||
17 | +
- for (x in names(data)) {+ #' during a reactive context (e.g. if filtering changes the code). Inside this |
|||
73 | -47x | +|||
18 | +
- if (!is.reactive(data[[x]])) {+ #' object `code` will be made reactive |
|||
74 | -31x | +|||
19 | +
- data[[x]] <- do.call(reactive, list(as.name(x)), envir = list2env(data[x]))+ #' @param join_keys (`teal.data::join_keys`) object containing relationships between the |
|||
75 | +20 |
- }+ #' datasets. |
||
76 | +21 |
- }+ #' @param metadata (named `list`) each element contains a list of metadata about the named `data.frame` |
||
77 | +22 |
-
+ #' Each element of these list should be atomic and length one. |
||
78 | +23 |
- # set attributes+ #' @return A `tdata` object. |
||
79 | -24x | +|||
24 | +
- attr(data, "code") <- if (is.reactive(code)) code else reactive(code)+ #' |
|||
80 | -24x | +|||
25 | +
- attr(data, "join_keys") <- join_keys+ #' @seealso `as_tdata` |
|||
81 | -24x | +|||
26 | +
- attr(data, "metadata") <- metadata+ #' |
|||
82 | +27 |
-
+ #' @examples |
||
83 | +28 |
- # set class+ #' |
||
84 | -24x | +|||
29 | +
- class(data) <- c("tdata", class(data))+ #' data <- new_tdata( |
|||
85 | -24x | +|||
30 | +
- data+ #' data = list(iris = iris, mtcars = reactive(mtcars), dd = data.frame(x = 1:10)), |
|||
86 | +31 |
- }+ #' code = "iris <- iris |
||
87 | +32 |
-
+ #' mtcars <- mtcars |
||
88 | +33 |
- #' Function to convert a `tdata` object to an `environment`+ #' dd <- data.frame(x = 1:10)", |
||
89 | +34 |
- #'+ #' metadata = list(dd = list(author = "NEST"), iris = list(version = 1)) |
||
90 | +35 |
- #' Any `reactive` expressions inside `tdata` are evaluated first.+ #' ) |
||
91 | +36 |
- #' @param data (`tdata`) object+ #' |
||
92 | +37 |
- #' @return An `environment`.+ #' # Extract a data.frame |
||
93 | +38 |
- #' @examples+ #' isolate(data[["iris"]]()) |
||
94 | +39 |
#' |
||
95 | +40 |
- #' data <- new_tdata(+ #' # Get code |
||
96 | +41 |
- #' data = list(iris = iris, mtcars = reactive(mtcars)),+ #' isolate(get_code_tdata(data)) |
||
97 | +42 |
- #' code = "iris <- iris+ #' |
||
98 | +43 |
- #' mtcars = mtcars"+ #' # Get metadata |
||
99 | +44 |
- #' )+ #' get_metadata(data, "iris") |
||
100 | +45 |
#' |
||
101 | +46 |
- #' my_env <- isolate(tdata2env(data))+ #' @export |
||
102 | +47 |
- #'+ new_tdata <- function(data, code = "", join_keys = NULL, metadata = NULL) { |
||
103 | -+ | |||
48 | +34x |
- #' @export+ lifecycle::deprecate_soft( |
||
104 | -+ | |||
49 | +34x |
- tdata2env <- function(data) { # nolint+ when = "0.99.0", |
||
105 | -2x | +50 | +34x | +
+ what = "tdata()",+ |
+
51 | +34x | +
+ details = paste(+ |
+ ||
52 | +34x |
- checkmate::assert_class(data, "tdata")+ "tdata is deprecated and will be removed in the next release. Use `teal_data` instead.\n", |
||
106 | -1x | +53 | +34x |
- list2env(lapply(data, function(x) if (is.reactive(x)) x() else x))+ "Please follow migration instructions https://github.com/insightsengineering/teal/discussions/987." |
107 | +54 |
- }+ ) |
||
108 | +55 |
-
+ ) |
||
109 | -+ | |||
56 | +34x |
-
+ checkmate::assert_list( |
||
110 | -+ | |||
57 | +34x |
- #' Wrapper for `get_code.tdata`+ data, |
||
111 | -+ | |||
58 | +34x |
- #'+ any.missing = FALSE, names = "unique", |
||
112 | -+ | |||
59 | +34x |
- #' This wrapper is to be used by downstream packages to extract the code of a `tdata` object.+ types = c("data.frame", "reactive", "MultiAssayExperiment") |
||
113 | +60 |
- #'+ ) |
||
114 | -+ | |||
61 | +30x |
- #' @param data (`tdata`) object+ checkmate::assert_class(join_keys, "join_keys", null.ok = TRUE) |
||
115 | -+ | |||
62 | +29x |
- #'+ checkmate::assert_multi_class(code, c("character", "reactive")) |
||
116 | +63 |
- #' @return (`character`) code used in the `tdata` object.+ |
||
117 | -+ | |||
64 | +28x |
- #' @export+ checkmate::assert_list(metadata, names = "unique", null.ok = TRUE)+ |
+ ||
65 | +26x | +
+ checkmate::assert_subset(names(metadata), names(data)) |
||
118 | +66 |
- get_code_tdata <- function(data) {+ |
||
119 | -7x | +67 | +25x |
- checkmate::assert_class(data, "tdata")+ if (is.reactive(code)) { |
120 | -5x | +68 | +9x |
- attr(data, "code")()+ isolate(checkmate::assert_class(code(), "character", .var.name = "code")) |
121 | +69 |
- }+ } |
||
122 | +70 | |||
123 | +71 |
- #' Extract `join_keys` from `tdata`+ # create reactive data.frames |
||
124 | -+ | |||
72 | +24x |
- #' @param data (`tdata`) object+ for (x in names(data)) { |
||
125 | -+ | |||
73 | +47x |
- #' @param ... Additional arguments (not used)+ if (!is.reactive(data[[x]])) { |
||
126 | -+ | |||
74 | +31x |
- #' @export+ data[[x]] <- do.call(reactive, list(as.name(x)), envir = list2env(data[x])) |
||
127 | +75 |
- join_keys.tdata <- function(data, ...) {- |
- ||
128 | -2x | -
- attr(data, "join_keys")+ } |
||
129 | +76 |
- }+ } |
||
130 | +77 | |||
131 | +78 |
- #' Function to get metadata from a `tdata` object+ # set attributes |
||
132 | -+ | |||
79 | +24x |
- #' @param data (`tdata` - object) to extract the data from+ attr(data, "code") <- if (is.reactive(code)) code else reactive(code) |
||
133 | -+ | |||
80 | +24x |
- #' @param dataname (`character(1)`) the dataset name whose metadata is requested+ attr(data, "join_keys") <- join_keys |
||
134 | -+ | |||
81 | +24x |
- #' @return Either list of metadata or NULL if no metadata.+ attr(data, "metadata") <- metadata |
||
135 | +82 |
- #' @export+ |
||
136 | +83 |
- get_metadata <- function(data, dataname) {+ # set class |
||
137 | -4x | +84 | +24x |
- checkmate::assert_string(dataname)+ class(data) <- c("tdata", class(data)) |
138 | -4x | +85 | +24x |
- UseMethod("get_metadata", data)+ data |
139 | +86 |
} |
||
140 | +87 | |||
141 | -- |
- #' @rdname get_metadata- |
- ||
142 | +88 |
- #' @export+ #' Function to convert a `tdata` object to an `environment` |
||
143 | +89 |
- get_metadata.tdata <- function(data, dataname) {- |
- ||
144 | -4x | -
- metadata <- attr(data, "metadata")- |
- ||
145 | -4x | -
- if (is.null(metadata)) {- |
- ||
146 | -1x | -
- return(NULL)+ #' |
||
147 | +90 |
- }- |
- ||
148 | -3x | -
- metadata[[dataname]]+ #' Any `reactive` expressions inside `tdata` are evaluated first. |
||
149 | +91 |
- }+ #' @param data (`tdata`) object |
||
150 | +92 |
-
+ #' @return An `environment`. |
||
151 | +93 |
- #' @rdname get_metadata+ #' @examples |
||
152 | +94 |
- #' @export+ #' |
||
153 | +95 |
- get_metadata.default <- function(data, dataname) {+ #' data <- new_tdata( |
||
154 | -! | +|||
96 | +
- stop("get_metadata function not implemented for this object")+ #' data = list(iris = iris, mtcars = reactive(mtcars)), |
|||
155 | +97 |
- }+ #' code = "iris <- iris |
||
156 | +98 |
-
+ #' mtcars = mtcars" |
||
157 | +99 |
-
+ #' ) |
||
158 | +100 |
- #' Downgrade `teal_data` objects in modules for compatibility+ #' |
||
159 | +101 |
- #'+ #' my_env <- isolate(tdata2env(data)) |
||
160 | +102 |
- #' Convert `teal_data` to `tdata` in `teal` modules.+ #' |
||
161 | +103 |
- #'+ #' @export |
||
162 | +104 |
- #' Recent changes in `teal` cause modules to fail because modules expect a `tdata` object+ tdata2env <- function(data) { # nolint+ |
+ ||
105 | +2x | +
+ checkmate::assert_class(data, "tdata")+ |
+ ||
106 | +1x | +
+ list2env(lapply(data, function(x) if (is.reactive(x)) x() else x)) |
||
163 | +107 |
- #' to be passed to the `data` argument but instead they receive a `teal_data` object,+ } |
||
164 | +108 |
- #' which is additionally wrapped in a reactive expression in the server functions.+ |
||
165 | +109 |
- #' In order to easily adapt such modules without a proper refactor,+ |
||
166 | +110 |
- #' use this function to downgrade the `data` argument.+ #' Wrapper for `get_code.tdata` |
||
167 | +111 |
#' |
||
168 | +112 |
- #' @param x data object, either `tdata` or `teal_data`, the latter possibly in a reactive expression+ #' This wrapper is to be used by downstream packages to extract the code of a `tdata` object. |
||
169 | +113 |
#' |
||
170 | +114 |
- #' @return Object of class `tdata`.+ #' @param data (`tdata`) object |
||
171 | +115 |
#' |
||
172 | +116 |
- #' @examples+ #' @return (`character`) code used in the `tdata` object. |
||
173 | +117 |
- #' td <- teal_data()+ #' @export |
||
174 | +118 |
- #' td <- within(td, iris <- iris) %>% within(mtcars <- mtcars)+ get_code_tdata <- function(data) { |
||
175 | -+ | |||
119 | +7x |
- #' td+ checkmate::assert_class(data, "tdata")+ |
+ ||
120 | +5x | +
+ attr(data, "code")() |
||
176 | +121 |
- #' as_tdata(td)+ } |
||
177 | +122 |
- #' as_tdata(reactive(td))+ |
||
178 | +123 |
- #'+ #' Extract `join_keys` from `tdata` |
||
179 | +124 |
- #' @export+ #' @param data (`tdata`) object |
||
180 | +125 |
- #' @rdname tdata_deprecation+ #' @param ... Additional arguments (not used) |
||
181 | +126 |
- #'+ #' @export |
||
182 | +127 |
- as_tdata <- function(x) {+ join_keys.tdata <- function(data, ...) { |
||
183 | -8x | +128 | +2x |
- if (inherits(x, "tdata")) {+ attr(data, "join_keys") |
184 | -2x | +|||
129 | +
- return(x)+ } |
|||
185 | +130 |
- }+ |
||
186 | -6x | +|||
131 | +
- if (is.reactive(x)) {+ #' Function to get metadata from a `tdata` object |
|||
187 | -1x | +|||
132 | +
- checkmate::assert_class(isolate(x()), "teal_data")+ #' @param data (`tdata` - object) to extract the data from |
|||
188 | -1x | +|||
133 | +
- datanames <- isolate(teal_data_datanames(x()))+ #' @param dataname (`character(1)`) the dataset name whose metadata is requested |
|||
189 | -1x | +|||
134 | +
- datasets <- sapply(datanames, function(dataname) reactive(x()[[dataname]]), simplify = FALSE)+ #' @return Either list of metadata or NULL if no metadata. |
|||
190 | -1x | +|||
135 | +
- code <- reactive(teal.code::get_code(x()))+ #' @export |
|||
191 | -1x | +|||
136 | +
- join_keys <- isolate(teal.data::join_keys(x()))+ get_metadata <- function(data, dataname) { |
|||
192 | -5x | +137 | +4x |
- } else if (inherits(x, "teal_data")) {+ checkmate::assert_string(dataname) |
193 | -5x | +138 | +4x |
- datanames <- teal_data_datanames(x)+ UseMethod("get_metadata", data) |
194 | -5x | +|||
139 | +
- datasets <- sapply(datanames, function(dataname) reactive(x[[dataname]]), simplify = FALSE)+ } |
|||
195 | -5x | +|||
140 | +
- code <- reactive(teal.code::get_code(x))+ |
|||
196 | -5x | +|||
141 | +
- join_keys <- isolate(teal.data::join_keys(x))+ #' @rdname get_metadata |
|||
197 | +142 |
- }+ #' @export |
||
198 | +143 |
-
+ get_metadata.tdata <- function(data, dataname) { |
||
199 | -6x | +144 | +4x |
- new_tdata(data = datasets, code = code, join_keys = join_keys)+ metadata <- attr(data, "metadata") |
200 | -+ | |||
145 | +4x |
- }+ if (is.null(metadata)) { |
1 | -+ | ||
146 | +1x |
- # This is the main function from teal to be used by the end-users. Although it delegates+ return(NULL) |
|
2 | +147 |
- # directly to `module_teal_with_splash.R`, we keep it in a separate file because its documentation is quite large+ } |
|
3 | -+ | ||
148 | +3x |
- # and it is very end-user oriented. It may also perform more argument checking with more informative+ metadata[[dataname]] |
|
4 | +149 |
- # error messages.+ } |
|
5 | +150 | ||
6 | +151 |
- #' Create the server and UI function for the `shiny` app+ #' @rdname get_metadata |
|
7 | +152 |
- #'+ #' @export |
|
8 | +153 |
- #' @description `r lifecycle::badge("stable")`+ get_metadata.default <- function(data, dataname) { |
|
9 | -+ | ||
154 | +! |
- #'+ stop("get_metadata function not implemented for this object") |
|
10 | +155 |
- #' End-users: This is the most important function for you to start a+ } |
|
11 | +156 |
- #' `teal` app that is composed of `teal` modules.+ |
|
12 | +157 |
- #'+ |
|
13 | +158 |
- #' @details+ #' Downgrade `teal_data` objects in modules for compatibility |
|
14 | +159 |
- #' When initializing the `teal` app, if `datanames` are not set for the `teal_data` object,+ #' |
|
15 | +160 |
- #' defaults from the `teal_data` environment will be used.+ #' Convert `teal_data` to `tdata` in `teal` modules. |
|
16 | +161 |
#' |
|
17 | +162 |
- #' @param data (`teal_data` or `teal_data_module`)+ #' Recent changes in `teal` cause modules to fail because modules expect a `tdata` object |
|
18 | +163 |
- #' For constructing the data object, refer to [teal_data()] and [teal_data_module()].+ #' to be passed to the `data` argument but instead they receive a `teal_data` object, |
|
19 | +164 |
- #' @param modules (`list` or `teal_modules` or `teal_module`)+ #' which is additionally wrapped in a reactive expression in the server functions. |
|
20 | +165 |
- #' nested list of `teal_modules` or `teal_module` objects or a single+ #' In order to easily adapt such modules without a proper refactor, |
|
21 | +166 |
- #' `teal_modules` or `teal_module` object. These are the specific output modules which+ #' use this function to downgrade the `data` argument. |
|
22 | +167 |
- #' will be displayed in the `teal` application. See [modules()] and [module()] for+ #' |
|
23 | +168 |
- #' more details.+ #' @param x data object, either `tdata` or `teal_data`, the latter possibly in a reactive expression |
|
24 | +169 |
- #' @param filter (`teal_slices`)+ #' |
|
25 | +170 |
- #' Specifies the initial filter using [teal_slices()].+ #' @return Object of class `tdata`. |
|
26 | +171 |
- #' @param title (`shiny.tag` or `character(1)`)+ #' |
|
27 | +172 |
- #' The browser window title. Defaults to a title "teal app" with the icon of NEST.+ #' @examples |
|
28 | +173 |
- #' Can be created using the `build_app_title()` or+ #' td <- teal_data() |
|
29 | +174 |
- #' by passing a valid `shiny.tag` which is a head tag with title and link tag.+ #' td <- within(td, iris <- iris) %>% within(mtcars <- mtcars) |
|
30 | +175 |
- #' @param header (`shiny.tag` or `character(1)`)+ #' td |
|
31 | +176 |
- #' The header of the app.+ #' as_tdata(td) |
|
32 | +177 |
- #' @param footer (`shiny.tag` or `character(1)`)+ #' as_tdata(reactive(td)) |
|
33 | +178 |
- #' The footer of the app.+ #' |
|
34 | +179 |
- #' @param id (`character`)+ #' @export |
|
35 | +180 |
- #' Optional string specifying the `shiny` module id in cases it is used as a `shiny` module+ #' @rdname tdata_deprecation |
|
36 | +181 |
- #' rather than a standalone `shiny` app. This is a legacy feature.+ #' |
|
37 | +182 |
- #'+ as_tdata <- function(x) { |
|
38 | -+ | ||
183 | +8x |
- #' @return Named list with server and UI functions.+ if (inherits(x, "tdata")) { |
|
39 | -+ | ||
184 | +2x |
- #'+ return(x) |
|
40 | +185 |
- #' @export+ } |
|
41 | -+ | ||
186 | +6x |
- #'+ if (is.reactive(x)) { |
|
42 | -+ | ||
187 | +1x |
- #' @include modules.R+ checkmate::assert_class(isolate(x()), "teal_data") |
|
43 | -+ | ||
188 | +1x |
- #'+ datanames <- isolate(teal_data_datanames(x())) |
|
44 | -+ | ||
189 | +1x |
- #' @examples+ datasets <- sapply(datanames, function(dataname) reactive(x()[[dataname]]), simplify = FALSE) |
|
45 | -+ | ||
190 | +1x |
- #' app <- init(+ code <- reactive(teal.code::get_code(x()))+ |
+ |
191 | +1x | +
+ join_keys <- isolate(teal.data::join_keys(x())) |
|
46 | -+ | ||
192 | +5x |
- #' data = teal_data(+ } else if (inherits(x, "teal_data")) { |
|
47 | -+ | ||
193 | +5x |
- #' new_iris = transform(iris, id = seq_len(nrow(iris))),+ datanames <- teal_data_datanames(x) |
|
48 | -+ | ||
194 | +5x |
- #' new_mtcars = transform(mtcars, id = seq_len(nrow(mtcars))),+ datasets <- sapply(datanames, function(dataname) reactive(x[[dataname]]), simplify = FALSE) |
|
49 | -+ | ||
195 | +5x |
- #' code = "+ code <- reactive(teal.code::get_code(x)) |
|
50 | -+ | ||
196 | +5x |
- #' new_iris <- transform(iris, id = seq_len(nrow(iris)))+ join_keys <- isolate(teal.data::join_keys(x)) |
|
51 | +197 |
- #' new_mtcars <- transform(mtcars, id = seq_len(nrow(mtcars)))+ } |
|
52 | +198 |
- #' "+ |
|
53 | -+ | ||
199 | +6x |
- #' ),+ new_tdata(data = datasets, code = code, join_keys = join_keys) |
|
54 | +200 |
- #' modules = modules(+ } |
55 | +1 |
- #' module(+ #' Manage multiple `FilteredData` objects |
||
56 | +2 |
- #' label = "data source",+ #' |
||
57 | +3 |
- #' server = function(input, output, session, data) {},+ #' Oversee filter states across the entire application. |
||
58 | +4 |
- #' ui = function(id, ...) div(p("information about data source")),+ #' |
||
59 | +5 |
- #' datanames = "all"+ #' This module observes changes in the filters of each `FilteredData` object |
||
60 | +6 |
- #' ),+ #' and keeps track of all filters used. A mapping of filters to modules |
||
61 | +7 |
- #' example_module(label = "example teal module"),+ #' is kept in the `mapping_matrix` object (which is actually a `data.frame`) |
||
62 | +8 |
- #' module(+ #' that tracks which filters (rows) are active in which modules (columns). |
||
63 | +9 |
- #' "Iris Sepal.Length histogram",+ #' |
||
64 | +10 |
- #' server = function(input, output, session, data) {+ #' @name module_filter_manager |
||
65 | +11 |
- #' output$hist <- renderPlot(+ #' |
||
66 | +12 |
- #' hist(data()[["new_iris"]]$Sepal.Length)+ #' @param id (`character(1)`) |
||
67 | +13 |
- #' )+ #' `shiny` module id. |
||
68 | +14 |
- #' },+ #' @param filtered_data_list (named `list`) |
||
69 | +15 |
- #' ui = function(id, ...) {+ #' A list, possibly nested, of `FilteredData` objects. |
||
70 | +16 |
- #' ns <- NS(id)+ #' Each `FilteredData` will be served to one module in the `teal` application. |
||
71 | +17 |
- #' plotOutput(ns("hist"))+ #' The structure of the list must reflect the nesting of modules in tabs |
||
72 | +18 |
- #' },+ #' and the names of the list must match the labels of their respective modules. |
||
73 | +19 |
- #' datanames = "new_iris"+ #' @inheritParams init |
||
74 | +20 |
- #' )+ #' @return A list of `reactive`s, each holding a `teal_slices`, as returned by `filter_manager_module_srv`. |
||
75 | +21 |
- #' ),+ #' @keywords internal |
||
76 | +22 |
- #' filter = teal_slices(+ #' |
||
77 | +23 |
- #' teal_slice(dataname = "new_iris", varname = "Species"),+ NULL |
||
78 | +24 |
- #' teal_slice(dataname = "new_iris", varname = "Sepal.Length"),+ |
||
79 | +25 |
- #' teal_slice(dataname = "new_mtcars", varname = "cyl"),+ #' Filter manager modal |
||
80 | +26 |
- #' exclude_varnames = list(new_iris = c("Sepal.Width", "Petal.Width")),+ #' |
||
81 | +27 |
- #' mapping = list(+ #' Opens a modal containing the filter manager UI. |
||
82 | +28 |
- #' `example teal module` = "new_iris Species",+ #' |
||
83 | +29 |
- #' `Iris Sepal.Length histogram` = "new_iris Species",+ #' @name module_filter_manager_modal |
||
84 | +30 |
- #' global_filters = "new_mtcars cyl"+ #' @inheritParams module_filter_manager |
||
85 | +31 |
- #' )+ #' @keywords internal |
||
86 | +32 |
- #' ),+ #' |
||
87 | +33 |
- #' title = "App title",+ NULL |
||
88 | +34 |
- #' header = tags$h1("Sample App"),+ |
||
89 | +35 |
- #' footer = tags$p("Copyright 2017 - 2023")+ #' @rdname module_filter_manager_modal |
||
90 | +36 |
- #' )+ filter_manager_modal_ui <- function(id) { |
||
91 | -+ | |||
37 | +! |
- #' if (interactive()) {+ ns <- NS(id) |
||
92 | -+ | |||
38 | +! |
- #' shinyApp(app$ui, app$server)+ tags$button( |
||
93 | -+ | |||
39 | +! |
- #' }+ id = ns("show"), |
||
94 | -+ | |||
40 | +! |
- #'+ class = "btn action-button filter_manager_button", |
||
95 | -+ | |||
41 | +! |
- init <- function(data,+ title = "Show filters manager modal", |
||
96 | -+ | |||
42 | +! |
- modules,+ icon("gear") |
||
97 | +43 |
- filter = teal_slices(),+ ) |
||
98 | +44 |
- title = build_app_title(),+ } |
||
99 | +45 |
- header = tags$p(),+ |
||
100 | +46 |
- footer = tags$p(),+ #' @rdname module_filter_manager_modal |
||
101 | +47 |
- id = character(0)) {+ filter_manager_modal_srv <- function(id, filtered_data_list, filter) { |
||
102 | -10x | -
- logger::log_trace("init initializing teal app with: data ('{ class(data) }').")- |
- ||
103 | -+ | 48 | +3x |
-
+ moduleServer(id, function(input, output, session) { |
104 | -+ | |||
49 | +3x |
- # argument checking (independent)+ observeEvent(input$show, { |
||
105 | -+ | |||
50 | +! |
- ## `data`+ logger::log_trace("filter_manager_modal_srv@1 show button has been clicked.") |
||
106 | -10x | +|||
51 | +! |
- if (inherits(data, "TealData")) {+ showModal( |
||
107 | +52 | ! |
- lifecycle::deprecate_stop(+ modalDialog( |
|
108 | +53 | ! |
- when = "0.99.0",+ filter_manager_ui(session$ns("filter_manager")), |
|
109 | +54 | ! |
- what = "init(data)",+ size = "l", |
|
110 | +55 | ! |
- paste(+ footer = NULL, |
|
111 | +56 | ! |
- "TealData is no longer supported. Use teal_data() instead.",+ easyClose = TRUE |
|
112 | -! | +|||
57 | +
- "Please follow migration instructions https://github.com/insightsengineering/teal/discussions/988."+ ) |
|||
113 | +58 |
) |
||
114 | +59 |
- )+ }) |
||
115 | +60 |
- }+ |
||
116 | -10x | +61 | +3x |
- checkmate::assert_multi_class(data, c("teal_data", "teal_data_module"))+ filter_manager_srv("filter_manager", filtered_data_list, filter) |
117 | +62 | ++ |
+ })+ |
+ |
63 | ++ |
+ }+ |
+ ||
64 | ||||
118 | +65 |
- ## `modules`+ #' @rdname module_filter_manager |
||
119 | -10x | +|||
66 | +
- checkmate::assert(+ filter_manager_ui <- function(id) { |
|||
120 | -10x | +|||
67 | +! |
- .var.name = "modules",+ ns <- NS(id) |
||
121 | -10x | +|||
68 | +! |
- checkmate::check_multi_class(modules, c("teal_modules", "teal_module")),+ div( |
||
122 | -10x | +|||
69 | +! |
- checkmate::check_list(modules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules"))+ class = "filter_manager_content",+ |
+ ||
70 | +! | +
+ tableOutput(ns("slices_table")),+ |
+ ||
71 | +! | +
+ snapshot_manager_ui(ns("snapshot_manager")) |
||
123 | +72 |
) |
||
124 | -10x | +|||
73 | +
- if (inherits(modules, "teal_module")) {+ } |
|||
125 | -1x | +|||
74 | +
- modules <- list(modules)+ |
|||
126 | +75 |
- }+ #' @rdname module_filter_manager+ |
+ ||
76 | ++ |
+ filter_manager_srv <- function(id, filtered_data_list, filter) { |
||
127 | -10x | +77 | +5x |
- if (checkmate::test_list(modules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules"))) {+ moduleServer(id, function(input, output, session) { |
128 | -4x | +78 | +5x |
- modules <- do.call(teal::modules, modules)+ logger::log_trace("filter_manager_srv initializing for: { paste(names(filtered_data_list), collapse = ', ')}.") |
129 | +79 |
- }+ + |
+ ||
80 | +5x | +
+ is_module_specific <- isTRUE(attr(filter, "module_specific")) |
||
130 | +81 | |||
131 | +82 |
- ## `filter`+ # Create a global list of slices. |
||
132 | -10x | +|||
83 | +
- checkmate::assert_class(filter, "teal_slices")+ # Contains all available teal_slice objects available to all modules. |
|||
133 | +84 |
-
+ # Passed whole to instances of FilteredData used for individual modules. |
||
134 | +85 |
- ## all other arguments+ # Down there a subset that pertains to the data sets used in that module is applied and displayed. |
||
135 | -9x | +86 | +5x |
- checkmate::assert(+ slices_global <- reactiveVal(filter) |
136 | -9x | +|||
87 | +
- .var.name = "title",+ |
|||
137 | -9x | +88 | +5x |
- checkmate::check_string(title),+ filtered_data_list <- |
138 | -9x | +89 | +5x |
- checkmate::check_multi_class(title, c("shiny.tag", "shiny.tag.list", "html"))+ if (!is_module_specific) { |
139 | +90 |
- )- |
- ||
140 | -9x | -
- checkmate::assert(+ # Retrieve the first FilteredData from potentially nested list. |
||
141 | -9x | +|||
91 | +
- .var.name = "header",+ # List of length one is named "global_filters" because that name is forbidden for a module label. |
|||
142 | -9x | +92 | +4x |
- checkmate::check_string(header),+ list(global_filters = unlist(filtered_data_list)[[1]]) |
143 | -9x | +|||
93 | +
- checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html"))+ } else { |
|||
144 | +94 |
- )+ # Flatten potentially nested list of FilteredData objects while maintaining useful names. |
||
145 | -9x | +|||
95 | +
- checkmate::assert(+ # Simply using `unlist` would result in concatenated names. |
|||
146 | -9x | +96 | +1x |
- .var.name = "footer",+ flatten_nested <- function(x, name = NULL) { |
147 | -9x | +97 | +5x |
- checkmate::check_string(footer),+ if (inherits(x, "FilteredData")) { |
148 | -9x | +98 | +3x |
- checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html"))+ setNames(list(x), name) |
149 | +99 |
- )+ } else { |
||
150 | -9x | +100 | +2x |
- checkmate::assert_character(id, max.len = 1, any.missing = FALSE)+ unlist(lapply(names(x), function(name) flatten_nested(x[[name]], name))) |
151 | +101 |
-
+ } |
||
152 | +102 |
- # log+ } |
||
153 | -9x | +103 | +1x |
- teal.logger::log_system_info()+ flatten_nested(filtered_data_list) |
154 | +104 |
-
+ } |
||
155 | +105 |
- # argument transformations+ |
||
156 | +106 |
- ## `modules` - landing module+ # Create mapping of filters to modules in matrix form (presented as data.frame). |
||
157 | -9x | +|||
107 | +
- landing <- extract_module(modules, "teal_module_landing")+ # Modules get NAs for filters that cannot be set for them. |
|||
158 | -9x | +108 | +5x |
- landing_module <- NULL+ mapping_matrix <- reactive({ |
159 | -9x | +109 | +5x |
- if (length(landing) == 1L) {+ state_ids_global <- vapply(slices_global(), `[[`, character(1L), "id") |
160 | -! | +|||
110 | +5x |
- landing_module <- landing[[1L]]+ mapping_smooth <- lapply(filtered_data_list, function(x) { |
||
161 | -! | +|||
111 | +7x |
- modules <- drop_module(modules, "teal_module_landing")+ state_ids_local <- vapply(x$get_filter_state(), `[[`, character(1L), "id") |
||
162 | -9x | +112 | +7x |
- } else if (length(landing) > 1L) {+ state_ids_allowed <- vapply(x$get_available_teal_slices()(), `[[`, character(1L), "id") |
163 | -! | +|||
113 | +7x |
- stop("Only one `landing_popup_module` can be used.")+ states_active <- state_ids_global %in% state_ids_local |
||
164 | -+ | |||
114 | +7x |
- }+ ifelse(state_ids_global %in% state_ids_allowed, states_active, NA) |
||
165 | +115 |
-
+ }) |
||
166 | +116 |
- ## `filter` - app_id attribute+ |
||
167 | -9x | +117 | +5x |
- attr(filter, "app_id") <- create_app_id(data, modules)+ as.data.frame(mapping_smooth, row.names = state_ids_global, check.names = FALSE) |
168 | +118 |
-
+ }) |
||
169 | +119 |
- ## `filter` - convert teal.slice::teal_slices to teal::teal_slices+ |
||
170 | -9x | -
- filter <- as.teal_slices(as.list(filter))- |
- ||
171 | -+ | 120 | +5x |
-
+ output$slices_table <- renderTable( |
172 | -+ | |||
121 | +5x |
- # argument checking (interdependent)+ expr = { |
||
173 | +122 |
- ## `filter` - `modules`+ # Display logical values as UTF characters. |
||
174 | -9x | -
- if (isTRUE(attr(filter, "module_specific"))) {- |
- ||
175 | -! | -
- module_names <- unlist(c(module_labels(modules), "global_filters"))- |
- ||
176 | -! | -
- failed_mod_names <- setdiff(names(attr(filter, "mapping")), module_names)- |
- ||
177 | -! | -
- if (length(failed_mod_names)) {- |
- ||
178 | -! | -
- stop(- |
- ||
179 | -! | +123 | +2x |
- sprintf(+ mm <- mapping_matrix() |
180 | -! | +|||
124 | +2x |
- "Some module names in the mapping arguments don't match module labels.\n %s not in %s",+ mm[] <- lapply(mm, ifelse, yes = intToUtf8(9989), no = intToUtf8(10060)) |
||
181 | -! | +|||
125 | +2x |
- toString(failed_mod_names),+ mm[] <- lapply(mm, function(x) ifelse(is.na(x), intToUtf8(128306), x)) |
||
182 | -! | +|||
126 | +2x |
- toString(unique(module_names))+ if (!is_module_specific) colnames(mm) <- "Global Filters" |
||
183 | +127 |
- )+ |
||
184 | +128 |
- )+ # Display placeholder if no filters defined. |
||
185 | -+ | |||
129 | +2x |
- }+ if (nrow(mm) == 0L) { |
||
186 | -+ | |||
130 | +2x |
-
+ mm <- data.frame(`Filter manager` = "No filters specified.", check.names = FALSE) |
||
187 | -! | +|||
131 | +2x |
- if (anyDuplicated(module_names)) {+ rownames(mm) <- "" |
||
188 | +132 |
- # In teal we are able to set nested modules with duplicated label.+ } |
||
189 | +133 |
- # Because mapping argument bases on the relationship between module-label and filter-id,+ |
||
190 | +134 |
- # it is possible that module-label in mapping might refer to multiple teal_module (identified by the same label)+ # Report Previewer will not be displayed. |
||
191 | -! | +|||
135 | +2x |
- stop(+ mm[names(mm) != "Report previewer"] |
||
192 | -! | +|||
136 | +
- sprintf(+ }, |
|||
193 | -! | +|||
137 | +5x |
- "Module labels should be unique when teal_slices(mapping = TRUE). Duplicated labels:\n%s ",+ align = paste(c("l", rep("c", sum(names(filtered_data_list) != "Report previewer"))), collapse = ""), |
||
194 | -! | +|||
138 | +5x |
- toString(module_names[duplicated(module_names)])+ rownames = TRUE |
||
195 | +139 |
- )+ ) |
||
196 | +140 |
- )+ |
||
197 | +141 |
- }+ # Create list of module calls. |
||
198 | -+ | |||
142 | +5x |
- }+ modules_out <- lapply(names(filtered_data_list), function(module_name) { |
||
199 | -+ | |||
143 | +7x |
-
+ filter_manager_module_srv( |
||
200 | -+ | |||
144 | +7x |
- ## `data` - `modules`+ id = module_name, |
||
201 | -9x | +145 | +7x |
- if (inherits(data, "teal_data")) {+ module_fd = filtered_data_list[[module_name]], |
202 | -8x | +146 | +7x |
- if (length(teal_data_datanames(data)) == 0) {+ slices_global = slices_global |
203 | -1x | +|||
147 | +
- stop("The environment of `data` is empty.")+ ) |
|||
204 | +148 |
- }+ }) |
||
205 | +149 |
- # in case of teal_data_module this check is postponed to the srv_teal_with_splash+ |
||
206 | -7x | +|||
150 | +
- is_modules_ok <- check_modules_datanames(modules, teal_data_datanames(data))+ # Call snapshot manager. |
|||
207 | -7x | +151 | +5x |
- if (!isTRUE(is_modules_ok)) {+ snapshot_manager_srv("snapshot_manager", slices_global, mapping_matrix, filtered_data_list) |
208 | -1x | +|||
152 | +
- logger::log_error(is_modules_ok)+ |
|||
209 | -1x | +153 | +5x |
- checkmate::assert(is_modules_ok, .var.name = "modules")+ modules_out # returned for testing purpose |
210 | +154 |
- }+ }) |
||
211 | +155 | ++ |
+ }+ |
+ |
156 | ||||
212 | -6x | +|||
157 | +
- is_filter_ok <- check_filter_datanames(filter, teal_data_datanames(data))+ #' Module specific filter manager |
|||
213 | -6x | +|||
158 | +
- if (!isTRUE(is_filter_ok)) {+ #' |
|||
214 | -1x | +|||
159 | +
- warning(is_filter_ok)+ #' Tracks filter states in a single module. |
|||
215 | +160 |
- # we allow app to continue if applied filters are outside+ #' |
||
216 | +161 |
- # of possible data range+ #' This module tracks the state of a single `FilteredData` object and global `teal_slices` |
||
217 | +162 |
- }+ #' and updates both objects as necessary. Filter states added in different modules+ |
+ ||
163 | ++ |
+ #' Filter states added any individual module are added to global `teal_slices`+ |
+ ||
164 | ++ |
+ #' and from there become available in other modules |
||
218 | +165 |
- }+ #' by setting `private$available_teal_slices` in each `FilteredData`. |
||
219 | +166 |
-
+ #' |
||
220 | +167 |
- # Note regarding case `id = character(0)`:+ #' @param id (`character(1)`) |
||
221 | +168 |
- # rather than creating a submodule of this module, we directly modify+ #' `shiny` module id. |
||
222 | +169 |
- # the UI and server with `id = character(0)` and calling the server function directly+ #' @param module_fd (`FilteredData`) |
||
223 | -7x | +|||
170 | +
- res <- list(+ #' Object containing the data to be filtered in a single `teal` module. |
|||
224 | -7x | +|||
171 | +
- ui = ui_teal_with_splash(id = id, data = data, title = title, header = header, footer = footer),+ #' @param slices_global (`reactiveVal`) |
|||
225 | -7x | +|||
172 | +
- server = function(input, output, session) {+ #' stores `teal_slices` with all available filters; allows the following actions: |
|||
226 | -! | +|||
173 | +
- if (!is.null(landing_module)) {+ #' - to disable/enable a specific filter in a module |
|||
227 | -! | +|||
174 | +
- do.call(landing_module$server, c(list(id = "landing_module_shiny_id"), landing_module$server_args))+ #' - to restore saved filter settings |
|||
228 | +175 |
- }+ #' - to save current filter panel settings |
||
229 | -! | +|||
176 | +
- srv_teal_with_splash(id = id, data = data, modules = modules, filter = deep_copy_filter(filter))+ #' @return A `reactive` expression containing the slices active in this module. |
|||
230 | +177 |
- }+ #' @keywords internal |
||
231 | +178 |
- )+ #' |
||
232 | +179 |
-
+ filter_manager_module_srv <- function(id, module_fd, slices_global) { |
||
233 | +180 | 7x |
- logger::log_trace("init teal app has been initialized.")+ moduleServer(id, function(input, output, session) { |
|
234 | +181 |
-
+ # Only operate on slices that refer to data sets present in this module. |
||
235 | +182 | 7x |
- res+ module_fd$set_available_teal_slices(reactive(slices_global())) |
|
236 | +183 |
- }+ |
1 | +184 |
- #' @title `TealReportCard`+ # Track filter state of this module. |
||
2 | -+ | |||
185 | +7x |
- #' @description `r lifecycle::badge("experimental")`+ slices_module <- reactive(module_fd$get_filter_state()) |
||
3 | +186 |
- #' Child class of [`ReportCard`] that is used for `teal` specific applications.+ |
||
4 | +187 |
- #' In addition to the parent methods, it supports rendering `teal` specific elements such as+ # Reactive values for comparing states. |
||
5 | -+ | |||
188 | +7x |
- #' the source code, the encodings panel content and the filter panel content as part of the+ previous_slices <- reactiveVal(isolate(slices_module())) |
||
6 | -+ | |||
189 | +7x |
- #' meta data.+ slices_added <- reactiveVal(NULL) |
||
7 | +190 |
- #' @export+ |
||
8 | +191 |
- #'+ # Observe changes in module filter state and trigger appropriate actions. |
||
9 | -+ | |||
192 | +7x |
- TealReportCard <- R6::R6Class( # nolint: object_name_linter.+ observeEvent(slices_module(), ignoreNULL = FALSE, { |
||
10 | -+ | |||
193 | +2x |
- classname = "TealReportCard",+ logger::log_trace("filter_manager_srv@1 detecting states deltas in module: { id }.") |
||
11 | -+ | |||
194 | +2x |
- inherit = teal.reporter::ReportCard,+ added <- setdiff_teal_slices(slices_module(), slices_global()) |
||
12 | -+ | |||
195 | +! |
- public = list(+ if (length(added)) slices_added(added) |
||
13 | -+ | |||
196 | +2x |
- #' @description Appends the source code to the `content` meta data of this `TealReportCard`.+ previous_slices(slices_module()) |
||
14 | +197 |
- #'+ }) |
||
15 | +198 |
- #' @param src (`character(1)`) code as text.+ |
||
16 | -+ | |||
199 | +7x |
- #' @param ... any `rmarkdown` `R` chunk parameter and its value.+ observeEvent(slices_added(), ignoreNULL = TRUE, { |
||
17 | -+ | |||
200 | +! |
- #' But `eval` parameter is always set to `FALSE`.+ logger::log_trace("filter_manager_srv@2 added filter in module: { id }.") |
||
18 | +201 |
- #' @return Object of class `TealReportCard`, invisibly.+ # In case the new state has the same id as an existing state, add a suffix to it. |
||
19 | -+ | |||
202 | +! |
- #' @examples+ global_ids <- vapply(slices_global(), `[[`, character(1L), "id") |
||
20 | -+ | |||
203 | +! |
- #' card <- TealReportCard$new()$append_src(+ lapply(+ |
+ ||
204 | +! | +
+ slices_added(),+ |
+ ||
205 | +! | +
+ function(slice) {+ |
+ ||
206 | +! | +
+ if (slice$id %in% global_ids) { |
||
21 | -+ | |||
207 | +! |
- #' "plot(iris)"+ slice$id <- utils::tail(make.unique(c(global_ids, slice$id), sep = "_"), 1) |
||
22 | +208 |
- #' )+ } |
||
23 | +209 |
- #' card$get_content()[[1]]$get_content()+ } |
||
24 | +210 |
- append_src = function(src, ...) {- |
- ||
25 | -4x | -
- checkmate::assert_character(src, min.len = 0, max.len = 1)+ ) |
||
26 | -4x | +|||
211 | +! |
- params <- list(...)+ slices_global_new <- c(slices_global(), slices_added()) |
||
27 | -4x | +|||
212 | +! |
- params$eval <- FALSE+ slices_global(slices_global_new) |
||
28 | -4x | +|||
213 | +! |
- rblock <- RcodeBlock$new(src)+ slices_added(NULL) |
||
29 | -4x | +|||
214 | +
- rblock$set_params(params)+ }) |
|||
30 | -4x | +|||
215 | +
- self$append_content(rblock)+ |
|||
31 | -4x | +216 | +7x |
- self$append_metadata("SRC", src)+ slices_module # returned for testing purpose |
32 | -4x | +|||
217 | +
- invisible(self)+ }) |
|||
33 | +218 |
- },+ } |
34 | +1 |
- #' @description Appends the filter state list to the `content` and `metadata` of this `TealReportCard`.+ #' Data module for `teal` applications |
||
35 | +2 |
- #' If the filter state list has an attribute named `formatted`, it appends it to the card otherwise it uses+ #' |
||
36 | +3 |
- #' the default `yaml::as.yaml` to format the list.+ #' @description |
||
37 | +4 |
- #' If the filter state list is empty, nothing is appended to the `content`.+ #' `r lifecycle::badge("experimental")` |
||
38 | +5 |
- #'+ #' |
||
39 | +6 |
- #' @param fs (`teal_slices`) object returned from [teal_slices()] function.+ #' Create a `teal_data_module` object and evaluate code on it with history tracking. |
||
40 | +7 |
- #' @return `self`, invisibly.+ #' |
||
41 | +8 |
- append_fs = function(fs) {+ #' @details |
||
42 | -5x | +|||
9 | +
- checkmate::assert_class(fs, "teal_slices")+ #' `teal_data_module` creates a `shiny` module to supply or modify data in a `teal` application. |
|||
43 | -4x | +|||
10 | +
- self$append_text("Filter State", "header3")+ #' The module allows for running data pre-processing code (creation _and_ some modification) after the app starts. |
|||
44 | -4x | +|||
11 | +
- self$append_content(TealSlicesBlock$new(fs))+ #' The body of the server function will be run in the app rather than in the global environment. |
|||
45 | -4x | +|||
12 | +
- invisible(self)+ #' This means it will be run every time the app starts, so use sparingly. |
|||
46 | +13 |
- },+ #' |
||
47 | +14 |
- #' @description Appends the encodings list to the `content` and `metadata` of this `TealReportCard`.+ #' Pass this module instead of a `teal_data` object in a call to [init()]. |
||
48 | +15 |
- #'+ #' Note that the server function must always return a `teal_data` object wrapped in a reactive expression. |
||
49 | +16 |
- #' @param encodings (`list`) list of encodings selections of the `teal` app.+ #' |
||
50 | +17 |
- #' @return `self`, invisibly.+ #' See vignette `vignette("data-as-shiny-module", package = "teal")` for more details. |
||
51 | +18 |
- #' @examples+ #' |
||
52 | +19 |
- #' card <- TealReportCard$new()$append_encodings(list(variable1 = "X"))+ #' @param ui (`function(id)`) |
||
53 | +20 |
- #' card$get_content()[[1]]$get_content()+ #' `shiny` module UI function; must only take `id` argument |
||
54 | +21 |
- #'+ #' @param server (`function(id)`) |
||
55 | +22 |
- append_encodings = function(encodings) {+ #' `shiny` module server function; must only take `id` argument; |
||
56 | -4x | +|||
23 | +
- checkmate::assert_list(encodings)+ #' must return reactive expression containing `teal_data` object |
|||
57 | -4x | +|||
24 | +
- self$append_text("Selected Options", "header3")+ #' |
|||
58 | -4x | +|||
25 | +
- if (requireNamespace("yaml", quietly = TRUE)) {+ #' @return |
|||
59 | -4x | +|||
26 | +
- self$append_text(yaml::as.yaml(encodings, handlers = list(+ #' `teal_data_module` returns an object of class `teal_data_module`. |
|||
60 | -4x | +|||
27 | +
- POSIXct = function(x) format(x, "%Y-%m-%d"),+ #' |
|||
61 | -4x | +|||
28 | +
- POSIXlt = function(x) format(x, "%Y-%m-%d"),+ #' @examples |
|||
62 | -4x | +|||
29 | +
- Date = function(x) format(x, "%Y-%m-%d")+ #' tdm <- teal_data_module( |
|||
63 | -4x | +|||
30 | +
- )), "verbatim")+ #' ui = function(id) { |
|||
64 | +31 |
- } else {+ #' ns <- NS(id) |
||
65 | -! | +|||
32 | +
- stop("yaml package is required to format the encodings list")+ #' actionButton(ns("submit"), label = "Load data") |
|||
66 | +33 |
- }+ #' }, |
||
67 | -4x | +|||
34 | +
- self$append_metadata("Encodings", encodings)+ #' server = function(id) { |
|||
68 | -4x | +|||
35 | +
- invisible(self)+ #' moduleServer(id, function(input, output, session) { |
|||
69 | +36 |
- }+ #' eventReactive(input$submit, { |
||
70 | +37 |
- ),+ #' data <- within( |
||
71 | +38 |
- private = list()+ #' teal_data(), |
||
72 | +39 |
- )+ #' { |
||
73 | +40 |
-
+ #' dataset1 <- iris |
||
74 | +41 |
- #' @title `RcodeBlock`+ #' dataset2 <- mtcars |
||
75 | +42 |
- #' @keywords internal+ #' } |
||
76 | +43 |
- TealSlicesBlock <- R6::R6Class( # nolint: object_name_linter.+ #' ) |
||
77 | +44 |
- classname = "TealSlicesBlock",+ #' datanames(data) <- c("dataset1", "dataset2") |
||
78 | +45 |
- inherit = teal.reporter:::TextBlock,+ #' |
||
79 | +46 |
- public = list(+ #' data |
||
80 | +47 |
- #' @description Returns a `TealSlicesBlock` object.+ #' }) |
||
81 | +48 |
- #'+ #' }) |
||
82 | +49 |
- #' @details Returns a `TealSlicesBlock` object with no content and no parameters.+ #' } |
||
83 | +50 |
- #'+ #' ) |
||
84 | +51 |
- #' @param content (`teal_slices`) object returned from [teal_slices()] function.+ #' |
||
85 | +52 |
- #' @param style (`character(1)`) string specifying style to apply.+ #' @name teal_data_module |
||
86 | +53 |
- #'+ #' @seealso [`teal.data::teal_data-class`], [teal.code::qenv()] |
||
87 | +54 |
- #' @return Object of class `TealSlicesBlock`, invisibly.+ #' |
||
88 | +55 |
- #'+ #' @export |
||
89 | +56 |
- initialize = function(content = teal_slices(), style = "verbatim") {+ teal_data_module <- function(ui, server) { |
||
90 | -10x | +57 | +36x |
- self$set_content(content)+ checkmate::assert_function(ui, args = "id", nargs = 1) |
91 | -9x | +58 | +35x |
- self$set_style(style)+ checkmate::assert_function(server, args = "id", nargs = 1) |
92 | -9x | +59 | +34x |
- invisible(self)+ structure(+ |
+
60 | +34x | +
+ list(ui = ui, server = server),+ |
+ ||
61 | +34x | +
+ class = "teal_data_module" |
||
93 | +62 |
- },+ ) |
||
94 | +63 |
-
+ } |
95 | +1 |
- #' @description Sets content of this `TealSlicesBlock`.+ # This is the main function from teal to be used by the end-users. Although it delegates |
|
96 | +2 |
- #' Sets content as `YAML` text which represents a list generated from `teal_slices`.+ # directly to `module_teal_with_splash.R`, we keep it in a separate file because its documentation is quite large |
|
97 | +3 |
- #' The list displays limited number of fields from `teal_slice` objects, but this list is+ # and it is very end-user oriented. It may also perform more argument checking with more informative |
|
98 | +4 |
- #' sufficient to conclude which filters were applied.+ # error messages. |
|
99 | +5 |
- #' When `selected` field in `teal_slice` object is a range, then it is displayed as a "min"+ |
|
100 | +6 |
- #'+ #' Create the server and UI function for the `shiny` app |
|
101 | +7 |
- #'+ #' |
|
102 | +8 |
- #' @param content (`teal_slices`) object returned from [teal_slices()] function.+ #' @description `r lifecycle::badge("stable")` |
|
103 | +9 |
- #' @return `self`, invisibly.+ #' |
|
104 | +10 |
- set_content = function(content) {+ #' End-users: This is the most important function for you to start a |
|
105 | -11x | +||
11 | +
- checkmate::assert_class(content, "teal_slices")+ #' `teal` app that is composed of `teal` modules. |
||
106 | -10x | +||
12 | +
- if (length(content) != 0) {+ #' |
||
107 | -7x | +||
13 | +
- states_list <- lapply(content, function(x) {+ #' @details |
||
108 | -7x | +||
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.+ |
+ |
16 | ++ |
+ #'+ |
+ |
17 | +
- x_list <- shiny::isolate(as.list(x))+ #' @param data (`teal_data` or `teal_data_module`) |
||
109 | -7x | +||
18 | +
- if (+ #' For constructing the data object, refer to [teal_data()] and [teal_data_module()]. |
||
110 | -7x | +||
19 | +
- inherits(x_list$choices, c("integer", "numeric", "Date", "POSIXct", "POSIXlt")) &&+ #' @param modules (`list` or `teal_modules` or `teal_module`) |
||
111 | -7x | +||
20 | +
- length(x_list$choices) == 2 &&+ #' nested list of `teal_modules` or `teal_module` objects or a single |
||
112 | -7x | +||
21 | +
- length(x_list$selected) == 2+ #' `teal_modules` or `teal_module` object. These are the specific output modules which |
||
113 | +22 |
- ) {+ #' will be displayed in the `teal` application. See [modules()] and [module()] for |
|
114 | -! | +||
23 | +
- x_list$range <- paste(x_list$selected, collapse = " - ")+ #' more details. |
||
115 | -! | +||
24 | +
- x_list["selected"] <- NULL+ #' @param filter (`teal_slices`) |
||
116 | +25 |
- }+ #' Specifies the initial filter using [teal_slices()]. |
|
117 | -7x | +||
26 | +
- if (!is.null(x_list$arg)) {+ #' @param title (`shiny.tag` or `character(1)`) |
||
118 | -! | +||
27 | +
- x_list$arg <- if (x_list$arg == "subset") "Genes" else "Samples"+ #' The browser window title. Defaults to a title "teal app" with the icon of NEST. |
||
119 | +28 |
- }+ #' Can be created using the `build_app_title()` or |
|
120 | +29 |
-
+ #' by passing a valid `shiny.tag` which is a head tag with title and link tag. |
|
121 | -7x | +||
30 | +
- x_list <- x_list[+ #' @param header (`shiny.tag` or `character(1)`) |
||
122 | -7x | +||
31 | +
- c("dataname", "varname", "experiment", "arg", "expr", "selected", "range", "keep_na", "keep_inf")+ #' The header of the app. |
||
123 | +32 |
- ]+ #' @param footer (`shiny.tag` or `character(1)`) |
|
124 | -7x | +||
33 | +
- names(x_list) <- c(+ #' The footer of the app. |
||
125 | -7x | +||
34 | +
- "Dataset name", "Variable name", "Experiment", "Filtering by", "Applied expression",+ #' @param id (`character`) |
||
126 | -7x | +||
35 | +
- "Selected Values", "Selected range", "Include NA values", "Include Inf values"+ #' Optional string specifying the `shiny` module id in cases it is used as a `shiny` module |
||
127 | +36 |
- )+ #' rather than a standalone `shiny` app. This is a legacy feature. |
|
128 | +37 |
-
+ #' |
|
129 | -7x | +||
38 | +
- Filter(Negate(is.null), x_list)+ #' @return Named list with server and UI functions. |
||
130 | +39 |
- })+ #' |
|
131 | +40 |
-
+ #' @export |
|
132 | -7x | +||
41 | +
- if (requireNamespace("yaml", quietly = TRUE)) {+ #' |
||
133 | -7x | +||
42 | +
- super$set_content(yaml::as.yaml(states_list))+ #' @include modules.R |
||
134 | +43 |
- } else {+ #' |
|
135 | -! | +||
44 | +
- stop("yaml package is required to format the filter state list")+ #' @examples |
||
136 | +45 |
- }+ #' app <- init( |
|
137 | +46 |
- }+ #' data = teal_data( |
|
138 | -10x | +||
47 | +
- private$teal_slices <- content+ #' new_iris = transform(iris, id = seq_len(nrow(iris))), |
||
139 | -10x | +||
48 | +
- invisible(self)+ #' new_mtcars = transform(mtcars, id = seq_len(nrow(mtcars))), |
||
140 | +49 |
- },+ #' code = " |
|
141 | +50 |
- #' @description Create the `RcodeBlock` from a list.+ #' new_iris <- transform(iris, id = seq_len(nrow(iris))) |
|
142 | +51 |
- #' @param x (named `list`) with two fields `c("text", "params")`.+ #' new_mtcars <- transform(mtcars, id = seq_len(nrow(mtcars))) |
|
143 | +52 |
- #' Use the `get_available_params` method to get all possible parameters.+ #' " |
|
144 | +53 |
- #' @return `self`, invisibly.+ #' ), |
|
145 | +54 |
- from_list = function(x) {+ #' modules = modules( |
|
146 | -1x | +||
55 | +
- checkmate::assert_list(x)+ #' module( |
||
147 | -1x | +||
56 | +
- checkmate::assert_names(names(x), must.include = c("teal_slices"))+ #' label = "data source", |
||
148 | -1x | +||
57 | +
- self$set_content(x$teal_slices)+ #' server = function(input, output, session, data) {}, |
||
149 | -1x | +||
58 | +
- invisible(self)+ #' ui = function(id, ...) div(p("information about data source")), |
||
150 | +59 |
- },+ #' datanames = "all" |
|
151 | +60 |
- #' @description Convert the `RcodeBlock` to a list.+ #' ), |
|
152 | +61 |
- #' @return named `list` with a text and `params`.+ #' example_module(label = "example teal module"), |
|
153 | +62 |
-
+ #' module( |
|
154 | +63 |
- to_list = function() {+ #' "Iris Sepal.Length histogram", |
|
155 | -2x | +||
64 | +
- list(teal_slices = private$teal_slices)+ #' server = function(input, output, session, data) { |
||
156 | +65 |
- }+ #' output$hist <- renderPlot( |
|
157 | +66 |
- ),+ #' hist(data()[["new_iris"]]$Sepal.Length) |
|
158 | +67 |
- private = list(+ #' ) |
|
159 | +68 |
- style = "verbatim",+ #' }, |
|
160 | +69 |
- teal_slices = NULL # teal_slices+ #' ui = function(id, ...) { |
|
161 | +70 |
- )+ #' ns <- NS(id) |
|
162 | +71 |
- )+ #' plotOutput(ns("hist")) |
1 | +72 |
- #' Landing popup module+ #' }, |
|
2 | +73 |
- #'+ #' datanames = "new_iris" |
|
3 | +74 |
- #' @description Creates a landing welcome popup for `teal` applications.+ #' ) |
|
4 | +75 |
- #'+ #' ), |
|
5 | +76 |
- #' This module is used to display a popup dialog when the application starts.+ #' filter = teal_slices( |
|
6 | +77 |
- #' The dialog blocks access to the application and must be closed with a button before the application can be viewed.+ #' teal_slice(dataname = "new_iris", varname = "Species"), |
|
7 | +78 |
- #'+ #' teal_slice(dataname = "new_iris", varname = "Sepal.Length"), |
|
8 | +79 |
- #' @param label (`character(1)`) Label of the module.+ #' teal_slice(dataname = "new_mtcars", varname = "cyl"), |
|
9 | +80 |
- #' @param title (`character(1)`) Text to be displayed as popup title.+ #' exclude_varnames = list(new_iris = c("Sepal.Width", "Petal.Width")), |
|
10 | +81 |
- #' @param content (`character(1)`, `shiny.tag` or `shiny.tag.list`) with the content of the popup.+ #' mapping = list( |
|
11 | +82 |
- #' Passed to `...` of `shiny::modalDialog`. See examples.+ #' `example teal module` = "new_iris Species", |
|
12 | +83 |
- #' @param buttons (`shiny.tag` or `shiny.tag.list`) Typically a `modalButton` or `actionButton`. See examples.+ #' `Iris Sepal.Length histogram` = "new_iris Species", |
|
13 | +84 |
- #'+ #' global_filters = "new_mtcars cyl" |
|
14 | +85 |
- #' @return A `teal_module` (extended with `teal_landing_module` class) to be used in `teal` applications.+ #' ) |
|
15 | +86 |
- #'+ #' ), |
|
16 | +87 |
- #' @examples+ #' title = "App title", |
|
17 | +88 |
- #' app1 <- init(+ #' header = tags$h1("Sample App"), |
|
18 | +89 |
- #' data = teal_data(iris = iris),+ #' footer = tags$p("Copyright 2017 - 2023") |
|
19 | +90 |
- #' modules = modules(+ #' ) |
|
20 | +91 |
- #' landing_popup_module(+ #' if (interactive()) { |
|
21 | +92 |
- #' content = "A place for the welcome message or a disclaimer statement.",+ #' shinyApp(app$ui, app$server) |
|
22 | +93 |
- #' buttons = modalButton("Proceed")+ #' } |
|
23 | +94 |
- #' ),+ #' |
|
24 | +95 |
- #' example_module()+ init <- function(data, |
|
25 | +96 |
- #' )+ modules, |
|
26 | +97 |
- #' )+ filter = teal_slices(), |
|
27 | +98 |
- #' if (interactive()) {+ title = build_app_title(), |
|
28 | +99 |
- #' shinyApp(app1$ui, app1$server)+ header = tags$p(), |
|
29 | +100 |
- #' }+ footer = tags$p(), |
|
30 | +101 |
- #'+ id = character(0)) { |
|
31 | -+ | ||
102 | +10x |
- #' app2 <- init(+ logger::log_trace("init initializing teal app with: data ('{ class(data) }').") |
|
32 | +103 |
- #' data = teal_data(iris = iris),+ |
|
33 | +104 |
- #' modules = modules(+ # argument checking (independent) |
|
34 | +105 |
- #' landing_popup_module(+ ## `data` |
|
35 | -+ | ||
106 | +10x |
- #' title = "Welcome",+ if (inherits(data, "TealData")) { |
|
36 | -+ | ||
107 | +! |
- #' content = tags$b(+ lifecycle::deprecate_stop( |
|
37 | -+ | ||
108 | +! |
- #' "A place for the welcome message or a disclaimer statement.",+ when = "0.99.0", |
|
38 | -+ | ||
109 | +! |
- #' style = "color: red;"+ what = "init(data)",+ |
+ |
110 | +! | +
+ paste(+ |
+ |
111 | +! | +
+ "TealData is no longer supported. Use teal_data() instead.",+ |
+ |
112 | +! | +
+ "Please follow migration instructions https://github.com/insightsengineering/teal/discussions/988." |
|
39 | +113 |
- #' ),+ ) |
|
40 | +114 |
- #' buttons = tagList(+ ) |
|
41 | +115 |
- #' modalButton("Proceed"),+ } |
|
42 | -+ | ||
116 | +10x |
- #' actionButton("read", "Read more",+ checkmate::assert_multi_class(data, c("teal_data", "teal_data_module")) |
|
43 | +117 |
- #' onclick = "window.open('http://google.com', '_blank')"+ |
|
44 | +118 |
- #' ),+ ## `modules` |
|
45 | -+ | ||
119 | +10x |
- #' actionButton("close", "Reject", onclick = "window.close()")+ checkmate::assert( |
|
46 | -+ | ||
120 | +10x |
- #' )+ .var.name = "modules", |
|
47 | -+ | ||
121 | +10x |
- #' ),+ checkmate::check_multi_class(modules, c("teal_modules", "teal_module")), |
|
48 | -+ | ||
122 | +10x |
- #' example_module()+ checkmate::check_list(modules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules")) |
|
49 | +123 |
- #' )+ ) |
|
50 | -+ | ||
124 | +10x |
- #' )+ if (inherits(modules, "teal_module")) { |
|
51 | -+ | ||
125 | +1x |
- #'+ modules <- list(modules) |
|
52 | +126 |
- #' if (interactive()) {+ } |
|
53 | -+ | ||
127 | +10x |
- #' shinyApp(app2$ui, app2$server)+ if (checkmate::test_list(modules, min.len = 1, any.missing = FALSE, types = c("teal_module", "teal_modules"))) { |
|
54 | -+ | ||
128 | +4x |
- #' }+ modules <- do.call(teal::modules, modules) |
|
55 | +129 |
- #'+ } |
|
56 | +130 |
- #' @export+ |
|
57 | +131 |
- landing_popup_module <- function(label = "Landing Popup",+ ## `filter` |
|
58 | -+ | ||
132 | +10x |
- title = NULL,+ checkmate::assert_class(filter, "teal_slices") |
|
59 | +133 |
- content = NULL,+ |
|
60 | +134 |
- buttons = modalButton("Accept")) {- |
- |
61 | -! | -
- checkmate::assert_string(label)+ ## all other arguments |
|
62 | -! | +||
135 | +9x |
- checkmate::assert_string(title, null.ok = TRUE)+ checkmate::assert( |
|
63 | -! | +||
136 | +9x |
- checkmate::assert_multi_class(+ .var.name = "title", |
|
64 | -! | +||
137 | +9x |
- content,+ checkmate::check_string(title), |
|
65 | -! | +||
138 | +9x |
- classes = c("character", "shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE+ checkmate::check_multi_class(title, c("shiny.tag", "shiny.tag.list", "html")) |
|
66 | +139 |
) |
|
67 | -! | +||
140 | +9x |
- checkmate::assert_multi_class(buttons, classes = c("shiny.tag", "shiny.tag.list"))+ checkmate::assert( |
|
68 | -+ | ||
141 | +9x |
-
+ .var.name = "header", |
|
69 | -! | +||
142 | +9x |
- logger::log_info("Initializing landing_popup_module")+ checkmate::check_string(header), |
|
70 | -+ | ||
143 | +9x |
-
+ checkmate::check_multi_class(header, c("shiny.tag", "shiny.tag.list", "html")) |
|
71 | -! | +||
144 | +
- module <- module(+ ) |
||
72 | -! | +||
145 | +9x |
- label = label,+ checkmate::assert( |
|
73 | -! | +||
146 | +9x |
- server = function(id) {+ .var.name = "footer", |
|
74 | -! | +||
147 | +9x |
- moduleServer(id, function(input, output, session) {+ checkmate::check_string(footer), |
|
75 | -! | +||
148 | +9x |
- showModal(+ checkmate::check_multi_class(footer, c("shiny.tag", "shiny.tag.list", "html")) |
|
76 | -! | +||
149 | +
- modalDialog(+ ) |
||
77 | -! | +||
150 | +9x |
- id = "landingpopup",+ checkmate::assert_character(id, max.len = 1, any.missing = FALSE) |
|
78 | -! | +||
151 | +
- title = title,+ |
||
79 | -! | +||
152 | +
- content,+ # log |
||
80 | -! | +||
153 | +9x |
- footer = buttons+ teal.logger::log_system_info() |
|
81 | +154 |
- )+ |
|
82 | +155 |
- )+ # argument transformations |
|
83 | +156 |
- })+ ## `modules` - landing module |
|
84 | -+ | ||
157 | +9x |
- }+ landing <- extract_module(modules, "teal_module_landing") |
|
85 | -+ | ||
158 | +9x |
- )+ landing_module <- NULL+ |
+ |
159 | +9x | +
+ if (length(landing) == 1L) { |
|
86 | +160 | ! |
- class(module) <- c("teal_module_landing", class(module))+ landing_module <- landing[[1L]] |
87 | +161 | ! |
- module+ modules <- drop_module(modules, "teal_module_landing") |
88 | -+ | ||
162 | +9x |
- }+ } else if (length(landing) > 1L) { |
1 | -+ | ||
163 | +! |
- #' Send input validation messages to output.+ stop("Only one `landing_popup_module` can be used.") |
|
2 | +164 |
- #'+ } |
|
3 | +165 |
- #' Captures messages from `InputValidator` objects and collates them+ |
|
4 | +166 |
- #' into one message passed to `validate`.+ ## `filter` - app_id attribute |
|
5 | -+ | ||
167 | +9x |
- #'+ attr(filter, "app_id") <- create_app_id(data, modules) |
|
6 | +168 |
- #' `shiny::validate` is used to withhold rendering of an output element until+ |
|
7 | +169 |
- #' certain conditions are met and to print a validation message in place+ ## `filter` - convert teal.slice::teal_slices to teal::teal_slices |
|
8 | -+ | ||
170 | +9x |
- #' of the output element.+ filter <- as.teal_slices(as.list(filter)) |
|
9 | +171 |
- #' `shinyvalidate::InputValidator` allows to validate input elements+ |
|
10 | +172 |
- #' and to display specific messages in their respective input widgets.+ # argument checking (interdependent) |
|
11 | +173 |
- #' `validate_inputs` provides a hybrid solution.+ ## `filter` - `modules` |
|
12 | -+ | ||
174 | +9x |
- #' Given an `InputValidator` object, messages corresponding to inputs that fail validation+ if (isTRUE(attr(filter, "module_specific"))) { |
|
13 | -+ | ||
175 | +! |
- #' are extracted and placed in one validation message that is passed to a `validate`/`need` call.+ module_names <- unlist(c(module_labels(modules), "global_filters")) |
|
14 | -+ | ||
176 | +! |
- #' This way the input `validator` messages are repeated in the output.+ failed_mod_names <- setdiff(names(attr(filter, "mapping")), module_names) |
|
15 | -+ | ||
177 | +! |
- #'+ if (length(failed_mod_names)) { |
|
16 | -+ | ||
178 | +! |
- #' The `...` argument accepts any number of `InputValidator` objects+ stop( |
|
17 | -+ | ||
179 | +! |
- #' or a nested list of such objects.+ sprintf( |
|
18 | -+ | ||
180 | +! |
- #' If `validators` are passed directly, all their messages are printed together+ "Some module names in the mapping arguments don't match module labels.\n %s not in %s",+ |
+ |
181 | +! | +
+ toString(failed_mod_names),+ |
+ |
182 | +! | +
+ toString(unique(module_names)) |
|
19 | +183 |
- #' under one (optional) header message specified by `header`. If a list is passed,+ ) |
|
20 | +184 |
- #' messages are grouped by `validator`. The list's names are used as headers+ ) |
|
21 | +185 |
- #' for their respective message groups.+ } |
|
22 | +186 |
- #' If neither of the nested list elements is named, a header message is taken from `header`.+ + |
+ |
187 | +! | +
+ if (anyDuplicated(module_names)) { |
|
23 | +188 |
- #'+ # In teal we are able to set nested modules with duplicated label. |
|
24 | +189 |
- #' @param ... either any number of `InputValidator` objects+ # Because mapping argument bases on the relationship between module-label and filter-id, |
|
25 | +190 |
- #' or an optionally named, possibly nested `list` of `InputValidator`+ # it is possible that module-label in mapping might refer to multiple teal_module (identified by the same label)+ |
+ |
191 | +! | +
+ stop(+ |
+ |
192 | +! | +
+ sprintf( |
|
26 | -+ | ||
193 | +! |
- #' objects, see `Details`+ "Module labels should be unique when teal_slices(mapping = TRUE). Duplicated labels:\n%s ", |
|
27 | -+ | ||
194 | +! |
- #' @param header (`character(1)`) generic validation message; set to NULL to omit+ toString(module_names[duplicated(module_names)]) |
|
28 | +195 |
- #'+ ) |
|
29 | +196 |
- #' @return+ ) |
|
30 | +197 |
- #' Returns NULL if the final validation call passes and a `shiny.silent.error` if it fails.+ } |
|
31 | +198 |
- #'+ } |
|
32 | +199 |
- #' @seealso [`shinyvalidate::InputValidator`], [`shiny::validate`]+ |
|
33 | +200 |
- #'+ ## `data` - `modules` |
|
34 | -+ | ||
201 | +9x |
- #' @examples+ if (inherits(data, "teal_data")) { |
|
35 | -+ | ||
202 | +8x |
- #' library(shiny)+ if (length(teal_data_datanames(data)) == 0) { |
|
36 | -+ | ||
203 | +1x |
- #' library(shinyvalidate)+ stop("The environment of `data` is empty.") |
|
37 | +204 |
- #'+ } |
|
38 | +205 |
- #' ui <- fluidPage(+ # in case of teal_data_module this check is postponed to the srv_teal_with_splash |
|
39 | -+ | ||
206 | +7x |
- #' selectInput("method", "validation method", c("sequential", "combined", "grouped")),+ is_modules_ok <- check_modules_datanames(modules, teal_data_datanames(data)) |
|
40 | -+ | ||
207 | +7x |
- #' sidebarLayout(+ if (!isTRUE(is_modules_ok)) { |
|
41 | -+ | ||
208 | +1x |
- #' sidebarPanel(+ logger::log_error(is_modules_ok) |
|
42 | -+ | ||
209 | +1x |
- #' selectInput("letter", "select a letter:", c(letters[1:3], LETTERS[4:6])),+ checkmate::assert(is_modules_ok, .var.name = "modules") |
|
43 | +210 |
- #' selectInput("number", "select a number:", 1:6),+ } |
|
44 | +211 |
- #' br(),+ |
|
45 | -+ | ||
212 | +6x |
- #' selectInput("color", "select a color:",+ is_filter_ok <- check_filter_datanames(filter, teal_data_datanames(data)) |
|
46 | -+ | ||
213 | +6x |
- #' c("black", "indianred2", "springgreen2", "cornflowerblue"),+ if (!isTRUE(is_filter_ok)) { |
|
47 | -+ | ||
214 | +1x |
- #' multiple = TRUE+ warning(is_filter_ok) |
|
48 | +215 |
- #' ),+ # we allow app to continue if applied filters are outside |
|
49 | +216 |
- #' sliderInput("size", "select point size:",+ # of possible data range |
|
50 | +217 |
- #' min = 0.1, max = 4, value = 0.25+ } |
|
51 | +218 |
- #' )+ } |
|
52 | +219 |
- #' ),+ |
|
53 | +220 |
- #' mainPanel(plotOutput("plot"))+ # Note regarding case `id = character(0)`: |
|
54 | +221 |
- #' )+ # rather than creating a submodule of this module, we directly modify |
|
55 | +222 |
- #' )+ # the UI and server with `id = character(0)` and calling the server function directly |
|
56 | -+ | ||
223 | +7x |
- #'+ res <- list( |
|
57 | -+ | ||
224 | +7x |
- #' server <- function(input, output) {+ ui = ui_teal_with_splash(id = id, data = data, title = title, header = header, footer = footer), |
|
58 | -+ | ||
225 | +7x |
- #' # set up input validation+ server = function(input, output, session) { |
|
59 | -+ | ||
226 | +! |
- #' iv <- InputValidator$new()+ if (!is.null(landing_module)) { |
|
60 | -+ | ||
227 | +! |
- #' iv$add_rule("letter", sv_in_set(LETTERS, "choose a capital letter"))+ do.call(landing_module$server, c(list(id = "landing_module_shiny_id"), landing_module$server_args)) |
|
61 | +228 |
- #' iv$add_rule("number", ~ if (as.integer(.) %% 2L == 1L) "choose an even number")+ } |
|
62 | -+ | ||
229 | +! |
- #' iv$enable()+ srv_teal_with_splash(id = id, data = data, modules = modules, filter = deep_copy_filter(filter)) |
|
63 | +230 |
- #' # more input validation+ } |
|
64 | +231 |
- #' iv_par <- InputValidator$new()+ ) |
|
65 | +232 |
- #' iv_par$add_rule("color", sv_required(message = "choose a color"))+ |
|
66 | -+ | ||
233 | +7x |
- #' iv_par$add_rule("color", ~ if (length(.) > 1L) "choose only one color")+ logger::log_trace("init teal app has been initialized.") |
|
67 | +234 |
- #' iv_par$add_rule(+ |
|
68 | -+ | ||
235 | +7x |
- #' "size",+ res |
|
69 | +236 |
- #' sv_between(+ } |
70 | +1 |
- #' left = 0.5, right = 3,+ #' Show `R` code modal |
|
71 | +2 |
- #' message_fmt = "choose a value between {left} and {right}"+ #' |
|
72 | +3 |
- #' )+ #' @description `r lifecycle::badge("stable")` |
|
73 | +4 |
- #' )+ #' |
|
74 | +5 |
- #' iv_par$enable()+ #' Use the [shiny::showModal()] function to show the `R` code inside. |
|
75 | +6 |
#' |
|
76 | +7 |
- #' output$plot <- renderPlot({+ #' @param title (`character(1)`) |
|
77 | +8 |
- #' # validate output+ #' Title of the modal, displayed in the first comment of the `R` code. |
|
78 | +9 |
- #' switch(input[["method"]],+ #' @param rcode (`character`) |
|
79 | +10 |
- #' "sequential" = {+ #' vector with `R` code to show inside the modal. |
|
80 | +11 |
- #' validate_inputs(iv)+ #' @param session (`ShinySession` optional) |
|
81 | +12 |
- #' validate_inputs(iv_par, header = "Set proper graphical parameters")+ #' `shiny` session object, if missing then [shiny::getDefaultReactiveDomain()] is used. |
|
82 | +13 |
- #' },+ #' |
|
83 | +14 |
- #' "combined" = validate_inputs(iv, iv_par),+ #' @references [shiny::showModal()] |
|
84 | +15 |
- #' "grouped" = validate_inputs(list(+ #' @export |
|
85 | +16 |
- #' "Some inputs require attention" = iv,+ show_rcode_modal <- function(title = NULL, rcode, session = getDefaultReactiveDomain()) { |
|
86 | -+ | ||
17 | +! |
- #' "Set proper graphical parameters" = iv_par+ rcode <- paste(rcode, collapse = "\n") |
|
87 | +18 |
- #' ))+ |
|
88 | -+ | ||
19 | +! |
- #' )+ ns <- session$ns |
|
89 | -+ | ||
20 | +! |
- #'+ showModal(modalDialog( |
|
90 | -+ | ||
21 | +! |
- #' plot(eruptions ~ waiting, faithful,+ tagList( |
|
91 | -+ | ||
22 | +! |
- #' las = 1, pch = 16,+ tags$div( |
|
92 | -+ | ||
23 | +! |
- #' col = input[["color"]], cex = input[["size"]]+ actionButton(ns("copyRCode"), "Copy to Clipboard", `data-clipboard-target` = paste0("#", ns("r_code"))), |
|
93 | -+ | ||
24 | +! |
- #' )+ modalButton("Dismiss"), |
|
94 | -+ | ||
25 | +! |
- #' })+ style = "mb-4" |
|
95 | +26 |
- #' }+ ), |
|
96 | -+ | ||
27 | +! |
- #'+ tags$div(tags$pre(id = ns("r_code"), rcode)), |
|
97 | +28 |
- #' if (interactive()) {+ ), |
|
98 | -+ | ||
29 | +! |
- #' shinyApp(ui, server)+ title = title, |
|
99 | -+ | ||
30 | +! |
- #' }+ footer = tagList( |
|
100 | -+ | ||
31 | +! |
- #'+ actionButton(ns("copyRCode"), "Copy to Clipboard", `data-clipboard-target` = paste0("#", ns("r_code"))), |
|
101 | -+ | ||
32 | +! |
- #' @export+ modalButton("Dismiss") |
|
102 | +33 |
- #'+ ), |
|
103 | -+ | ||
34 | +! |
- validate_inputs <- function(..., header = "Some inputs require attention") {+ size = "l", |
|
104 | -36x | +||
35 | +! |
- dots <- list(...)+ easyClose = TRUE |
|
105 | -2x | +||
36 | +
- if (!is_validators(dots)) stop("validate_inputs accepts validators or a list thereof")+ )) |
||
106 | +37 |
-
+ } |
|
107 | -34x | +
1 | +
- messages <- extract_validator(dots, header)+ #' Landing popup module |
|||||
108 | -34x | +|||||
2 | +
- failings <- if (!any_names(dots)) {+ #' |
|||||
109 | -29x | +|||||
3 | +
- add_header(messages, header)+ #' @description Creates a landing welcome popup for `teal` applications. |
|||||
110 | +4 |
- } else {+ #' |
||||
111 | -5x | +|||||
5 | +
- unlist(messages)+ #' This module is used to display a popup dialog when the application starts. |
|||||
112 | +6 |
- }+ #' The dialog blocks access to the application and must be closed with a button before the application can be viewed. |
||||
113 | +7 |
-
+ #' |
||||
114 | -34x | +|||||
8 | +
- shiny::validate(shiny::need(is.null(failings), failings))+ #' @param label (`character(1)`) Label of the module. |
|||||
115 | +9 |
- }+ #' @param title (`character(1)`) Text to be displayed as popup title. |
||||
116 | +10 |
-
+ #' @param content (`character(1)`, `shiny.tag` or `shiny.tag.list`) with the content of the popup. |
||||
117 | +11 |
- ### internal functions+ #' Passed to `...` of `shiny::modalDialog`. See examples. |
||||
118 | +12 |
-
+ #' @param buttons (`shiny.tag` or `shiny.tag.list`) Typically a `modalButton` or `actionButton`. See examples. |
||||
119 | +13 |
- #' @noRd+ #' |
||||
120 | +14 |
- #' @keywords internal+ #' @return A `teal_module` (extended with `teal_landing_module` class) to be used in `teal` applications. |
||||
121 | +15 |
- # recursive object type test+ #' |
||||
122 | +16 |
- # returns logical of length 1+ #' @examples |
||||
123 | +17 |
- is_validators <- function(x) {+ #' app1 <- init( |
||||
124 | -118x | +|||||
18 | +
- all(if (is.list(x)) unlist(lapply(x, is_validators)) else inherits(x, "InputValidator"))+ #' data = teal_data(iris = iris), |
|||||
125 | +19 |
- }+ #' modules = modules( |
||||
126 | +20 |
-
+ #' landing_popup_module( |
||||
127 | +21 |
- #' @noRd+ #' content = "A place for the welcome message or a disclaimer statement.", |
||||
128 | +22 |
- #' @keywords internal+ #' buttons = modalButton("Proceed") |
||||
129 | +23 |
- # test if an InputValidator object is enabled+ #' ), |
||||
130 | +24 |
- # returns logical of length 1+ #' example_module() |
||||
131 | +25 |
- # official method requested at https://github.com/rstudio/shinyvalidate/issues/64+ #' ) |
||||
132 | +26 |
- validator_enabled <- function(x) {+ #' ) |
||||
133 | -49x | +|||||
27 | +
- x$.__enclos_env__$private$enabled+ #' if (interactive()) { |
|||||
134 | +28 |
- }+ #' shinyApp(app1$ui, app1$server) |
||||
135 | +29 |
-
+ #' } |
||||
136 | +30 |
- #' Recursively extract messages from validator list+ #' |
||||
137 | +31 |
- #' @return A character vector or a list of character vectors, possibly nested and named.+ #' app2 <- init( |
||||
138 | +32 |
- #' @noRd+ #' data = teal_data(iris = iris), |
||||
139 | +33 |
- #' @keywords internal+ #' modules = modules( |
||||
140 | +34 |
- extract_validator <- function(iv, header) {+ #' landing_popup_module( |
||||
141 | -113x | +|||||
35 | +
- if (inherits(iv, "InputValidator")) {+ #' title = "Welcome", |
|||||
142 | -49x | +|||||
36 | +
- add_header(gather_messages(iv), header)+ #' content = tags$b( |
|||||
143 | +37 |
- } else {+ #' "A place for the welcome message or a disclaimer statement.", |
||||
144 | -58x | +|||||
38 | +
- if (is.null(names(iv))) names(iv) <- rep("", length(iv))+ #' style = "color: red;" |
|||||
145 | -64x | +|||||
39 | +
- mapply(extract_validator, iv = iv, header = names(iv), SIMPLIFY = FALSE)+ #' ), |
|||||
146 | +40 |
- }+ #' buttons = tagList( |
||||
147 | +41 |
- }+ #' modalButton("Proceed"), |
||||
148 | +42 |
-
+ #' actionButton("read", "Read more", |
||||
149 | +43 |
- #' Collate failing messages from validator.+ #' onclick = "window.open('http://google.com', '_blank')" |
||||
150 | +44 |
- #' @return `list`+ #' ), |
||||
151 | +45 |
- #' @noRd+ #' actionButton("close", "Reject", onclick = "window.close()") |
||||
152 | +46 |
- #' @keywords internal+ #' ) |
||||
153 | +47 |
- gather_messages <- function(iv) {+ #' ), |
||||
154 | -49x | +|||||
48 | +
- if (validator_enabled(iv)) {+ #' example_module() |
|||||
155 | -46x | +|||||
49 | +
- status <- iv$validate()+ #' ) |
|||||
156 | -46x | +|||||
50 | +
- failing_inputs <- Filter(Negate(is.null), status)+ #' ) |
|||||
157 | -46x | +|||||
51 | +
- unique(lapply(failing_inputs, function(x) x[["message"]]))+ #' |
|||||
158 | +52 |
- } else {+ #' if (interactive()) { |
||||
159 | -3x | +|||||
53 | +
- warning("Validator is disabled and will be omitted.")+ #' shinyApp(app2$ui, app2$server) |
|||||
160 | -3x | +|||||
54 | +
- list()+ #' } |
|||||
161 | +55 |
- }+ #' |
||||
162 | +56 |
- }+ #' @export |
||||
163 | +57 |
-
+ landing_popup_module <- function(label = "Landing Popup", |
||||
164 | +58 |
- #' Add optional header to failing messages+ title = NULL, |
||||
165 | +59 |
- #' @noRd+ content = NULL, |
||||
166 | +60 |
- #' @keywords internal+ buttons = modalButton("Accept")) { |
||||
167 | -+ | |||||
61 | +! |
- add_header <- function(messages, header = "") {+ checkmate::assert_string(label) |
||||
168 | -78x | +|||||
62 | +! |
- ans <- unlist(messages)+ checkmate::assert_string(title, null.ok = TRUE) |
||||
169 | -78x | +|||||
63 | +! |
- if (length(ans) != 0L && isTRUE(nchar(header) > 0L)) {+ checkmate::assert_multi_class( |
||||
170 | -31x | +|||||
64 | +! |
- ans <- c(paste0(header, "\n"), ans, "\n")+ content,+ |
+ ||||
65 | +! | +
+ classes = c("character", "shiny.tag", "shiny.tag.list", "html"), null.ok = TRUE |
||||
171 | +66 |
- }+ ) |
||||
172 | -78x | +|||||
67 | +! |
- ans+ checkmate::assert_multi_class(buttons, classes = c("shiny.tag", "shiny.tag.list")) |
||||
173 | +68 |
- }+ + |
+ ||||
69 | +! | +
+ logger::log_info("Initializing landing_popup_module") |
||||
174 | +70 | |||||
175 | -+ | |||||
71 | +! |
- #' Recursively check if the object contains a named list+ module <- module( |
||||
176 | -+ | |||||
72 | +! |
- #' @noRd+ label = label, |
||||
177 | -+ | |||||
73 | +! |
- #' @keywords internal+ server = function(id) { |
||||
178 | -+ | |||||
74 | +! |
- any_names <- function(x) {+ moduleServer(id, function(input, output, session) { |
||||
179 | -103x | +|||||
75 | +! |
- any(+ showModal( |
||||
180 | -103x | +|||||
76 | +! |
- if (is.list(x)) {+ modalDialog( |
||||
181 | -58x | +|||||
77 | +! |
- if (!is.null(names(x)) && any(names(x) != "")) TRUE else unlist(lapply(x, any_names))+ id = "landingpopup",+ |
+ ||||
78 | +! | +
+ title = title,+ |
+ ||||
79 | +! | +
+ content,+ |
+ ||||
80 | +! | +
+ footer = buttons |
||||
182 | +81 |
- } else {+ ) |
||||
183 | -40x | +|||||
82 | +
- FALSE+ ) |
|||||
184 | +83 | ++ |
+ })+ |
+ |||
84 |
} |
|||||
185 | +85 |
) |
||||
86 | +! | +
+ class(module) <- c("teal_module_landing", class(module))+ |
+ ||||
87 | +! | +
+ module+ |
+ ||||
186 | +88 |
}@@ -29520,207 +27784,151 @@ teal coverage - 68.02% | 62 |
- }- |
- ||
63 | -- | - - | -||||
64 | -- |
- #' Code to include `teal` `CSS` and `JavaScript` files- |
- ||||
65 | -- |
- #'- |
- ||||
66 | -- |
- #' This is useful when you want to use the same `JavaScript` and `CSS` files that are- |
- ||||
67 | -- |
- #' used with the `teal` application.- |
- ||||
68 | -- |
- #' This is also useful for running standalone modules in `teal` with the correct- |
- ||||
69 | -- |
- #' styles.- |
- ||||
70 | -- |
- #' Also initializes `shinyjs` so you can use it.+ } |
||||
71 | +63 |
- #'+ |
||||
72 | +64 |
- #' Simply add `include_teal_css_js()` as one of the UI elements.+ #' Code to include `teal` `CSS` and `JavaScript` files |
||||
73 | +65 |
- #' @return A `shiny.tag.list`.+ #' |
||||
74 | +66 |
- #' @examples+ #' This is useful when you want to use the same `JavaScript` and `CSS` files that are |
||||
75 | +67 |
- #' # use non-exported function from teal+ #' used with the `teal` application. |
||||
76 | +68 |
- #' include_teal_css_js <- getFromNamespace("include_teal_css_js", "teal")+ #' This is also useful for running standalone modules in `teal` with the correct |
||||
77 | +69 |
- #' shiny_ui <- tagList(+ #' styles. |
||||
78 | +70 |
- #' include_teal_css_js(),+ #' Also initializes `shinyjs` so you can use it. |
||||
79 | +71 |
- #' p("Hello")+ #' |
||||
80 | +72 |
- #' )+ #' Simply add `include_teal_css_js()` as one of the UI elements. |
||||
81 | +73 |
- #'+ #' @return A `shiny.tag.list`. |
||||
82 | +74 |
#' @keywords internal |
||||
83 | +75 |
include_teal_css_js <- function() { |
||||
84 | +76 | 7x |
tagList( |
|||
85 | +77 | 7x |
shinyjs::useShinyjs(), |
|||
86 | +78 | 7x |
include_css_files(), |
|||
87 | +79 |
# init.js is executed from the server |
||||
88 | +80 | 7x |
include_js_files(except = "init.js"), |
|||
89 | +81 | 7x |
shinyjs::hidden(icon("gear")), # add hidden icon to load font-awesome css for icons |
|||
90 | +82 |
) |
||||
91 | +83 |
}@@ -29729,77 +27937,77 @@ teal coverage - 68.02% |
1 |
- #' Evaluate expression on `teal_data_module`+ setOldClass("teal_data_module") |
|||
2 |
- #'+ |
|||
3 |
- #' @details+ #' Evaluate code on `teal_data_module` |
|||
4 |
- #' `within` is a convenience function for evaluating inline code inside the environment of a `teal_data_module`.+ #' |
|||
5 |
- #' It accepts only inline expressions (both simple and compound) and allows for injecting values into `expr` through+ #' @details |
|||
6 |
- #' the `...` argument: as `name:value` pairs are passed to `...`, `name` in `expr` will be replaced with `value.`+ #' `eval_code` evaluates given code in the environment of the `teal_data` object created by the `teal_data_module`. |
|||
7 |
- #'+ #' The code is added to the `@code` slot of the `teal_data`. |
|||
8 |
- #' @param data (`teal_data_module`) object+ #' |
|||
9 |
- #' @param expr (`expression`) to evaluate. Must be inline code. See+ #' @param object (`teal_data_module`) |
|||
10 |
- #' @param ... See `Details`.+ #' @inheritParams teal.code::eval_code |
|||
13 |
- #' `within` returns a `teal_data_module` object with a delayed evaluation of `expr` when the module is run.+ #' `eval_code` returns a `teal_data_module` object with a delayed evaluation of `code` when the module is run. |
|||
16 |
- #' within(tdm, dataset1 <- subset(dataset1, Species == "virginica"))+ #' eval_code(tdm, "dataset1 <- subset(dataset1, Species == 'virginica')") |
|||
18 |
- #' # use additional parameter for expression value substitution.+ #' @include teal_data_module.R |
|||
19 |
- #' valid_species <- "versicolor"+ #' @name eval_code |
|||
20 |
- #' within(tdm, dataset1 <- subset(dataset1, Species %in% species), species = valid_species)+ #' @rdname teal_data_module |
|||
21 |
- #' @include teal_data_module.R+ #' @aliases eval_code,teal_data_module,character-method+ |
+ |||
22 | ++ |
+ #' @aliases eval_code,teal_data_module,language-method+ |
+ ||
23 | ++ |
+ #' @aliases eval_code,teal_data_module,expression-method+ |
+ ||
24 | ++ |
+ #'+ |
+ ||
25 | ++ |
+ #' @importFrom methods setMethod+ |
+ ||
26 | ++ |
+ #' @importMethodsFrom teal.code eval_code+ |
+ ||
27 | ++ |
+ #'+ |
+ ||
28 | ++ |
+ setMethod("eval_code", signature = c("teal_data_module", "character"), function(object, code) {+ |
+ ||
29 | +13x | +
+ teal_data_module(+ |
+ ||
30 | +13x | +
+ ui = function(id) {+ |
+ ||
31 | +1x | +
+ ns <- NS(id)+ |
+ ||
32 | +1x | +
+ object$ui(ns("mutate_inner"))+ |
+ ||
33 | ++ |
+ },+ |
+ ||
34 | +13x | +
+ server = function(id) {+ |
+ ||
35 | +11x | +
+ moduleServer(id, function(input, output, session) {+ |
+ ||
36 | +11x | +
+ teal_data_rv <- object$server("mutate_inner")+ |
+ ||
37 | ++ | + + | +||
38 | +11x | +
+ if (!is.reactive(teal_data_rv)) {+ |
+ ||
39 | +1x | +
+ stop("The `teal_data_module` must return a reactive expression.", call. = FALSE)+ |
+ ||
40 | ++ |
+ }+ |
+ ||
41 | ++ | + + | +||
42 | +10x | +
+ td <- eventReactive(teal_data_rv(),+ |
+ ||
43 | ++ |
+ {+ |
+ ||
44 | +10x | +
+ if (inherits(teal_data_rv(), c("teal_data", "qenv.error"))) { |
||
22 | -+ | |||
45 | +6x |
- #' @name within+ eval_code(teal_data_rv(), code) |
||
23 | +46 |
- #' @rdname teal_data_module+ } else { |
||
24 | -+ | |||
47 | +4x |
- #'+ teal_data_rv() |
||
25 | +48 |
- #' @export+ } |
||
26 | +49 |
- #'+ }, |
||
27 | -+ | |||
50 | +10x |
- within.teal_data_module <- function(data, expr, ...) {+ ignoreNULL = FALSE |
||
28 | -6x | +|||
51 | +
- expr <- substitute(expr)+ ) |
|||
29 | -6x | +52 | +10x |
- extras <- list(...)+ td |
30 | +53 |
-
+ }) |
||
31 | +54 |
- # Add braces for consistency.+ } |
||
32 | -6x | +|||
55 | +
- if (!identical(as.list(expr)[[1L]], as.symbol("{"))) {+ ) |
|||
33 | -6x | +|||
56 | +
- expr <- call("{", expr)+ }) |
|||
34 | +57 |
- }+ |
||
35 | +58 |
-
+ setMethod("eval_code", signature = c("teal_data_module", "language"), function(object, code) { |
||
36 | -6x | +59 | +1x |
- calls <- as.list(expr)[-1]+ eval_code(object, code = paste(lang2calls(code), collapse = "\n")) |
37 | +60 |
-
+ }) |
||
38 | +61 |
- # Inject extra values into expressions.- |
- ||
39 | -6x | -
- calls <- lapply(calls, function(x) do.call(substitute, list(x, env = extras)))+ |
||
40 | +62 |
-
+ setMethod("eval_code", signature = c("teal_data_module", "expression"), function(object, code) { |
||
41 | +63 | 6x |
- eval_code(object = data, code = as.expression(calls))+ eval_code(object, code = paste(lang2calls(code), collapse = "\n")) |
|
42 | +64 |
- }+ }) |
1 |
- #' An example `teal` module+ #' Evaluate expression on `teal_data_module` |
|||
3 |
- #' `r lifecycle::badge("experimental")`+ #' @details |
|||
4 |
- #'+ #' `within` is a convenience function for evaluating inline code inside the environment of a `teal_data_module`. |
|||
5 |
- #' @inheritParams teal_modules+ #' It accepts only inline expressions (both simple and compound) and allows for injecting values into `expr` through |
|||
6 |
- #' @return A `teal` module which can be included in the `modules` argument to [init()].+ #' the `...` argument: as `name:value` pairs are passed to `...`, `name` in `expr` will be replaced with `value.` |
|||
7 |
- #' @examples+ #' |
|||
8 |
- #' app <- init(+ #' @param data (`teal_data_module`) object |
|||
9 |
- #' data = teal_data(IRIS = iris, MTCARS = mtcars),+ #' @param expr (`expression`) to evaluate. Must be inline code. See |
|||
10 |
- #' modules = example_module()+ #' @param ... See `Details`. |
|||
11 |
- #' )+ #' |
|||
12 |
- #' if (interactive()) {+ #' @return |
|||
13 |
- #' shinyApp(app$ui, app$server)+ #' `within` returns a `teal_data_module` object with a delayed evaluation of `expr` when the module is run. |
|||
14 |
- #' }+ #' |
|||
15 |
- #' @export+ #' @examples |
|||
16 |
- example_module <- function(label = "example teal module", datanames = "all") {+ #' within(tdm, dataset1 <- subset(dataset1, Species == "virginica")) |
|||
17 | -49x | +
- checkmate::assert_string(label)+ #' |
||
18 | -49x | +
- module(+ #' # use additional parameter for expression value substitution. |
||
19 | -49x | +
- label,+ #' valid_species <- "versicolor" |
||
20 | -49x | +
- server = function(id, data) {+ #' within(tdm, dataset1 <- subset(dataset1, Species %in% species), species = valid_species) |
||
21 | -! | +
- checkmate::assert_class(data(), "teal_data")+ #' @include teal_data_module.R |
||
22 | -! | +
- moduleServer(id, function(input, output, session) {+ #' @name within |
||
23 | -! | +
- updateSelectInput(session, "dataname", choices = isolate(teal.data::datanames(data())))+ #' @rdname teal_data_module |
||
24 | -! | -
- output$text <- renderPrint({- |
- ||
25 | -! | -
- req(input$dataname)- |
- ||
26 | -! | +
- data()[[input$dataname]]+ #' |
||
27 | +25 |
- })- |
- ||
28 | -! | -
- teal.widgets::verbatim_popup_srv(- |
- ||
29 | -! | -
- id = "rcode",- |
- ||
30 | -! | -
- verbatim_content = reactive(teal.code::get_code(data())),- |
- ||
31 | -! | -
- title = "Example Code"+ #' @export |
||
32 | +26 |
- )+ #' |
||
33 | +27 |
- })+ within.teal_data_module <- function(data, expr, ...) { |
||
34 | -+ | |||
28 | +6x |
- },+ expr <- substitute(expr) |
||
35 | -49x | +29 | +6x |
- ui = function(id) {+ extras <- list(...) |
36 | -! | +|||
30 | +
- ns <- NS(id)+ |
|||
37 | -! | +|||
31 | +
- teal.widgets::standard_layout(+ # Add braces for consistency. |
|||
38 | -! | +|||
32 | +6x |
- output = verbatimTextOutput(ns("text")),+ if (!identical(as.list(expr)[[1L]], as.symbol("{"))) { |
||
39 | -! | +|||
33 | +6x |
- encoding = div(+ expr <- call("{", expr) |
||
40 | -! | +|||
34 | +
- selectInput(ns("dataname"), "Choose a dataset", choices = NULL),+ } |
|||
41 | -! | +|||
35 | +
- teal.widgets::verbatim_popup_ui(ns("rcode"), "Show R code")+ |
|||
42 | -+ | |||
36 | +6x |
- )+ calls <- as.list(expr)[-1] |
||
43 | +37 |
- )+ |
||
44 | +38 |
- },+ # Inject extra values into expressions. |
||
45 | -49x | +39 | +6x |
- datanames = datanames+ calls <- lapply(calls, function(x) do.call(substitute, list(x, env = extras))) |
46 | +40 |
- )+ + |
+ ||
41 | +6x | +
+ eval_code(object = data, code = as.expression(calls)) |
||
47 | +42 |
}@@ -30364,530 +28691,600 @@ teal coverage - 68.02% |
1 |
- #' Show `R` code modal+ .onLoad <- function(libname, pkgname) { # nolint |
||
2 |
- #'+ # adapted from https://github.com/r-lib/devtools/blob/master/R/zzz.R |
||
3 | -+ | ! |
- #' @description `r lifecycle::badge("stable")`+ teal_default_options <- list(teal.show_js_log = FALSE) |
4 |
- #'+ |
||
5 | -+ | ! |
- #' Use the [shiny::showModal()] function to show the `R` code inside.+ op <- options() |
6 | -+ | ! |
- #'+ toset <- !(names(teal_default_options) %in% names(op)) |
7 | -+ | ! |
- #' @param title (`character(1)`)+ if (any(toset)) options(teal_default_options[toset]) |
8 |
- #' Title of the modal, displayed in the first comment of the `R` code.+ |
||
9 | -+ | ! |
- #' @param rcode (`character`)+ options("shiny.sanitize.errors" = FALSE) |
10 |
- #' vector with `R` code to show inside the modal.+ |
||
11 |
- #' @param session (`ShinySession` optional)+ # Set up the teal logger instance |
||
12 | -+ | ! |
- #' `shiny` session object, if missing then [shiny::getDefaultReactiveDomain()] is used.+ teal.logger::register_logger("teal") |
13 |
- #'+ |
||
14 | -+ | ! |
- #' @references [shiny::showModal()]+ invisible() |
15 |
- #' @export+ } |
||
16 |
- show_rcode_modal <- function(title = NULL, rcode, session = getDefaultReactiveDomain()) {+ |
||
17 | -! | +
- rcode <- paste(rcode, collapse = "\n")+ .onAttach <- function(libname, pkgname) { # nolint |
|
18 | -+ | 2x |
-
+ packageStartupMessage( |
19 | -! | +2x |
- ns <- session$ns+ "\nYou are using teal version ", |
20 | -! | +
- showModal(modalDialog(+ # `system.file` uses the `shim` of `system.file` by `teal` |
|
21 | -! | +
- tagList(+ # we avoid `desc` dependency here to get the version |
|
22 | -! | +2x |
- tags$div(+ read.dcf(system.file("DESCRIPTION", package = "teal"))[, "Version"] |
23 | -! | +
- actionButton(ns("copyRCode"), "Copy to Clipboard", `data-clipboard-target` = paste0("#", ns("r_code"))),+ ) |
|
24 | -! | +
- modalButton("Dismiss"),+ } |
|
25 | -! | +
- style = "mb-4"+ |
|
26 |
- ),+ # This one is here because setdiff_teal_slice should not be exported from teal.slice. |
||
27 | -! | +
- tags$div(tags$pre(id = ns("r_code"), rcode)),+ setdiff_teal_slices <- getFromNamespace("setdiff_teal_slices", "teal.slice") |
|
28 |
- ),+ # This one is here because it is needed by c.teal_slices but we don't want it exported from teal.slice. |
||
29 | -! | +
- title = title,+ coalesce_r <- getFromNamespace("coalesce_r", "teal.slice") |
|
30 | -! | +
- footer = tagList(+ # all *Block objects are private in teal.reporter |
|
31 | -! | -
- actionButton(ns("copyRCode"), "Copy to Clipboard", `data-clipboard-target` = paste0("#", ns("r_code"))),- |
- |
32 | -! | +
- modalButton("Dismiss")+ RcodeBlock <- getFromNamespace("RcodeBlock", "teal.reporter") # nolint |
|
33 | +32 |
- ),- |
- |
34 | -! | -
- size = "l",+ |
- |
35 | -! | +||
33 | +
- easyClose = TRUE+ # Use non-exported function(s) from teal.code |
||
36 | +34 |
- ))+ # This one is here because lang2calls should not be exported from teal.code |
|
37 | +35 |
- }+ lang2calls <- getFromNamespace("lang2calls", "teal.code") |
1 |
- .onLoad <- function(libname, pkgname) { # nolint+ #' An example `teal` module |
||
2 |
- # adapted from https://github.com/r-lib/devtools/blob/master/R/zzz.R+ #' |
||
3 | -! | +
- teal_default_options <- list(teal.show_js_log = FALSE)+ #' `r lifecycle::badge("experimental")` |
|
4 |
-
+ #' |
||
5 | -! | +
- op <- options()+ #' @inheritParams teal_modules |
|
6 | -! | +
- toset <- !(names(teal_default_options) %in% names(op))+ #' @return A `teal` module which can be included in the `modules` argument to [init()]. |
|
7 | -! | +
- if (any(toset)) options(teal_default_options[toset])+ #' @examples |
|
8 |
-
+ #' app <- init( |
||
9 | -! | +
- options("shiny.sanitize.errors" = FALSE)+ #' data = teal_data(IRIS = iris, MTCARS = mtcars), |
|
10 |
-
+ #' modules = example_module() |
||
11 |
- # Set up the teal logger instance+ #' ) |
||
12 | -! | +
- teal.logger::register_logger("teal")+ #' if (interactive()) { |
|
13 |
-
+ #' shinyApp(app$ui, app$server) |
||
14 | -! | +
- invisible()+ #' } |
|
15 |
- }+ #' @export |
||
16 |
-
+ example_module <- function(label = "example teal module", datanames = "all") { |
||
17 | -+ | 49x |
- .onAttach <- function(libname, pkgname) { # nolint+ checkmate::assert_string(label) |
18 | -2x | +49x |
- packageStartupMessage(+ module( |
19 | -2x | +49x |
- "\nYou are using teal version ",+ label, |
20 | -+ | 49x |
- # `system.file` uses the `shim` of `system.file` by `teal`+ server = function(id, data) { |
21 | -+ | ! |
- # we avoid `desc` dependency here to get the version+ checkmate::assert_class(data(), "teal_data") |
22 | -2x | +! |
- read.dcf(system.file("DESCRIPTION", package = "teal"))[, "Version"]+ moduleServer(id, function(input, output, session) { |
23 | -+ | ! |
- )+ updateSelectInput(session, "dataname", choices = isolate(teal.data::datanames(data()))) |
24 | -+ | ! |
- }+ output$text <- renderPrint({ |
25 | -+ | ! |
-
+ req(input$dataname) |
26 | -+ | ! |
- # This one is here because setdiff_teal_slice should not be exported from teal.slice.+ data()[[input$dataname]] |
27 |
- setdiff_teal_slices <- getFromNamespace("setdiff_teal_slices", "teal.slice")+ }) |
||
28 | +! | +
+ teal.widgets::verbatim_popup_srv(+ |
+ |
29 | +! | +
+ id = "rcode",+ |
+ |
30 | +! | +
+ verbatim_content = reactive(teal.code::get_code(data())),+ |
+ |
31 | +! | +
+ title = "Example Code"+ |
+ |
32 |
- # This one is here because it is needed by c.teal_slices but we don't want it exported from teal.slice.+ ) |
||
29 | +33 |
- coalesce_r <- getFromNamespace("coalesce_r", "teal.slice")+ }) |
|
30 | +34 |
- # all *Block objects are private in teal.reporter+ },+ |
+ |
35 | +49x | +
+ ui = function(id) {+ |
+ |
36 | +! | +
+ ns <- NS(id)+ |
+ |
37 | +! | +
+ teal.widgets::standard_layout(+ |
+ |
38 | +! | +
+ output = verbatimTextOutput(ns("text")),+ |
+ |
39 | +! | +
+ encoding = div(+ |
+ |
40 | +! | +
+ selectInput(ns("dataname"), "Choose a dataset", choices = NULL),+ |
+ |
41 | +! | +
+ teal.widgets::verbatim_popup_ui(ns("rcode"), "Show R code") |
|
31 | +42 |
- RcodeBlock <- getFromNamespace("RcodeBlock", "teal.reporter") # nolint+ ) |
|
32 | +43 |
-
+ ) |
|
33 | +44 |
- # Use non-exported function(s) from teal.code+ },+ |
+ |
45 | +49x | +
+ datanames = datanames |
|
34 | +46 |
- # This one is here because lang2calls should not be exported from teal.code+ ) |
|
35 | +47 |
- lang2calls <- getFromNamespace("lang2calls", "teal.code")+ } |
1 |
- #' Data module for `teal` applications+ #' Create a `teal` module for previewing a report |
||
3 |
- #' @description+ #' @description `r lifecycle::badge("experimental")` |
||
4 |
- #' `r lifecycle::badge("experimental")`+ #' |
||
5 |
- #'+ #' This function wraps [teal.reporter::reporter_previewer_ui()] and |
||
6 |
- #' Create a `teal_data_module` object and evaluate code on it with history tracking.+ #' [teal.reporter::reporter_previewer_srv()] into a `teal_module` to be |
||
7 |
- #'+ #' used in `teal` applications. |
||
8 |
- #' @details+ #' |
||
9 |
- #' `teal_data_module` creates a `shiny` module to supply or modify data in a `teal` application.+ #' If you are creating a `teal` application using [init()] then this |
||
10 |
- #' The module allows for running data pre-processing code (creation _and_ some modification) after the app starts.+ #' module will be added to your application automatically if any of your `teal_modules` |
||
11 |
- #' The body of the server function will be run in the app rather than in the global environment.+ #' support report generation. |
||
12 |
- #' This means it will be run every time the app starts, so use sparingly.+ #' |
||
13 |
- #'+ #' @inheritParams teal_modules |
||
14 |
- #' Pass this module instead of a `teal_data` object in a call to [init()].+ #' @param server_args (named `list`) |
||
15 |
- #' Note that the server function must always return a `teal_data` object wrapped in a reactive expression.+ #' Arguments passed to [teal.reporter::reporter_previewer_srv()]. |
||
17 |
- #' See vignette `vignette("data-as-shiny-module", package = "teal")` for more details.+ #' @return |
||
18 |
- #'+ #' `teal_module` (extended with `teal_module_previewer` class) containing the `teal.reporter` previewer functionality. |
||
19 |
- #' @param ui (`function(id)`)+ #' |
||
20 |
- #' `shiny` module UI function; must only take `id` argument+ #' @export |
||
21 |
- #' @param server (`function(id)`)+ #' |
||
22 |
- #' `shiny` module server function; must only take `id` argument;+ reporter_previewer_module <- function(label = "Report previewer", server_args = list()) { |
||
23 | -+ | 4x |
- #' must return reactive expression containing `teal_data` object+ checkmate::assert_string(label) |
24 | -+ | 2x |
- #'+ checkmate::assert_list(server_args, names = "named") |
25 | -+ | 2x |
- #' @return+ checkmate::assert_true(all(names(server_args) %in% names(formals(teal.reporter::reporter_previewer_srv)))) |
26 |
- #' `teal_data_module` returns an object of class `teal_data_module`.+ |
||
27 | -+ | 2x |
- #'+ logger::log_info("Initializing reporter_previewer_module") |
28 |
- #' @examples+ |
||
29 | -+ | 2x |
- #' tdm <- teal_data_module(+ srv <- function(id, reporter, ...) { |
30 | -+ | ! |
- #' ui = function(id) {+ teal.reporter::reporter_previewer_srv(id, reporter, ...) |
31 |
- #' ns <- NS(id)+ } |
||
32 |
- #' actionButton(ns("submit"), label = "Load data")+ |
||
33 | -+ | 2x |
- #' },+ ui <- function(id, ...) { |
34 | -+ | ! |
- #' server = function(id) {+ teal.reporter::reporter_previewer_ui(id, ...) |
35 |
- #' moduleServer(id, function(input, output, session) {+ } |
||
36 |
- #' eventReactive(input$submit, {+ |
||
37 | -+ | 2x |
- #' data <- within(+ module <- module( |
38 | -+ | 2x |
- #' teal_data(),+ label = "temporary label", |
39 | -+ | 2x |
- #' {+ server = srv, ui = ui, |
40 | -+ | 2x |
- #' dataset1 <- iris+ server_args = server_args, ui_args = list(), datanames = NULL |
41 |
- #' dataset2 <- mtcars+ ) |
||
42 |
- #' }+ # Module is created with a placeholder label and the label is changed later. |
||
43 |
- #' )+ # This is to prevent another module being labeled "Report previewer". |
||
44 | -+ | 2x |
- #' datanames(data) <- c("dataset1", "dataset2")+ class(module) <- c("teal_module_previewer", class(module)) |
45 | -+ | 2x |
- #'+ module$label <- label |
46 | -+ | 2x |
- #' data+ module |
47 | - |
- #' })- |
- |
48 | -- |
- #' })- |
- |
49 | -- |
- #' }- |
- |
50 | -- |
- #' )- |
- |
51 | -- |
- #'- |
- |
52 | -- |
- #' @name teal_data_module- |
- |
53 | -- |
- #' @seealso [`teal.data::teal_data-class`], [teal.code::qenv()]- |
- |
54 | -- |
- #'- |
- |
55 | -- |
- #' @export- |
- |
56 | -- |
- teal_data_module <- function(ui, server) {- |
- |
57 | -36x | -
- checkmate::assert_function(ui, args = "id", nargs = 1)- |
- |
58 | -35x | -
- checkmate::assert_function(server, args = "id", nargs = 1)- |
- |
59 | -34x | -
- structure(- |
- |
60 | -34x | -
- list(ui = ui, server = server),- |
- |
61 | -34x | -
- class = "teal_data_module"- |
- |
62 | -- |
- )- |
- |
63 | -
} |