-
-
Notifications
You must be signed in to change notification settings - Fork 50
898 save app state version 3 #1011
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
Merged
Merged
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
4278015
add state manager module
edfd6be
amend documentation
1b3b524
insert state manager into filter manager
1b149e8
rename one funciton
a7405d9
omit action buttons from grabs
6a0cc29
re-click until grab fully reset
130fbc8
encapsulate creating grabs
3b963fa
reorder file
634b5e8
amend documentation
d452326
remove dewclassing of grabbed values to keep dates and date times
e131b71
handle POSIXct in airDatePickerInput
7c21911
Merge e131b713094b85a6f73f85d82b97a5ff8e3bce87 into 54c683b92f845075a…
chlebowa 709c4ef
[skip actions] Restyle files
github-actions[bot] c2e0be4
[skip actions] Roxygen Man Pages Auto Update
dependabot-preview[bot] 735650c
remove storing initial input state as always empty
f854b9d
spelling
556c1bb
spelling
696375e
Merge branch 'main' into 898_save_app_state@main
chlebowa 3e40918
use native shiny bookmarking
0c22dcd
open bookmarks in new window
b9e3e37
minor update to defauts
958c8d7
Merge branch 'main' into 898_save_app_state2@main
edb94dc
fix logic operators
67f8be5
fix state manager server definition and call
5606703
add hook to teal to set shiny option for bookmarking
5b47506
fix bookmarking and restoring callbacks
a583e23
add missing arguments in state manager and add return value in snapsh…
47cc2a5
properly call public methods of session object
81f10fb
add more missing arguments to state manager module
5bb0125
clean up docs
9e9c9de
rebuild module
e2fea5d
remove grab_state function
ca24e29
add argument checks to state manager module
3ec250d
remove filter panel exclusion
c9bbb54
don't store initial app state
63eb738
clean up comemnts
c3d40cf
init returns ui as function
ac363d2
use state manager module always
f24dd8c
Merge branch 'main' into 898_save_app_state3@main
b02fee4
Merge branch 'main' into 898_save_app_state3@main
c0f854c
all managers return
159e337
isolate manager_manager_module
0eb331e
rename manager_manager where it is used
ef6c049
move snapshot_manager and state_manager to manager manager
d25397c
rename module
b8b7b5c
shorten line
712a5c3
remove superfluous utility function
1084353
rename module to bookmark_manager
b950e31
rename grab to bookmark
a3d2147
fix typo
2149306
separate utility function in filter manager
af3ebfc
move renaming of global filtered data list
8ba143a
rename flat filtered data list and tweak utility function
6a3689a
rename filtered_data_list to datasets
fe0afb0
rename filtered_data_flat to datasets_flat
e59ab91
improve documentation for flatten_datasets
2b08206
add logging to all manager modules
3b82671
update name of programmatically clicked button
4371359
fix erroneous log
5d62ff4
remove delayed module initiation when starting from bookmark
23d408d
bug fix: missing argument value
c4b669c
fix update in example module
6d07bd2
improve logs in bookmark manager
43752bf
add bookmark exclusions
cd6c6bc
add code comment
fc3872a
modify button titles
8181ca6
remove superfluous CSS
07fa52f
change CSS class names
d896983
adjust style of bnon-first children in manager_table_row
6fbf146
Merge branch 'main' into 898_save_app_state3@main
chlebowa ed34a94
update code comment
2495fb3
simplify 'when from bookmark' condition
81f9346
Merge branch '898_save_app_state3@main' of github.com:insightsenginee…
5b5a1e2
replace bookmarkButton with action Button
debf0f2
amend documentation
24966a6
amend NEWS
ed94c39
rearrange code
38c9598
[skip style] [skip vbump] Restyle files
github-actions[bot] fa33460
fix spelling
9ac1fbd
Merge branch 'main' into 898_save_app_state3@main
chlebowa 4dcc66c
improve flow control condition
882c1cf
amend unit tests
9db31ac
Merge branch '898_save_app_state3@main' of github.com:insightsenginee…
40d7cb9
assign return value in wunder_bar server
6b99ded
simplify unit test for snapshot manager
2b81ce2
add unit tests for wunder_bar module
47f93dd
Merge branch 'main' into 898_save_app_state3@main
chlebowa 7888356
Merge branch 'main' into 898_save_app_state3@main
chlebowa 68a7a0d
Merge branch 'main' into 898_save_app_state3@main
chlebowa a766110
remove delay on starting report previewer
40bed62
exclude all buttons (app-wide) from bookmark
7e3aed9
Merge branch 'main' into 898_save_app_state3@main
chlebowa 92391a5
simplify code
a83b42c
change condition for initiating reporte previewer module
e2836b0
Merge branch 'main' into 898_save_app_state3@main
9d7d7b9
restoreValue
gogonzo 557364c
remove browser
gogonzo be642f5
trigger
d36aac6
Merge branch 'main' into 898_save_app_state3@main
55bf1e0
add documentation for restoreValue
75ffb21
add function for comparing bookmarked states
4b94872
reorganize code
72bce64
amend documentation for restoreValue
c4e7556
move storing values to respective modules
c0d1e32
correct for namespace when restoring filter in module_teal
1b716d2
[skip roxygen] [skip vbump] Roxygen Man Pages Auto Update
dependabot-preview[bot] 02a7db4
modify restoreValue
320014e
update documentation for restoreValue
5dbf498
clean up bookmark exclusions in bookmark manager
02b6710
modify storing filter state on bookmark
9e0f213
add comment headers
5c1896e
add enforcement of server-side bookmarks
ae46faa
register bookmark exclusions in snapshot manager
3423345
[skip style] [skip vbump] Restyle files
github-actions[bot] 0e25d4c
[skip roxygen] [skip vbump] Roxygen Man Pages Auto Update
dependabot-preview[bot] e1df2fd
remove agruments in bookmark manager
b01a395
fix return value
5294f7c
exclude buttons from bookmark in wunder bar
56c55a0
amend documentation for snapshot and bookmark managers
8aac98c
extend message in bookmarks_identical
bf8b39b
teal_bookmarkable flags
d4a2b4d
only set app id on filter when missing
9edf512
Bookmarking info (#1184)
gogonzo 92890d5
remove test
gogonzo 30e8b28
trigger
33a5ba7
Merge branch 'main' into 898_save_app_state3@main
chlebowa a3659db
fix docs collate
gogonzo d3a0e09
fix docs
gogonzo 7b40974
[skip roxygen] [skip vbump] Roxygen Man Pages Auto Update
dependabot-preview[bot] 0aca94c
trigger
97c2efb
adding testing
gogonzo b40bd15
Merge branch '898_save_app_state3@main' of github.com:insightsenginee…
gogonzo 6682d75
revert NEWS update
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
chlebowa marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.