4

Below is a sample code which takes two inputs: 1) input file and 2) input number of rows. Upon clicking the "Analyze" button the output from the server command return to the "Table" in "Results" tabset. This is a simple example where the command will be executed quickly and switches to the "Results" tabsetpanel.

The below withProgress code only shows the progress bar for the set time and disappears and then the actual code is executed. I would like to show a "Status Message" or "Progress Bar" when the "Analyze" is hit and show as long as the command is run. As long as the progress bar is running the current user (other users can use the app) cannot perform any action from the side bar. Because in the real app, sidebar has more menuItems which does similar tasks like this and each task has a Analyze button. If the user is allowed to browse to sidebar pages and hit Analyze then the app will have overload of performing multiple tasks. Ideally the progress bar functionality should we used with multiple actionButtons.

I read the blogs about async but unable to put right code in the right place. any help is appreciated with a bounty!!

library(shiny)
library(shinydashboard)
sidebar <- dashboardSidebar(width = 200,
                    sidebarMenu(id = "tabs",
                                menuItem(
                                  "File", tabName = "tab1", icon = icon("fas fa-file")
                                )))
body <- tabItem(tabName = "tab1",
        h2("Input File"),
        fluidRow(
          tabPanel(
            "Upload file",
            value = "upload_file",
            fileInput(
              inputId = "uploadFile",
              label = "Upload Input file",
              multiple = FALSE,
              accept = c(".txt")
            ),
            checkboxInput('header', label = 'Header', TRUE)
          ),
          box(
            title = "Filter X rows",
            width = 7,
            status = "info",
            tabsetPanel(
              id = "input_tab",
              tabPanel(
                "Parameters",
                numericInput(
                  "nrows",
                  label = "Entire number of rows",
                  value = 5,
                  max = 10
                ),
                actionButton("run", "Analyze")
              ),
              tabPanel(
                "Results",
                value = "results",
                navbarPage(NULL,
                           tabPanel(
                             "Table", DT::dataTableOutput("res_table"), 
icon = icon("table")
                           )),
                downloadButton("downList", "Download")
              )
            )
          )
        ))
ui <-
shinyUI(dashboardPage(
dashboardHeader(title = "TestApp", titleWidth = 150),
sidebar,dashboardBody(tabItems(body))
))


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

observeEvent(input$run, {
withProgress(session, min = 1, max = 15, {
  setProgress(message = 'Analysis in progress',
              detail = 'This may take a while...')
  for (i in 1:15) {
    setProgress(value = i)
    Sys.sleep(0.5)
  }
})
system(paste(
  "cat",
  input$uploadFile$datapath,
  "|",
  paste0("head -", input$nrows) ,
  ">",
  "out.txt"
),
intern = TRUE)
head_rows <- read.delim("out.txt")
file_rows(head_rows)
  })

observeEvent(file_rows(), {
updateTabsetPanel(session, "input_tab", "results")
output$res_table <-
DT::renderDataTable(DT::datatable(
file_rows(),
options = list(
  searching = TRUE,
  pageLength = 10,
  rownames(NULL),
  scrollX = T
  )
  ))
 })

output$downList <- downloadHandler(
filename = function() {
paste0("output", ".txt")
}, content = function(file) {
write.table(file_rows(), file, row.names = FALSE)
}
)
}

shinyApp(ui = ui, server = server)
chas
  • 1,565
  • 5
  • 26
  • 54
  • 2
    see `withProgress` – OganM Oct 17 '18 at 22:09
  • please see the code, withProgress is not working. Any thing wrong there? and i have modified the OP description. – chas Oct 19 '18 at 09:32
  • To prevent one user session from blocking other sessions or cancel tasks, you'll probably want to check out the new async support. This blog post has a good intro: https://blog.rstudio.com/2018/06/26/shiny-1-1-0/ – greg L Oct 20 '18 at 17:00
  • Is your system() command part of your actual code or just an example? Setting wait to FALSE also provides async behaviour. Even though on the long run using promises (also see library(future.callr)) will be the way to go. – ismirsehregal Oct 21 '18 at 14:05
  • yes, actual code has system() commands as the app uses external softwares. – chas Oct 21 '18 at 19:58

2 Answers2

3

Here is a solution based on the (absolutely under-star-ed) library(ipc).

I came across this library due to a question of @Dean Attali, where Joe Cheng mentioned it.

The quick-start guide of the ipc-package gives an example of what you are asking for: AsyncProgress.

Furthermore it provides an example on how to kill a future using AsyncInterruptor. However, I haven't been able to test it yet.

I worked around the cancel-problem by using @Dean Attali's great package shinyjs to simply start a new session and ignore the old Future (You might be able to improve this, by using AsyncInterruptor).

But nevertheless, I gave your code a Future, dropped your system() cmd because I'm currently running R on Windows and found a way to disable (tribute to @Dean Attali) the analyze button session-wise by giving it session-dependant names:

library(shiny)
library(shinydashboard)
library(ipc)
library(promises)
library(future)
library(shinyjs)
library(datasets)
library(V8)

plan(multiprocess)

header <- dashboardHeader(title = "TestApp", titleWidth = 150)

sidebar <- dashboardSidebar(width = 200,
                            sidebarMenu(id = "tabs",
                                        menuItem(
                                          "File", tabName = "tab1", icon = icon("fas fa-file")
                                        )))

body <- dashboardBody(useShinyjs(),
                      fluidRow(column(
                        12, tabItem(
                          tabName = "tab1",
                          h2("Input File"),
                          textOutput("shiny_session"),
                          tabPanel(
                            "Upload file",
                            value = "upload_file",
                            fileInput(
                              inputId = "uploadFile",
                              label = "Upload Input file",
                              multiple = FALSE,
                              accept = c(".txt")
                            ),
                            checkboxInput('header', label = 'Header', TRUE)
                          ),
                          box(
                            title = "Filter X rows",
                            width = 7,
                            status = "info",
                            tabsetPanel(
                              id = "input_tab",
                              tabPanel(
                                "Parameters",
                                numericInput(
                                  "nrows",
                                  label = "Entire number of rows",
                                  value = 5,
                                  max = 10
                                ),
                                column(1, uiOutput("sessionRun")),
                                column(1, uiOutput("sessionCancel"))
                              ),
                              tabPanel(
                                "Results",
                                value = "results",
                                navbarPage(NULL,
                                           tabPanel(
                                             "Table", DT::dataTableOutput("res_table"),
                                             icon = icon("table")
                                           )),
                                downloadButton("downList", "Download")
                              )
                            )
                          )
                        )
                      )))



ui <- shinyUI(dashboardPage(
  header = header,
  sidebar = sidebar,
  body = body,
  title = "TestApp"
))


server <- function(input, output, session) {
  
  output$shiny_session <-
    renderText(paste("Shiny session:", session$token))
  
  file_rows <- reactiveVal()
  
  run_btn_id <- paste0("run_", session$token)
  cancel_btn_id <- paste0("cancel_", session$token)
  
  output$sessionRun <- renderUI({
    actionButton(run_btn_id, "Analyze")
  })
  
  output$sessionCancel <- renderUI({
    actionButton(cancel_btn_id, "Cancel")
  })
  
  paste("Shiny session:", session$token)
  
  
  observeEvent(input[[run_btn_id]], {
    file_rows(NULL)
    
    shinyjs::disable(id = run_btn_id)
    
    progress <- AsyncProgress$new(message = 'Analysis in progress',
                                  detail = 'This may take a while...')
    row_cnt <- isolate(input$nrows)
    get_header <- isolate(input$header)
    
    future({
      fileCon <- file("out.txt", "w+", blocking = TRUE)
      linesCnt <- nrow(iris)
      for (i in seq(linesCnt)) {
        Sys.sleep(0.1)
        progress$inc(1 / linesCnt)
        writeLines(as.character(iris$Species)[i],
                   con = fileCon,
                   sep = "\n")
      }
      close(fileCon)
      head_rows <- read.delim("out.txt", nrows = row_cnt, header=get_header)
      progress$close() # Close the progress bar
      return(head_rows)
    }) %...>% file_rows
    
    return(NULL) # Return something other than the future so we don't block the UI
  })
  
  observeEvent(input[[cancel_btn_id]],{
    session$reload()
  })
  
  observeEvent(file_rows(), {
    shinyjs::enable(id = run_btn_id)
    updateTabsetPanel(session, "input_tab", "results")
    output$res_table <-
      DT::renderDataTable(DT::datatable(
        req(file_rows()),
        options = list(
          searching = TRUE,
          pageLength = 10,
          rownames(NULL),
          scrollX = T
        )
      ))
  })
  
  output$downList <- downloadHandler(
    filename = function() {
      paste0("output", ".txt")
    },
    content = function(file) {
      write.table(file_rows(), file, row.names = FALSE)
    }
  )
}

shinyApp(ui = ui, server = server)

App running:

App running:

ismirsehregal
  • 30,045
  • 5
  • 31
  • 78
  • i tried to exceute the above code but the app is idle and not responding after giving the input. – chas Oct 27 '18 at 08:42
  • Runs fine on my machine, using: `other attached packages: [1] shinyjs_1.0 future_1.10.0 promises_1.0.1 ipc_0.1.0 [5] shinydashboard_0.7.1 shiny_1.1.0.9001` (see screenshot), feedback from other users appreciated. – ismirsehregal Oct 27 '18 at 09:21
  • I explicitly added library(datasets) for the iris data. The only thing I can imagine to be missing. Please try again. – ismirsehregal Oct 27 '18 at 09:28
  • PS: you don't need to input a file - just click 'Analyze'. I dropped that part of your code because I'm running R on Windows, so your system call doesn't work for me. That part of the App isn' critical to show the async behaviour. – ismirsehregal Oct 27 '18 at 10:16
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/182631/discussion-between-chas-and-ismirsehregal). – chas Oct 27 '18 at 11:03
2

This question has been answered on a different forum

For future reference, if anyone comes across this question, here's the full answer (I did not come up with this answer, it's by Joe Cheng)


This seems to be the main piece of code you're asking about:

  observeEvent(input$run, {
    withProgress(session, min = 1, max = 15, {
      setProgress(message = 'Analysis in progress',
        detail = 'This may take a while...')
      for (i in 1:15) {
        setProgress(value = i)
        Sys.sleep(0.5)
      }
    })
    system(paste(
      "cat",
      input$uploadFile$datapath,
      "|",
      paste0("head -", input$nrows) ,
      ">",
      "out.txt"
    ),
      intern = TRUE)
    head_rows <- read.delim("out.txt")
    file_rows(head_rows)
  })

With futures/promises, you need to clearly decide what operations happen inside of the Shiny process, and what operations happen in the future process. In this case, here are the steps that we want to happen, in order:

  1. Show progress message (Shiny process)
  2. Read reactives: input$uploadFile$datapath, input$nrows (Shiny)
  3. Write all but the last nrows to out.txt (future process)
  4. Read out.txt (Could be either, let's say future)
  5. Dismiss progress (Shiny)
  6. Assign result to file_rows (Shiny)

Here's what that looks like:

observeEvent(input$run, {
  prog <- Progress$new(session)
  prog$set(message = "Analysis in progress",
    detail = "This may take a while...",
    value = NULL)

  path <- input$uploadFile$datapath
  nrows <- input$nrows

  future({
    readLines(path) %>% head(-nrows) %>% writeLines("out.txt")
    read.delim("out.txt")
  }) %...>%
    file_rows() %>%
    finally(~prog$close())
})

As long as the future/promise pipeline is the last expression in the observeEvent (which it is in this case, as file_rows() and finally(...) are part of the pipeline) then Shiny will hold off on processing any messages on behalf of the user.

There are two things this solution doesn't address.

  1. Progress messages take a step back; not only are we forced to use the Progress$new() syntax instead of the cleaner withProgress(), but we lost the ability to report on the progress percentage. You can try the new ipc package for a solution to that problem.

This doesn't stop the user from clicking around in the UI; it won't do anything while the async operation is executing, but when the operation is done those interactions will have accumulated in a queue and will be handled in the order that they arrived. If you'd like to actually disable the UI entirely so that they're not able to do anything at all, there's not currently a built-in way to do that in Shiny. Although come to think of it, you might try replacing the use of Progress with showModal(modalDialog(title = "Analysis in progress", "This may take a while...", footer=NULL)); I think that will at least stop mouse clicks.

DeanAttali
  • 25,268
  • 10
  • 92
  • 118