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 #907

Closed
wants to merge 20 commits into from
Closed

898 save app state #907

wants to merge 20 commits into from

Conversation

chlebowa
Copy link
Contributor

@chlebowa chlebowa commented Aug 24, 2023

Closes #898

Prototype for saving and restoring app state, i.e. selection state of all inputs in app. When complete, can be easily combined with saving filter state.

The problem

In essence, everything comes down to two actions: 1) find state of all inputs in the app, and 2) set state of all inputs in the app. Once we have the state in hand, saving and retrieving it is trivial.

Ad 1) One simply has to go through the input object. Modules slightly complicate things as things are namespaced and separated but that is solved by finding the master session object (the one for the app, not for individual modules) and working from there.
Ad 2) This is not hard in principle as one can set the state of any input with session$sendInputMessage, using the master session object and namespaced input ids. However...

The issue

Inputs in data_extract_specs are hierarchical, meaning given a dataset, one first selects a column and then a value from that column. The value input is created in response to selecting a column value. Now, when one wants to restore input states, one will click a button to trigger an observer and within that observer session$sendInputMessage will be called. This will queue messages that will update all inputs as required but those messages will only be applied after the observer closes. Then, when the higher order input is updated, the lower order input will be reset to default.

See the example app below.

So, the nature of the reactive cycle kills the reset process. I tried forcing flushes, I tried calling the update function recursively, at the moment I cannot solve this.

The only solution I was able to find is to click the button again and keep clicking it until all inputs are identical to the saved state. This usually takes 2-3 cycles but may potentially take more if the input hierarchy is more than two deep. I consider this a workaround rather than a mature solution. Repeated clicks translate to multiple updates of plots, which deteriorates user experience. I am aware.

You can try the solution by running a teal app (on this branch). Repeated clicks in the module are done programmatically but you have to click manually in the example app.

Notes

I packaged it into a module styled after (filter) snapshot manager to allow testing in teal apps. This need not be the final solution.

Saving and loading may still have some bugs (probably does), I don't want to get into minutiae before the major issues are solved.

Simple testing app

library(shiny)
rm(list = ls())
devtools::load_all("../teal")

nameUI <- function(id) {
  ns <- NS(id)
  tagList(
    actionButton(ns("inside"), "inside"), # for browser in module
    uiOutput(ns("inputs1")),
    uiOutput(ns("inputs2")),
    uiOutput(ns("inputs3")),
    plotOutput(ns("plot")),
    verbatimTextOutput(ns("value")),      # past and current values of value
    NULL
  )
}

nameServer <- function(id) {
  moduleServer(id, function(input, output, session) {

    ns <- session$ns

    observeEvent(input$inside, {
      browser()
    })

    value_history <- reactiveVal()
    observeEvent(input[["value"]], {
      add <- if (is.null(input[["value"]])) "NULL" else input[["value"]]
      update <- c(value_history(), add)
      value_history(update)
    }, ignoreNULL = FALSE)
    output[["value"]] <- renderPrint({
      value_history()
    })


    output[["inputs1"]] <- renderUI({
      choices <- names(Filter(is.factor, warpbreaks))
      selectInput(ns("column"), "column", choices = choices, selected = choices[1])
    })
    output[["inputs2"]] <- renderUI({
      col <- req(input[["column"]])
      choices <- levels(warpbreaks[[col]])
      selectInput(ns("value"), "value", choices = c("please select" = "", choices), selected = NULL)
    })
    output[["inputs3"]] <- renderUI({
      choices <- letters[1:4]
      radioButtons(ns("letter"), "letter", choices = choices, selected = choices[1], inline = TRUE)
    })


    output[["plot"]] <- renderPlot({
      col <- req(input[["column"]])
      val <- req(input[["value"]])
      data <- warpbreaks[warpbreaks[[col]] == val, "breaks"]
      tryCatch(
        hist(data, breaks = 10, las = 1),
        warning = function(w) req(FALSE)
      )
    })

  })
}

ui <- fluidPage(
  actionButton("store", "store"),        # capture inputs
  actionButton("restore", "restore"),    # set inputs
  nameUI("module1"),                     # module
  verbatimTextOutput("grab"),            # current grab
  NULL
)

server <- function(input, output, session) {

  nameServer("module1")

  grab_history <- reactiveVal({
    list(
      "Initial input state" = isolate(app_state_grab())
    )
  })

  observeEvent(input$store, {
    grab_update <- c(grab_history(), list(app_state_grab()))
    grab_history(grab_update)
  })
  observeEvent(input$restore, {
    grab <- rev(grab_history())[[1L]]
    app_state_restore(grab)
  })

  output[["grab"]] <- renderPrint({
    rev(grab_history())[[1L]]
  })

}

shinyApp(ui, server)

@chlebowa chlebowa added the core label Aug 24, 2023
@m7pr m7pr self-requested a review August 25, 2023 09:46
@m7pr
Copy link
Contributor

m7pr commented Aug 25, 2023

Hey, well done with shinyjs::click() idea. I was able to run the app and the described behavior is provided. I skimmed through the code and it's clean and readable. Really solid, as you always keep up to high standards. I think you are good to go exploring how would you store filters, even though we know limitations of the click bottlenecks.

@chlebowa
Copy link
Contributor Author

Thanks @m7pr 🙂

I have done some more research into how one can interfere with the reactive cycle. I wanted to manually manipulate invalidation flags but I didn't find a way. I tried dynamically injecting JS that would ignore some events on some elements, the assumption was that I could kill part of the reactivity for the duration of the update, but in the end it has to be brought back up in another flush cycle. I give up. The cyclical clicks are the bes I can do.

To mitigate the side effect I tried to add a loader that will obscure the plot until all updates are complete but it keeps popping up anyway.

I will spend a little more time to make sure saving and loading works properly and then open the PR for review.

@chlebowa
Copy link
Contributor Author

chlebowa commented Aug 28, 2023

Update: saving and loading works ok in tests but I have not incorporated it into the module fully yet, meaning one can save but not load (much like filter snapshots). We should consider whether we want to save inputs and filter state separately or together, or let the user decide. The latter two would require rebuilding the modules so I would like to discuss this issue before getting into it.

@chlebowa chlebowa marked this pull request as ready for review August 28, 2023 15:26
@github-actions
Copy link
Contributor

github-actions bot commented Aug 28, 2023

badge

Code Coverage Summary

Filename                          Stmts    Miss  Cover    Missing
------------------------------  -------  ------  -------  -------------------------------------------------------------------------------------------------------------------------------
R/dummy_functions.R                  97      88  9.28%    9-71, 93-106, 109-116, 131-146
R/get_rcode_utils.R                  32       1  96.88%   52
R/include_css_js.R                   24       0  100.00%
R/init.R                             80      31  61.25%   114-121, 145, 164-185, 215-217, 219-220
R/landing_popup_module.R             25      25  0.00%    61-87
R/module_filter_manager.R           109      37  66.06%   49-55, 62-70, 79-85, 231, 236-249
R/module_nested_tabs.R              149      58  61.07%   64-137, 153, 205, 227, 253
R/module_snapshot_manager.R         209     157  24.88%   87-99, 127-136, 140-152, 154-161, 168-182, 186-188, 190-195, 198-208, 211-227, 236-251, 265-288, 291-302, 305-311, 325, 346-369
R/module_state_manager.R            190     160  15.79%   22-33, 54-63, 67-79, 81-87, 93-98, 100, 112-151, 163, 186-365
R/module_tabs_with_filters.R         73      33  54.79%   60-95, 127, 140
R/module_teal_with_splash.R          94       4  95.74%   67, 88, 153-154
R/module_teal.R                     141      32  77.30%   68, 71, 158-159, 165, 196, 204-205, 227-259
R/modules_debugging.R                19      19  0.00%    25-45
R/modules.R                         155      26  83.23%   119, 132, 224-227, 241-246, 257-261, 391-434
R/reporter_previewer_module.R        18       2  88.89%   26, 30
R/show_rcode_modal.R                 20      20  0.00%    16-37
R/tdata.R                            53       1  98.11%   153
R/teal_data_module-eval_code.R       27       0  100.00%
R/teal_data_module-within.R           7       0  100.00%
R/teal_data_module.R                  6       0  100.00%
R/teal_reporter.R                    60       5  91.67%   65, 116-117, 120, 137
R/teal_slices-store.R                29       0  100.00%
R/teal_slices.R                      59      12  79.66%   135-148
R/utils.R                           111      27  75.68%   112-139
R/validate_inputs.R                  32       0  100.00%
R/validations.R                      58      37  36.21%   109-371
R/zzz.R                              11       7  36.36%   3-14
TOTAL                              1888     782  58.58%

Diff against main

Filename                     Stmts    Miss  Cover
-------------------------  -------  ------  -------
R/module_filter_manager.R       +2      +1  -0.30%
R/module_state_manager.R      +190    +160  +15.79%
TOTAL                         +192    +161  -4.80%

Results for commit: c2e1d66

Minimum allowed coverage is 80%

♻️ This comment has been updated with latest results

@github-actions
Copy link
Contributor

github-actions bot commented Aug 28, 2023

Unit Tests Summary

    1 files    19 suites   11s ⏱️
202 tests 200 ✔️ 2 💤 0
402 runs  399 ✔️ 3 💤 0

Results for commit c2e1d66.

♻️ This comment has been updated with latest results.

Aleksander Chlebowski added 2 commits August 28, 2023 18:40
Copy link
Contributor

@gogonzo gogonzo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we are trying redo what shiny offers with bookmarking. There is a nice option to save states on the server and reuse them with url like https://gallery.shinyapps.io/bookmark-saved/?_state_id_=<STATE ID>. Maybe we can store there selected inputs plus reactiveValues (eg. slices_global).

In the "ancient" teal we had bookmarking which handled filter-states

datasets$restore_state_from_bookmark(saved_datasets_state)

Maybe we can address it in a similar way?

@chlebowa chlebowa marked this pull request as draft December 18, 2023 15:06
@gogonzo gogonzo closed this Mar 28, 2024
@gogonzo gogonzo deleted the 898_save_app_state@main branch March 28, 2024 15:37
@github-actions github-actions bot locked and limited conversation to collaborators Mar 28, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Research] Ability to save and restore the state of teal app
3 participants