It is possible to restore a session, locally, in a Shiny app if the inputs have been previously written in a RDS file?
Asked Answered
W

2

5

I am developing a shiny app to be used locally. I am trying to develop a system for the user to be able to restore a former session.

For that, I took the code from this entrance: Saving state of Shiny app to be restored later , and it did work, however I wanted to be able to restore the inputs within a different session, so that I added a fileInput (Restore Session) and a downloadButton (Save Session) to the code, but unfortunately I could not make it work.

My code is as follows:

library(shiny)  

ui <- fluidPage(
  textInput("control_label",
            "This controls some of the labels:",
            "LABEL TEXT"),
  numericInput("inNumber", "Number input:", min = 1, max = 20, value = 5, step = 0.5),
  radioButtons("inRadio", "Radio buttons:",
               c("label 1" = "option1",
                 "label 2" = "option2",
                 "label 3" = "option3")),
  fileInput("load_inputs", "Restore Session", multiple = FALSE),
  downloadButton("save_inputs", 'Save Session')
)

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

  # SAVE SESSION
  output$save_inputs <- downloadHandler(
    filename = function() {
      paste("session", ".RDS", sep = "")
    },
    content = function(file) {
      saveRDS( reactiveValuesToList(input), file)
    })

  # LOAD SESSION
  load_sesion <- reactive({
    req(input$load_inputs)
    load_session <- readRDS( input$load_inputs$datapath )
  })

  observeEvent(input$load_inputs,{       
    if(is.null(input$load_inputs)) {return(NULL)}

    savedInputs <- load_sesion()
    inputIDs      <- names(savedInputs) 
    inputvalues   <- unlist(savedInputs) 

    for (i in 1:length(inputvalues)) { 
      session$sendInputMessage(inputIDs[i], list(value=inputvalues[[i]]) )
    }
  })}

shinyApp(ui, server)

With this code I can save the inputs of the session and I can read them in the following session, however I am not able to use those values stored on the RDS as inputs in another session.

Thanks a lot,

Rachael

Whilom answered 30/7, 2019 at 8:43 Comment(6)
Don't reinvent the wheel. You are better off using bookmarks.Nomenclature
Can I use bookmarks locally?Whilom
Yes, you can use them locally.Nomenclature
Is it possible to restore the session taking advantage of the files saved on shiny_bookmarks rather than copying and pasting the link? It is my first time using this.. Thanks a lot!Whilom
When using saved-to-server bookmarks you can readRDS("...myAppPath/shiny_bookmarks/cad39269e2348a09/input.rds") the input list - even though restoring via URL for sure is more convenient. Here is a related answer I gave.Nomenclature
I think this can work.. Thanks a lot!!Whilom
N
6

As suggested in my above comments the following app uses shiny's built-in capabilities to create bookmarks instead of using a custom function to save the current state of the inputs.

After the download button is clicked a bookmark is stored on the server side, renamed and copied to the downloadHandler.

If the user uploads a bookmark file, the needed path is created based on the filename and the user gets redirected to the earlier session. (Also see the commented out alternative, which requires the user to actively switch sessions).

Of course you could implement a modal to have the user input a name for the session to avoid using the rather cryptic bookmark hash as the filename.

Edit: Implemented a modal to let the user provide a custom session name (limited to alphanumeric characters)

library(shiny)
library(shinyjs)
library(utils)
library(tools)
library(stringi)

ui <- function(request) {
    fluidPage(
        useShinyjs(),
        textInput("control_label", "This controls some of the labels:", "LABEL TEXT"),
        numericInput("inNumber", "Number input:", min = 1, max = 20, value = 5, step = 0.5 ),
        radioButtons("inRadio", "Radio buttons:", c("label 1" = "option1", "label 2" = "option2", "label 3" = "option3")),
        fileInput("restore_bookmark", "Restore Session", multiple = FALSE, accept = ".rds"),
        actionButton("save_inputs", 'Save Session', icon = icon("download"))
    )
}

server <-  function(input, output, session) {
    latestBookmarkURL <- reactiveVal()
    
    onBookmarked(
        fun = function(url) {
            latestBookmarkURL(parseQueryString(url))
        }
    )
    
    onRestored(function(state) {
        showNotification(paste("Restored session:", basename(state$dir)), duration = 10, type = "message")
    })
    
    observeEvent(input$save_inputs, {
        showModal(modalDialog(
            title = "Session Name",
            textInput("session_name", "Please enter a session name (optional):"),
            footer = tagList(
                modalButton("Cancel"),
                downloadButton("download_inputs", "OK")
            )
        ))
    }, ignoreInit = TRUE)
    
    # SAVE SESSION
    output$download_inputs <- downloadHandler(
        filename = function() {
            removeModal()
            session$doBookmark()
            if (input$session_name != "") {
                
                tmp_session_name <- sub("\\.rds$", "", input$session_name)
                
                # "Error: Invalid state id" when using special characters - removing them:
                tmp_session_name <- stri_replace_all(tmp_session_name, "", regex = "[^[:alnum:]]")
                # TODO: check if a valid filename is provided (e.g. via library(shinyvalidate)) for better user feedback
                
                tmp_session_name <- paste0(tmp_session_name, ".rds")
                
            } else {
                paste(req(latestBookmarkURL()), "rds", sep = ".")
            }
        },
        content = function(file) {
            file.copy(from = file.path(
                ".",
                "shiny_bookmarks",
                req(latestBookmarkURL()),
                "input.rds"
            ),
            to = file)
        }
    )
    
    # LOAD SESSION
    observeEvent(input$restore_bookmark, {
        
        sessionName <- file_path_sans_ext(input$restore_bookmark$name)
        targetPath <- file.path(".", "shiny_bookmarks", sessionName, "input.rds")
        
        if (!dir.exists(dirname(targetPath))) {
            dir.create(dirname(targetPath), recursive = TRUE)
        }
        
        file.copy(
            from = input$restore_bookmark$datapath,
            to = targetPath,
            overwrite = TRUE
        )
        
        restoreURL <- paste0(session$clientData$url_protocol, "//", session$clientData$url_hostname, ":", session$clientData$url_port, session$clientData$url_pathname, "?_state_id_=", sessionName)
        
        # redirect user to restoreURL
        runjs(sprintf("window.location = '%s';", restoreURL))
        
        # showModal instead of redirecting the user
        # showModal(modalDialog(
        #     title = "Restore Session",
        #     "The session data was uploaded to the server. Please visit:",
        #     tags$a(restoreURL),
        #     "to restore the session"
        # ))
    })
    
}

shinyApp(ui, server, enableBookmarking = "server")
Nomenclature answered 5/7, 2021 at 9:8 Comment(3)
This looks great! Can you please point to resources on how to let users save the rds file with a custom name?Pernod
This looks fantastic! I love the shiny modal to save with a custom name. I don't know if this is too specific to ask. But I was wondering if it would be possible to not let shiny save the bookmarked rds files into the default shiny_bookmarks folder. My concern is that I don't want users to deal with two rds files stored in their machine. I was hoping that would be the purpose of the saved rds file with the custom name so users can choose the name and location of the rds file.Pernod
There is an open issue on GitHub regarding this. A workaround is mentioned here. However, as long as there is no official support to change the bookmark location I'd recommend using the default folder. As an alternative you could create an additional table for the user providing more information regardig the bookmarks, as done here.Nomenclature
C
0

Just wanted to add a note here because I spent awhile figuring out how to make this work with saved values. My version is very much derived from @ismirsehregal. I also created bookmarking modules that might be helpful to others. This was needed because shinyFiles inputs caused an error and needed to be excluded from bookmarks so I saved the value in a reactive and then saved it in onBookmark. This was the error when they were not excluded: Error in writeImpl: Text to be written must be a length-one character vector


library(shiny)
library(shinyFiles)

# source these functions
#Saving #=======================================================================
save_bookmark_ui <- function(id){
  actionButton(NS(id, "start_save"), "Save")
}

save_bookmark_server <- function(id, latestBookmarkURL, volumes){
  moduleServer(id, function(input, output, session) {
    
    shinyDirChoose(input, "save_dir", root = volumes)
    
    save_dir_pth <- reactive(parseDirPath(volumes, input$save_dir))
    
    onRestored(function(state) {
      showNotification(paste("Restored session:", basename(state$dir)),
                       duration = 10, type = "message")
    })
    
    setBookmarkExclude(c("save_dir", "start_save", "save_action", "new_dir_name"))
    
    observeEvent(input$start_save, {
      showModal(
        modalDialog(
          p("The app session is saved using two files, input.rds and values.rds",
            "You will provide a location and name for a new folder that will",
            " be created to store these files. Make sure you choose a name",
            "and location that will be easy to find when you want to load the ",
            "saved inputs."),
          strong("Choose location to save progess"),
          br(),
          shinyDirButton(NS(id, "save_dir"), "Location to create folder",
                         "Location to create folder"),
          br(),
          textInput(NS(id, "new_dir_name"),
                    "Choose a name for the new folder that will be created"),
          br(),
          footer = tagList(
            actionButton(NS(id, "save_action"), "Save"),
            modalButton("Cancel")
          ),
          title = "Save assessment progress"
        )
      )
    })
    
    iv <- shinyvalidate::InputValidator$new()
    iv$add_rule("new_dir_name", shinyvalidate::sv_optional())
    iv$add_rule("new_dir_name",
                shinyvalidate::sv_regex("[^[:alnum:]]",
                                        paste0("Please choose a name with only",
                                               " letters or numbers and no spaces"),
                                        invert = TRUE))
    
    observeEvent(input$save_action, {
      if (!iv$is_valid()) {
        iv$enable()
      } else {
        removeModal()
        session$doBookmark()
        if (input$new_dir_name != "") {
          
          # "Error: Invalid state id" when using special characters - removing them:
          tmp_session_name <- stringr::str_replace_all(input$new_dir_name,
                                                       "[^[:alnum:]]", "")
          
        } else {
          tmp_session_name <- paste(req(latestBookmarkURL))
        }
        # create the new directory in the chosen location
        new_dir <- fs::dir_create(fs::path(save_dir_pth(), tmp_session_name))
        
        message("Saving session")
        
        # move the files from where shiny saves them to where the user can find them
        fs::dir_copy(path = fs::path(".", "shiny_bookmarks", req(latestBookmarkURL)),
                     new_path = new_dir,
                     overwrite = TRUE)
      }
    }, ignoreInit = TRUE)
  })
}

# Load #=======================================================================
load_bookmark_ui <- function(id){
  actionButton(NS(id, "start_load"), "Load")
}

load_bookmark_server <- function(id, volumes){
  moduleServer(id, function(input, output, session){
    shinyDirChoose(input, "load_dir", root = volumes)
    load_dir_pth <- reactive(parseDirPath(volumes, input$load_dir))
    
    setBookmarkExclude(c("load_dir", "load_action", "start_load"))
    
    observeEvent(input$start_load, {
      showModal(
        modalDialog(
          strong("Select the folder where the app was saved"),
          br(),
          shinyDirButton(NS(id, "load_dir"), "Select Folder",
                         "Location of folder with previous state"),
          footer = tagList(
            actionButton(NS(id, "load_action"), "Load"),
            modalButton("Cancel")
          ),
          title = "Load existing assessment"
        )
      )
    })
    
    # LOAD SESSION
    observeEvent(input$load_action, {
      sessionName <- fs::path_file(load_dir_pth())
      
      targetPath <- file.path(".", "shiny_bookmarks", sessionName)
      
      if (!dir.exists(dirname(targetPath))) {
        dir.create(dirname(targetPath), recursive = TRUE)
      }
      
      # copy the bookmark to where shiny expects it to be
      fs::dir_copy(path = load_dir_pth(),
                   new_path = targetPath,
                   overwrite = TRUE)
      
      restoreURL <- paste0(session$clientData$url_protocol, "//",
                           session$clientData$url_hostname, ":",
                           session$clientData$url_port, "/?_state_id_=",
                           sessionName)
      
      removeModal()
      
      # redirect user to restoreURL
      shinyjs::runjs(sprintf("window.location = '%s';", restoreURL))
      
      # showModal instead of redirecting the user
      # showModal(modalDialog(
      #     title = "Restore Session",
      #     "The session data was uploaded to the server. Please visit:",
      #     tags$a(restoreURL, href = restoreURL),
      #     "to restore the session"
      # ))
      
    })
  })
}



# Input options
valueNms <- c("Greatly increase", "Increase", "Somewhat increase", "Neutral")
valueOpts <- c(3, 2, 1, 0)

ui <- function(request) {
  fluidPage(
    shinyjs::useShinyjs(),
    textInput("control_label", "This controls some of the labels:", "LABEL TEXT"),
    numericInput("inNumber", "Number input:", min = 1, max = 20, value = 5, step = 0.5 ),
    radioButtons("inRadio", "Radio buttons:",
                 c("label 1" = "option1", "label 2" = "option2", "label 3" = "option3")),
    checkboxGroupInput("inChk","Checkbox:", choiceNames = valueNms,
                       choiceValues = valueOpts),
    shinyFilesButton("range_poly_pth", "Choose file",
                     "Range polygon shapefile", multiple = FALSE),
    verbatimTextOutput("range_poly_pth_out", placeholder = TRUE),
    
    save_bookmark_ui("save"), 
    load_bookmark_ui("load"),

    tableOutput("table")
  )
}


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

  shinyFileChoose("range_poly_pth", root = volumes, input = input)
  

  file_pth <- reactive({
    if(is.integer(input$range_poly_pth)){
      if(!is.null(restored$yes)){
        return(file_pth_restore())
      }
      return(NULL)
    } else{
      return(parseFilePaths(volumes, input$range_poly_pth)$datapath)
    }
  })


  output$table <- renderTable({
    req(file_pth())
    read.csv(file_pth())
  })
  
  output$range_poly_pth_out <- renderText({
    file_pth()
  })
  
  
  setBookmarkExclude(c("range_poly_pth",
                       "range_poly_pth_out"))
  
  # this part is not allowed to be inside the module
  latestBookmarkURL <- reactiveVal()
  
  onBookmarked(
    fun = function(url) {
      latestBookmarkURL(parseQueryString(url))
      showNotification("Session saved",
                       duration = 10, type = "message")
    }
  )
  
  
  R.utils::withTimeout({
    volumes <- c(wd = getShinyOption("file_dir"),
                 Home = fs::path_home(),
                 getVolumes()())
  }, timeout = 10, onTimeout = "error")
  
  save_bookmark_server("save", latestBookmarkURL(), volumes)
  
  load_bookmark_server("load", volumes)
  
  # Need to explicitly save and restore reactive values.
  onBookmark(function(state) {
    val2 <- Sys.Date()
    state$values$date <- val2
    state$values$file <- file_pth()
  })
  
  restored <- reactiveValues()
  file_pth_restore <- reactiveVal()
  
  onRestore(fun = function(state){
    file_pth_restore(state$values$file)
    print(file_pth_restore())
    restored$yes <- TRUE
  })

}

shinyApp(ui, server, enableBookmarking = "server")
Calices answered 18/11, 2021 at 22:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.