1

I have an example app allowing users to do something in the shiny app and then bookmark the state. I understand that the state files are stored in the shiny_bookmarks folder. Typically, I would copy the bookmarked server URL and restore the app. But I'm wondering if it would be possible to upload the rds file and restore the state of the bookmarked app.

The shiny_bookmarks folder creates folders with .rds files. And my goal is to restore the state of the app by loading the .rds files.

Ideally, I would like to bundle the rds and other files into a single rda file and then upload the rda file using fileInput. I think rda files would work better as they can save multiple objects.

For the example app below, I have the rda fileInput as a placeholder. I've been trying to restore the app using the files saved in the shiny_bookmarks folder but I'm not sure how to go about that as I couldn't find much documentation so far.

Screenshot: shiny_bookmarks folder

enter image description here

Screenshot: .rds files inside a bookmark folder

enter image description here

Example app:

library(shiny)

ui <- function(request){fluidPage(
  sidebarLayout(
    sidebarPanel(
      fileInput("data", "Choose CSV File", accept = ".csv"),
      checkboxInput("header", "Header", TRUE),
      fileInput("file1", "Choose RDA File", accept = ".rda"),
      bookmarkButton()
    ),
    mainPanel(
      tableOutput("data_head"),
      tableOutput("contents"),
      selectInput("select", "Select Variable", choices = NULL, selected = NULL),
      plotOutput("boxplot")
    )
  )
)
}

server <- function(input, output) {
  
 dataset <- reactive({
    file <- input$data
    ext <- tools::file_ext(file$datapath)
    
    req(file)
    validate(need(ext == "csv", "Please upload a csv file"))
    
    my_data  <- read.csv(file$datapath, header = input$header)
    
    my_data
  })
  
  output$data_head <- renderTable({
    head(dataset())
  })
  
  output$boxplot <- renderPlot({
    req(dataset())
    req(input$select)
     boxplot(dataset()[[input$select]], horizontal = TRUE)
  })
  
  observeEvent(input$data, {
   req(dataset())
    updateSelectInput(session = getDefaultReactiveDomain(), "select", label = "Select Variable", choices = c("", names(dataset())))
  })
  
  
  output$out <- renderText({
    if (input$caps)
      toupper(input$txt)
    else
      input$txt
  })
  
  output$contents <- renderTable({
    file <- input$file1
    ext <- tools::file_ext(file$datapath)
    
    req(file)
    validate(need(ext == "rds", "Please upload a RDA file"))
    
    readRDS(file$datapath)
  })
}

shinyApp(ui, server, enableBookmarking = "server")

Example csv data from base R:

write.csv("attitude", "attitude.csv")

App screenshot:

enter image description here

I found a potential workaround here in this post with this github repo, but they don't use bookmarks. Maybe their solution may be easier, but I'm struggling to integrate that with my example app.

writer_typer
  • 708
  • 7
  • 25
  • What is supposed to be happening when the user restores the app? – jpdugo17 Jun 20 '21 at 03:31
  • jpdugo17, I've edited my question to clarify what I'm trying to accomplish. Thanks for your help! – writer_typer Jun 20 '21 at 03:44
  • 1
    For future readers: [Here](https://stackoverflow.com/questions/57267644/it-is-possible-to-restore-a-session-locally-in-a-shiny-app-if-the-inputs-have/68253464#68253464) you can find a related answer. – ismirsehregal Jul 06 '21 at 08:32

1 Answers1

2

Edit: Found a proxy solution using updateQueryString function that updates the url location bar. Basically what i did was to add one selectInput inside the ui that displays all the bookmarks previously saved to let the user pick one to restore.

There are a few limitations for implementing restore with rds files. The first one is that input.rds (the file created by the bookmark) only displays the name and the value(s) of the input but not the type, so there's no way of telling if the value corresponds to an actionButton or textInput. Even if we implement a system for recognizing each type, some functions like updateActionButton does not have a value argument, so it's value will mostly never be correct.

library(shiny)
library(tidyverse)


get_last_bookmark <- function(){
  list.files(path = 'shiny_bookmarks/') %>% #path to every folder containing bookmarked data.
    map_df(., ~paste0('shiny_bookmarks/', .x) %>%
             file.info ) %>% 
    slice_max(atime) %>%
    rownames()
}

get_last_bookmark <- possibly(get_last_bookmark, otherwise = '') #avoid empty folder error 


ui <- function(request){fluidPage(
  sidebarLayout(
    sidebarPanel(
      fileInput("data", "Choose CSV File", accept = ".csv"),
      checkboxInput("header", "Header", TRUE),
      fileInput("file1", "Choose RDA File", accept = ".rda"),
      bookmarkButton(),
      verbatimTextOutput('last_dir'),
      br(),
      selectInput('select_state', 'Select Bookmark Folder To Restore', choices = list.files(path = 'shiny_bookmarks/'),selected = get_last_bookmark() %>% str_sub(17, -1)),
      actionButton('dorestore', 'Restore it!')
    ),
    mainPanel(
      tableOutput("data_head"),
      tableOutput("contents"),
      selectInput("select", "Select Variable", choices = NULL, selected = NULL),
      plotOutput("boxplot")
    )
  )
)
}

server <- function(input, output, session) {
  
  output$last_dir <- renderText({
    
    paste0('The last bookmark is stored in: ', get_last_bookmark())
    
  })
  
  
  
  
  onBookmark(function(state){
    
    output$last_dir <- renderText({
      paste0('The last bookmark is stored in: ', get_last_bookmark())
    })
    
    updateSelectInput(session = session, 'select_state', choices = list.files(path = 'shiny_bookmarks/'), selected = get_last_bookmark() %>% str_sub(17, -1) )
    
    
    state$values$input_value_to_restore <- input$select
  })
  
  onBookmarked(function(state){
    #avoid showing message window
  })
  
  observeEvent(input$dorestore, {
    updateQueryString(queryString = paste0('?_state_id_=', input$select_state), session = session)
    session$reload()
  })
  
  dataset <- reactive({
    file <- input$data
    ext <- tools::file_ext(file$datapath)
    
    req(file)
    validate(need(ext == "csv", "Please upload a csv file"))
    
    my_data  <- read.csv(file$datapath, header = input$header)
    
    my_data
  })
  
  output$data_head <- renderTable({
    head(dataset())
  })
  
  output$boxplot <- renderPlot({
    req(dataset())
    req(input$select)
    boxplot(dataset()[[input$select]], horizontal = TRUE)
  })
  
  observeEvent(input$data, { #this will cause input$select to reset when the app restores because of the way shiny restores the app. 
    req(dataset())
      updateSelectInput(session = getDefaultReactiveDomain(), "select", label = "Select Variable", choices = c("", names(dataset())))
  })
  
  
  #avoid the dynamic parts of the app to reset
  onRestored(function(state) {
    
    updateSelectInput(session, 'select', selected = state$values$input_value_to_restore)
    
  })
  
  output$out <- renderText({
    if (input$caps)
      toupper(input$txt)
    else
      input$txt
  })
  
  output$contents <- renderTable({
    file <- input$file1
    ext <- tools::file_ext(file$datapath)
    
    req(file)
    validate(need(ext == "rds", "Please upload a RDA file"))
    
    readRDS(file$datapath)
  })
}

shinyApp(ui, server, enableBookmarking = "server")

This app will read the last created folder with the bookmark data and print it's contents in a tab called "Rds From Last State"

library(shiny)
library(tidyverse)

ui <- function(request){fluidPage(
    sidebarLayout(
        sidebarPanel(
            fileInput("data", "Choose CSV File", accept = ".csv"),
            checkboxInput("header", "Header", TRUE),
            fileInput("file1", "Choose RDS File", accept = ".rds"),
            bookmarkButton()
        ),
        mainPanel(
            tabsetPanel(
                tabPanel('Tables',
                    tableOutput("data_head"),
                    tableOutput("contents"),
                    selectInput("select", "Select Variable", choices = NULL, selected = NULL),
                    plotOutput("boxplot")),
                tabPanel('Rds From Last State', 
                     verbatimTextOutput('bookmark_rds'))
        )
        )
    )
)
}

server <- function(input, output) {
    
    dataset <- reactive({
        file <- input$data
        ext <- tools::file_ext(file$datapath)
        
        req(file)
        validate(need(ext == "csv", "Please upload a csv file"))
        
        my_data  <- read.csv(file$datapath, header = input$header)
        
        my_data
    })
    
    output$data_head <- renderTable({
        head(dataset())
    })
    
    output$boxplot <- renderPlot({
        req(dataset())
        boxplot(dataset(), main = input$select, horizontal = TRUE)
    })
    
    observeEvent(input$data, {
        req(dataset())
        updateSelectInput(session = getDefaultReactiveDomain(), "select", label = "Select Variable", choices = c("", names(dataset())))
    })
    
    
    output$out <- renderText({
        if (input$caps)
            toupper(input$txt)
        else
            input$txt
    })
    
    output$contents <- renderTable({
        file <- input$file1
        ext <- tools::file_ext(file$datapath)
        
        req(file)
        validate(need(ext == "rds", "Please upload a RDS file"))
        
        readRDS(file$datapath)
    })
    
    
    rds_directory <- reactiveValues()
    
    
    
    onRestore(function(state) {
        #The last modified folder inside the bookmarks directory will contain the latest values to restore.
        last_bookmark_dir <- 
        list.files(path = 'shiny_bookmarks/') %>% #path to every folder containing bookmarked data.
            map_df(., ~paste0('shiny_bookmarks/', .x) %>%
                       file.info ) %>% 
            slice_max(atime) %>% rownames()
        
        print(paste('Last bookmark dir:', last_bookmark_dir))
        
        print(list.files(last_bookmark_dir))
        
        rds_directory$rds <- last_bookmark_dir %>%
                                list.files %>%
                                str_subset('\\.rds') %>% #subset all the .rds files
                                {paste0(last_bookmark_dir, '/', .)} %>% map(~ readRDS(.x))
        
        print(rds_directory$rds)
    })
    
    output$bookmark_rds <- renderPrint({
        req(rds_directory$rds) #If there's no bookmark this chunk won't execute
        
        
            map(rds_directory$rds, ~ .x) 
        
    })
    
    
}

shinyApp(ui, server, enableBookmarking = "server")
jpdugo17
  • 6,816
  • 2
  • 11
  • 23
  • Thanks for your help! This is great. But I'm trying to restore the app by loading the .rds files in the shiny_bookmark directory. I've clarified my question with a couple of screenshots. – writer_typer Jun 20 '21 at 04:44
  • @TyperWriter I updated the answer, hope that is closer to what you asked. – jpdugo17 Jun 20 '21 at 06:38
  • @TyperWriter after restoring the app, `rds_directory$rds` object will contain the rds files from the bookmark directory. A file named 0.rds will correspond to fileInput if a file is uploaded when bookmark button is pressed. Also, upon restore R console will print the folder were the bookmark so you can use that when browsing through fileInput. – jpdugo17 Jun 20 '21 at 20:15
  • 1
    Do you mean like pressing an actionButton inside the ui to restore everything? In that case i think it is possible but requires to manually pass the data to each input. – jpdugo17 Jun 21 '21 at 02:00
  • I would like to upload the bookmarks into the fileInput and restore everything. – writer_typer Jun 21 '21 at 02:07
  • 1
    Like uploading input.rds with fileInput and using its values to update all the inputs? – jpdugo17 Jun 22 '21 at 03:01
  • Would it be possible to upload a file instead of referencing the directory? – writer_typer Jun 29 '21 at 03:19