Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

898 save app state version 3 #1011

Merged
merged 134 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
134 commits
Select commit Hold shift + click to select a range
9ee3c22
create functions to grab and restore app state
Aug 24, 2023
4278015
add state manager module
Aug 24, 2023
edfd6be
amend documentation
Aug 24, 2023
1b3b524
insert state manager into filter manager
Aug 24, 2023
1b149e8
rename one funciton
Aug 24, 2023
a7405d9
omit action buttons from grabs
Aug 24, 2023
6a0cc29
re-click until grab fully reset
Aug 24, 2023
130fbc8
encapsulate creating grabs
Aug 24, 2023
3b963fa
reorder file
Aug 24, 2023
634b5e8
amend documentation
Aug 24, 2023
d452326
remove dewclassing of grabbed values to keep dates and date times
Aug 25, 2023
e131b71
handle POSIXct in airDatePickerInput
Aug 25, 2023
7c21911
Merge e131b713094b85a6f73f85d82b97a5ff8e3bce87 into 54c683b92f845075a…
chlebowa Aug 25, 2023
709c4ef
[skip actions] Restyle files
github-actions[bot] Aug 28, 2023
c2e0be4
[skip actions] Roxygen Man Pages Auto Update
dependabot-preview[bot] Aug 28, 2023
735650c
remove storing initial input state as always empty
Aug 28, 2023
f854b9d
spelling
Aug 28, 2023
556c1bb
spelling
Aug 28, 2023
696375e
Merge branch 'main' into 898_save_app_state@main
chlebowa Sep 8, 2023
3e40918
use native shiny bookmarking
Sep 8, 2023
0c22dcd
open bookmarks in new window
Sep 8, 2023
b9e3e37
minor update to defauts
Sep 11, 2023
958c8d7
Merge branch 'main' into 898_save_app_state2@main
Dec 14, 2023
edb94dc
fix logic operators
Dec 14, 2023
67f8be5
fix state manager server definition and call
Dec 14, 2023
5606703
add hook to teal to set shiny option for bookmarking
Dec 15, 2023
5b47506
fix bookmarking and restoring callbacks
Dec 15, 2023
a583e23
add missing arguments in state manager and add return value in snapsh…
Dec 15, 2023
47cc2a5
properly call public methods of session object
Dec 15, 2023
81f10fb
add more missing arguments to state manager module
Dec 15, 2023
5bb0125
clean up docs
Dec 15, 2023
9e9c9de
rebuild module
Dec 15, 2023
e2fea5d
remove grab_state function
Dec 15, 2023
ca24e29
add argument checks to state manager module
Dec 15, 2023
3ec250d
remove filter panel exclusion
Dec 15, 2023
c9bbb54
don't store initial app state
Dec 15, 2023
63eb738
clean up comemnts
Dec 15, 2023
c3d40cf
init returns ui as function
Dec 18, 2023
ac363d2
use state manager module always
Dec 18, 2023
f24dd8c
Merge branch 'main' into 898_save_app_state3@main
Dec 18, 2023
b02fee4
Merge branch 'main' into 898_save_app_state3@main
Mar 13, 2024
c0f854c
all managers return
Mar 13, 2024
159e337
isolate manager_manager_module
Mar 13, 2024
0eb331e
rename manager_manager where it is used
Mar 13, 2024
ef6c049
move snapshot_manager and state_manager to manager manager
Mar 13, 2024
d25397c
rename module
Mar 13, 2024
b8b7b5c
shorten line
Mar 13, 2024
712a5c3
remove superfluous utility function
Mar 13, 2024
1084353
rename module to bookmark_manager
Mar 13, 2024
b950e31
rename grab to bookmark
Mar 13, 2024
a3d2147
fix typo
Mar 14, 2024
2149306
separate utility function in filter manager
Mar 14, 2024
af3ebfc
move renaming of global filtered data list
Mar 14, 2024
8ba143a
rename flat filtered data list and tweak utility function
Mar 14, 2024
6a3689a
rename filtered_data_list to datasets
Mar 14, 2024
fe0afb0
rename filtered_data_flat to datasets_flat
Mar 14, 2024
e59ab91
improve documentation for flatten_datasets
Mar 14, 2024
2b08206
add logging to all manager modules
Mar 14, 2024
3b82671
update name of programmatically clicked button
Mar 14, 2024
4371359
fix erroneous log
Mar 14, 2024
5d62ff4
remove delayed module initiation when starting from bookmark
Mar 14, 2024
23d408d
bug fix: missing argument value
Mar 14, 2024
c4b669c
fix update in example module
Mar 14, 2024
6d07bd2
improve logs in bookmark manager
Mar 14, 2024
43752bf
add bookmark exclusions
Mar 14, 2024
cd6c6bc
add code comment
Mar 14, 2024
fc3872a
modify button titles
Mar 15, 2024
8181ca6
remove superfluous CSS
Mar 15, 2024
07fa52f
change CSS class names
Mar 15, 2024
d896983
adjust style of bnon-first children in manager_table_row
Mar 15, 2024
6fbf146
Merge branch 'main' into 898_save_app_state3@main
chlebowa Mar 15, 2024
ed34a94
update code comment
Mar 15, 2024
2495fb3
simplify 'when from bookmark' condition
Mar 15, 2024
81f9346
Merge branch '898_save_app_state3@main' of github.com:insightsenginee…
Mar 15, 2024
5b5a1e2
replace bookmarkButton with action Button
Mar 15, 2024
debf0f2
amend documentation
Mar 15, 2024
24966a6
amend NEWS
Mar 15, 2024
ed94c39
rearrange code
Mar 15, 2024
38c9598
[skip style] [skip vbump] Restyle files
github-actions[bot] Mar 15, 2024
fa33460
fix spelling
Mar 18, 2024
9ac1fbd
Merge branch 'main' into 898_save_app_state3@main
chlebowa Mar 18, 2024
4dcc66c
improve flow control condition
Mar 18, 2024
882c1cf
amend unit tests
Mar 18, 2024
9db31ac
Merge branch '898_save_app_state3@main' of github.com:insightsenginee…
Mar 18, 2024
40d7cb9
assign return value in wunder_bar server
Mar 18, 2024
6b99ded
simplify unit test for snapshot manager
Mar 18, 2024
2b81ce2
add unit tests for wunder_bar module
Mar 18, 2024
47f93dd
Merge branch 'main' into 898_save_app_state3@main
chlebowa Mar 19, 2024
7888356
Merge branch 'main' into 898_save_app_state3@main
chlebowa Mar 19, 2024
68a7a0d
Merge branch 'main' into 898_save_app_state3@main
chlebowa Mar 19, 2024
a766110
remove delay on starting report previewer
Mar 20, 2024
40bed62
exclude all buttons (app-wide) from bookmark
Mar 20, 2024
7e3aed9
Merge branch 'main' into 898_save_app_state3@main
chlebowa Mar 20, 2024
92391a5
simplify code
Mar 21, 2024
a83b42c
change condition for initiating reporte previewer module
Mar 21, 2024
e2836b0
Merge branch 'main' into 898_save_app_state3@main
Mar 21, 2024
9d7d7b9
restoreValue
gogonzo Mar 22, 2024
557364c
remove browser
gogonzo Mar 22, 2024
be642f5
trigger
Mar 22, 2024
d36aac6
Merge branch 'main' into 898_save_app_state3@main
Mar 22, 2024
55bf1e0
add documentation for restoreValue
Mar 22, 2024
75ffb21
add function for comparing bookmarked states
Mar 22, 2024
4b94872
reorganize code
Mar 22, 2024
72bce64
amend documentation for restoreValue
Mar 22, 2024
c4e7556
move storing values to respective modules
Mar 25, 2024
c0d1e32
correct for namespace when restoring filter in module_teal
Mar 25, 2024
1b716d2
[skip roxygen] [skip vbump] Roxygen Man Pages Auto Update
dependabot-preview[bot] Mar 25, 2024
02a7db4
modify restoreValue
Mar 25, 2024
320014e
update documentation for restoreValue
Mar 25, 2024
5dbf498
clean up bookmark exclusions in bookmark manager
Mar 25, 2024
02b6710
modify storing filter state on bookmark
Mar 25, 2024
9e0f213
add comment headers
Mar 25, 2024
5c1896e
add enforcement of server-side bookmarks
Mar 25, 2024
ae46faa
register bookmark exclusions in snapshot manager
Mar 25, 2024
3423345
[skip style] [skip vbump] Restyle files
github-actions[bot] Mar 25, 2024
0e25d4c
[skip roxygen] [skip vbump] Roxygen Man Pages Auto Update
dependabot-preview[bot] Mar 25, 2024
e1df2fd
remove agruments in bookmark manager
Mar 25, 2024
b01a395
fix return value
Mar 25, 2024
5294f7c
exclude buttons from bookmark in wunder bar
Mar 25, 2024
56c55a0
amend documentation for snapshot and bookmark managers
Mar 25, 2024
8aac98c
extend message in bookmarks_identical
Mar 25, 2024
bf8b39b
teal_bookmarkable flags
Mar 26, 2024
d4a2b4d
only set app id on filter when missing
Mar 26, 2024
9edf512
Bookmarking info (#1184)
gogonzo Mar 28, 2024
92890d5
remove test
gogonzo Mar 28, 2024
30e8b28
trigger
Mar 28, 2024
33a5ba7
Merge branch 'main' into 898_save_app_state3@main
chlebowa Mar 28, 2024
a3659db
fix docs collate
gogonzo Mar 28, 2024
d3a0e09
fix docs
gogonzo Mar 28, 2024
7b40974
[skip roxygen] [skip vbump] Roxygen Man Pages Auto Update
dependabot-preview[bot] Mar 28, 2024
0aca94c
trigger
Mar 28, 2024
97c2efb
adding testing
gogonzo Mar 28, 2024
b40bd15
Merge branch '898_save_app_state3@main' of github.com:insightsenginee…
gogonzo Mar 28, 2024
6682d75
revert NEWS update
Mar 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,14 @@ Collate:
'modules.R'
'init.R'
'landing_popup_module.R'
'module_bookmark_manager.R'
'module_filter_manager.R'
'module_nested_tabs.R'
'module_snapshot_manager.R'
'module_tabs_with_filters.R'
'module_teal.R'
'module_teal_with_splash.R'
'module_wunder_bar.R'
'reporter_previewer_module.R'
'show_rcode_modal.R'
'tdata.R'
Expand Down
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# teal 0.15.2.9018

### Miscellaneous
* Filter mapping display is no longer coupled to the snapshot manager.

# teal 0.15.2

### Bug fixes
Expand Down
10 changes: 8 additions & 2 deletions R/dummy_functions.R
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@
#' @export
example_module <- function(label = "example teal module", datanames = "all") {
checkmate::assert_string(label)
module(
ans <- module(
label,
server = function(id, data) {
checkmate::assert_class(data(), "teal_data")
moduleServer(id, function(input, output, session) {
updateSelectInput(session, "dataname", choices = isolate(teal.data::datanames(data())))
updateSelectInput(
inputId = "dataname",
choices = isolate(teal.data::datanames(data())),
selected = restoreInput(session$ns("dataname"), NULL)
)
output$text <- renderPrint({
req(input$dataname)
data()[[input$dataname]]
Expand All @@ -44,4 +48,6 @@ example_module <- function(label = "example teal module", datanames = "all") {
},
datanames = datanames
)
attr(ans, "teal_bookmarkable") <- TRUE
ans
}
9 changes: 5 additions & 4 deletions R/init.R
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
#' string specifying the `shiny` module id in cases it is used as a `shiny` module
#' rather than a standalone `shiny` app. This is a legacy feature.
#'
#' @return Named list with server and UI functions.
#' @return Named list containing server and UI functions.
#'
#' @export
#'
Expand Down Expand Up @@ -164,8 +164,8 @@ init <- function(data,
stop("Only one `landing_popup_module` can be used.")
}

## `filter` - app_id attribute
attr(filter, "app_id") <- create_app_id(data, modules)
## `filter` - set app_id attribute unless present (when restoring bookmark)
if (is.null(attr(filter, "app_id", exact = TRUE))) attr(filter, "app_id") <- create_app_id(data, modules)

## `filter` - convert teal.slice::teal_slices to teal::teal_slices
filter <- as.teal_slices(as.list(filter))
Expand Down Expand Up @@ -221,8 +221,9 @@ init <- function(data,
# Note regarding case `id = character(0)`:
# rather than creating a submodule of this module, we directly modify
# the UI and server with `id = character(0)` and calling the server function directly
# Note: UI must be a function to support bookmarking.
res <- list(
ui = ui_teal_with_splash(id = id, data = data, title = title, header = header, footer = footer),
ui = function(request) ui_teal_with_splash(id = id, data = data, title = title, header = header, footer = footer),
server = function(input, output, session) {
if (!is.null(landing_module)) {
do.call(landing_module$server, c(list(id = "landing_module_shiny_id"), landing_module$server_args))
Expand Down
313 changes: 313 additions & 0 deletions R/module_bookmark_manager.R
chlebowa marked this conversation as resolved.
Show resolved Hide resolved
chlebowa marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
#' App state management.
#'
#' @description
#' `r lifecycle::badge("experimental")`
#'
#' Capture and restore the global (app) input state.
#'
#' @details
#' This module introduces bookmarks into `teal` apps: the `shiny` bookmarking mechanism becomes enabled
#' and server-side bookmarks can be created.
#'
#' The bookmark manager presents a button with the bookmark icon and is placed in the [`wunder_bar`].
#' When clicked, the button creates a bookmark and opens a modal which displays the bookmark URL.
#'
#' `teal` does not guarantee that all modules (`teal_module` objects) are bookmarkable.
#' Those that are, have a `teal_bookmarkable` attribute set to `TRUE`. If any modules are not bookmarkable,
#' the bookmark manager modal displays a warning and the bookmark button displays a flag.
#' In order to communicate that a external module is bookmarkable, the module developer
#' should set the `teal_bookmarkable` attribute to `TRUE`.
#'
#' @section Server logic:
#' A bookmark is a URL that contains the app address with a `/?_state_id_=<bookmark_dir>` suffix.
#' `<bookmark_dir>` is a directory created on the server, where the state of the application is saved.
#' Accessing the bookmark URL opens a new session of the app that starts in the previously saved state.
#'
#' @section Note:
#' To enable bookmarking use either:
#' - `shiny` app by using `shinyApp(..., enableBookmarking = "server")` (not supported in `shinytest2`)
#' - set `options(shiny.bookmarkStore = "server")` before running the app
#'
#'
#' @inheritParams module_wunder_bar
#'
#' @return Invisible `NULL`.
#'
#' @aliases bookmark bookmark_manager bookmark_manager_module
#'
#' @name module_bookmark_manager
#' @keywords internal
#'
bookmark_manager_ui <- function(id) {
ns <- NS(id)
uiOutput(ns("bookmark_button"), inline = TRUE)
}

#' @rdname module_bookmark_manager
#' @keywords internal
#'
bookmark_manager_srv <- function(id, modules) {
checkmate::assert_character(id)
checkmate::assert_class(modules, "teal_modules")
moduleServer(id, function(input, output, session) {
logger::log_trace("bookmark_manager_srv initializing")
ns <- session$ns
bookmark_option <- getShinyOption("bookmarkStore")
if (is.null(bookmark_option) && identical(getOption("shiny.bookmarkStore"), "server")) {
bookmark_option <- getOption("shiny.bookmarkStore")
# option alone doesn't activate bookmarking - we need to set shinyOptions
shinyOptions(bookmarkStore = bookmark_option)
}

is_unbookmarkable <- unlist(rapply2(
modules_bookmarkable(modules),
Negate(isTRUE)
))

# Render bookmark warnings count
output$bookmark_button <- renderUI({
if (!all(is_unbookmarkable) && identical(bookmark_option, "server")) {
tags$button(
id = ns("do_bookmark"),
class = "btn action-button wunder_bar_button bookmark_manager_button",
title = "Add bookmark",
tags$span(
suppressMessages(icon("solid fa-bookmark")),
if (any(is_unbookmarkable)) {
tags$span(
sum(is_unbookmarkable),
class = "badge-warning badge-count text-white bg-danger"
)
}
)
)
}
})

# Set up bookmarking callbacks ----
# Register bookmark exclusions: do_bookmark button to avoid re-bookmarking
setBookmarkExclude(c("do_bookmark"))
# This bookmark can only be used on the app session.
app_session <- .subset2(shiny::getDefaultReactiveDomain(), "parent")
app_session$onBookmarked(function(url) {
logger::log_trace("bookmark_manager_srv@onBookmarked: bookmark button clicked, registering bookmark")
modal_content <- if (bookmark_option != "server") {
msg <- sprintf(
"Bookmarking has been set to \"%s\".\n%s\n%s",
bookmark_option,
"Only server-side bookmarking is supported.",
"Please contact your app developer."
)
tags$div(
tags$p(msg, class = "text-warning")
)
} else {
tags$div(
tags$span(
tags$pre(url)
),
if (any(is_unbookmarkable)) {
bkmb_summary <- rapply2(
modules_bookmarkable(modules),
function(x) {
if (isTRUE(x)) {
"\u2705" # check mark
} else if (isFALSE(x)) {
"\u274C" # cross mark
} else {
"\u2753" # question mark
}
}
)
tags$div(
tags$p(
icon("fas fa-exclamation-triangle"),
"Some modules will not be restored when using this bookmark.",
tags$br(),
"Check the list below to see which modules are not bookmarkable.",
class = "text-warning"
),
tags$pre(yaml::as.yaml(bkmb_summary))
)
}
)
}

showModal(
modalDialog(
id = ns("bookmark_modal"),
title = "Bookmarked teal app url",
modal_content,
easyClose = TRUE
)
)
})

# manually trigger bookmarking because of the problems reported on windows with bookmarkButton in teal
observeEvent(input$do_bookmark, {
logger::log_trace("bookmark_manager_srv@1 do_bookmark module clicked.")
session$doBookmark()
})

invisible(NULL)
})
}

# utilities ----

#' Restore value from bookmark.
#'
#' Get value from bookmark or return default.
#'
#' Bookmarks can store not only inputs but also arbitrary values.
#' These values are stored by `onBookmark` callbacks and restored by `onBookmarked` callbacks,
#' and they are placed in the `values` environment in the `session$restoreContext` field.
#' Using `teal_data_module` makes it impossible to run the callbacks
#' because the app becomes ready before modules execute and callbacks are registered.
#' In those cases the stored values can still be recovered from the `session` object directly.
#'
#' Note that variable names in the `values` environment are prefixed with module name space names,
#' therefore, when using this function in modules, `value` must be run through the name space function.
#'
#' @param value (`character(1)`) name of value to restore
#' @param default fallback value
#'
#' @return
#' In an application restored from a server-side bookmark,
#' the variable specified by `value` from the `values` environment.
#' Otherwise `default`.
#'
#' @keywords internal
#'
restoreValue <- function(value, default) { # nolint: object_name.
checkmate::assert_character("value")
session_default <- shiny::getDefaultReactiveDomain()
session_parent <- .subset2(session_default, "parent")
session <- if (is.null(session_parent)) session_default else session_parent

if (isTRUE(session$restoreContext$active) && exists(value, session$restoreContext$values, inherits = FALSE)) {
session$restoreContext$values[[value]]
} else {
default
}
}

#' Compare bookmarks.
#'
#' Test if two bookmarks store identical state.
#'
#' `input` environments are compared one variable at a time and if not identical,
#' values in both bookmarks are reported. States of `datatable`s are stripped
#' of the `time` element before comparing because the time stamp is always different.
#' The contents themselves are not printed as they are large and the contents are not informative.
#' Elements present in one bookmark and absent in the other are also reported.
#' Differences are printed as messages.
#'
#' `values` environments are compared with `all.equal`.
#'
#' @section How to use:
#' Open an application, change relevant inputs (typically, all of them), and create a bookmark.
#' Then open that bookmark and immediately create a bookmark of that.
#' If restoring bookmarks occurred properly, the two bookmarks should store the same state.
#'
#'
#' @param book1,book2 bookmark directories stored in `shiny_bookmarks/`;
#' default to the two most recently modified directories
#'
#' @return
#' Invisible `NULL` if bookmarks are identical or if there are no bookmarks to test.
#' `FALSE` if inconsistencies are detected.
#'
#' @keywords internal
#'
bookmarks_identical <- function(book1, book2) {
if (!dir.exists("shiny_bookmarks")) {
message("no bookmark directory")
return(invisible(NULL))
}

ans <- TRUE

if (missing(book1) && missing(book2)) {
dirs <- list.dirs("shiny_bookmarks", recursive = FALSE)
bookmarks_sorted <- basename(rev(dirs[order(file.mtime(dirs))]))
if (length(bookmarks_sorted) < 2L) {
message("no bookmarks to compare")
return(invisible(NULL))
}
book1 <- bookmarks_sorted[2L]
book2 <- bookmarks_sorted[1L]
} else {
if (!dir.exists(file.path("shiny_bookmarks", book1))) stop(book1, " not found")
if (!dir.exists(file.path("shiny_bookmarks", book2))) stop(book2, " not found")
}

book1_input <- readRDS(file.path("shiny_bookmarks", book1, "input.rds"))
book2_input <- readRDS(file.path("shiny_bookmarks", book2, "input.rds"))

elements_common <- intersect(names(book1_input), names(book2_input))
dt_states <- grepl("_state$", elements_common)
if (any(dt_states)) {
for (el in elements_common[dt_states]) {
book1_input[[el]][["time"]] <- NULL
book2_input[[el]][["time"]] <- NULL
}
}

identicals <- mapply(identical, book1_input[elements_common], book2_input[elements_common])
non_identicals <- names(identicals[!identicals])
compares <- sprintf("$ %s:\t%s --- %s", non_identicals, book1_input[non_identicals], book2_input[non_identicals])
if (length(compares) != 0L) {
message("common elements not identical: \n", paste(compares, collapse = "\n"))
ans <- FALSE
}

elements_boook1 <- setdiff(names(book1_input), names(book2_input))
if (length(elements_boook1) != 0L) {
dt_states <- grepl("_state$", elements_boook1)
if (any(dt_states)) {
for (el in elements_boook1[dt_states]) {
if (is.list(book1_input[[el]])) book1_input[[el]] <- "--- data table state ---"
}
}
excess1 <- sprintf("$ %s:\t%s", elements_boook1, book1_input[elements_boook1])
message("elements only in book1: \n", paste(excess1, collapse = "\n"))
ans <- FALSE
}

elements_boook2 <- setdiff(names(book2_input), names(book1_input))
if (length(elements_boook2) != 0L) {
dt_states <- grepl("_state$", elements_boook1)
if (any(dt_states)) {
for (el in elements_boook1[dt_states]) {
if (is.list(book2_input[[el]])) book2_input[[el]] <- "--- data table state ---"
}
}
excess2 <- sprintf("$ %s:\t%s", elements_boook2, book2_input[elements_boook2])
message("elements only in book2: \n", paste(excess2, collapse = "\n"))
ans <- FALSE
}

book1_values <- readRDS(file.path("shiny_bookmarks", book1, "values.rds"))
book2_values <- readRDS(file.path("shiny_bookmarks", book2, "values.rds"))

if (!isTRUE(all.equal(book1_values, book2_values))) {
message("different values detected")
message("choices for numeric filters MAY be different, see RangeFilterState$set_choices")
ans <- FALSE
}

if (ans) message("perfect!")
invisible(NULL)
}


# Replacement for [base::rapply] which doesn't handle NULL values - skips the evaluation
# of the function and returns NULL for given element.
rapply2 <- function(x, f) {
if (inherits(x, "list")) {
lapply(x, rapply2, f = f)
} else {
f(x)
}
}
Loading
Loading