--- title: "Adding Support for Reporting to Custom Modules" author: "NEST CoreDev" output: rmarkdown::html_vignette: toc: true vignette: > %\VignetteIndexEntry{Adding Support for Reporting to Custom Modules} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ## Introduction The `teal` package offers an integrated reporting feature utilizing the `teal.reporter` package. For a comprehensive explanation of the reporting functionality itself, please refer to the documentation therein. This article is *intended for module developers* and aims to provide guidance on enhancing a custom `teal` module with an automatic reporting feature. This enhancement enables users to incorporate snapshots of the module outputs into a report which can then be reviewed in another module automatically provided by `teal`. Thus the app user can interact with the report. The responsibilities of a module developer include: - Choosing whether reporting of their module is needed. - Specifying the outputs that constitute a snapshot of their module. The entire life cycle of objects involved in creating the report and configuring the module to preview the report is handled by `teal`. ## Custom module ```{r setup, include=FALSE} library(teal) library(teal.reporter) ``` ```{r as_interactive, eval=FALSE, echo=FALSE} interactive <- function() TRUE ``` Let us consider an example module, based on the example module from `teal`: ```{r module_1} library(teal) library(teal.reporter) my_module <- function(label = "example teal module") { module( label = label, server = function(id, data) { checkmate::assert_class(isolate(data()), "teal_data") moduleServer(id, function(input, output, session) { updateSelectInput(session, "dataname", choices = isolate(names(data()))) output$dataset <- renderPrint({ req(input$dataname) data()[[input$dataname]] }) }) }, ui = function(id) { ns <- NS(id) sidebarLayout( sidebarPanel(selectInput(ns("dataname"), "Choose a dataset", choices = NULL)), mainPanel(verbatimTextOutput(ns("dataset"))) ) } ) } ``` Using `teal`, you can launch this example module with the following: ```{r app_1} app <- init( data = teal_data(IRIS = iris, MTCARS = mtcars), modules = my_module() ) if (interactive()) { shinyApp(app$ui, app$server) } ``` ```{r shinylive_iframe_1, echo = FALSE, out.width = '150%', out.extra = 'style = "position: relative; z-index:1"', eval = requireNamespace("roxy.shinylive", quietly = TRUE) && knitr::is_html_output() && identical(Sys.getenv("IN_PKGDOWN"), "true")} code <- paste0(c( knitr::knit_code$get("as_interactive"), knitr::knit_code$get("module_1"), knitr::knit_code$get("app_1") ), collapse = "\n") url <- roxy.shinylive::create_shinylive_url(code) knitr::include_url(url, height = "800px") ``` ## Add support for reporting ### Modify the declaration of the server function First we need to prepare the code inside the module to be added to the report. See below: ```{r module_2} my_module_with_card <- function(label = "example teal module") { module( label = label, server = function(id, data) { moduleServer(id, function(input, output, session) { updateSelectInput(session, "dataname", choices = isolate(names(data()))) # Prepare the report: report <- reactive({ req(obj <- data()) teal_card(obj) <- c( teal_card("# Module with reporting"), teal_card(obj), teal_card("## Module's code") ) obj }) # Add to the report the code of the module data_r <- reactive({ req(teal_data <- report(), input$dataname) within(teal_data, table, table = as.name(input$dataname)) }) output$dataset <- renderPrint({ req(teal_data <- data_r()) teal_data[[input$dataname]] }) }) }, ui = function(id) { ns <- NS(id) sidebarLayout( sidebarPanel(selectInput(ns("dataname"), "Choose a dataset", choices = NULL)), mainPanel(verbatimTextOutput(ns("dataset"))) ) } ) } ``` With these modifications, the module is now ready to be launched with `teal`: ```{r app_2} app <- init( data = teal_data(IRIS = iris, MTCARS = mtcars), modules = my_module_with_card() ) if (interactive()) { shinyApp(app$ui, app$server) } ``` ```{r shinylive_iframe_2, echo = FALSE, out.width = '150%', out.extra = 'style = "position: relative; z-index:1"', eval = requireNamespace("roxy.shinylive", quietly = TRUE) && knitr::is_html_output() && identical(Sys.getenv("IN_PKGDOWN"), "true")} code <- paste0(c( knitr::knit_code$get("as_interactive"), knitr::knit_code$get("setup"), knitr::knit_code$get("module_2"), knitr::knit_code$get("app_2") ), collapse = "\n") url <- roxy.shinylive::create_shinylive_url(code) knitr::include_url(url, height = "800px") ``` The output hasn't changed (yet). The final step is to have the server return the reporter object, enabling the module to be reported. ### Return the reporter object ```{r module_3} my_module_with_reporting <- function(label = "example teal module") { module( label = label, server = function(id, data) { moduleServer(id, function(input, output, session) { updateSelectInput(session, "dataname", choices = isolate(names(data()))) # Prepare the report: report <- reactive({ req(obj <- data()) teal_card(obj) <- c( teal_card("# Module with reporting"), teal_card(obj), teal_card("## Module's code") ) obj }) # Add to the report the code of the module data_r <- reactive({ req(rtd <- report(), input$dataname) within(rtd, table, table = as.name(input$dataname)) }) output$dataset <- renderPrint({ req(dr <- data_r()) dr[[input$dataname]] }) # the reactive teal_report is returned by the module data_r }) }, ui = function(id) { ns <- NS(id) sidebarLayout( sidebarPanel(selectInput(ns("dataname"), "Choose a dataset", choices = NULL)), mainPanel(verbatimTextOutput(ns("dataset"))) ) } ) } ``` With these modifications, the module is now ready to be launched with `teal`: ```{r app_3} app <- init( data = teal_data(IRIS = iris, MTCARS = mtcars), modules = my_module_with_reporting() ) if (interactive()) { shinyApp(app$ui, app$server) } ``` ```{r shinylive_iframe_3, echo = FALSE, out.width = '150%', out.extra = 'style = "position: relative; z-index:1"', eval = requireNamespace("roxy.shinylive", quietly = TRUE) && knitr::is_html_output() && identical(Sys.getenv("IN_PKGDOWN"), "true")} code <- paste0(c( knitr::knit_code$get("as_interactive"), knitr::knit_code$get("setup"), knitr::knit_code$get("module_3"), knitr::knit_code$get("app_3") ), collapse = "\n") url <- roxy.shinylive::create_shinylive_url(code) knitr::include_url(url, height = "800px") ``` The key step is to return a reactive `teal_report` object containing everything. This informs `teal` that the module provides a `reporter`, and teal will add a button `+ Add to Report` to add the modules' content to the report. The user can now add a card to the report with the current state of the module. The report can be seen after clicking `Preview report` under the `Report` button. ### Add content to the card The user can modify the text of a card or add new text with the button `+ Add text block` present at the bottom of the card. Text can also be added inside the module by appending a `teal_card()` to the card of the report. As the module writer, you can also add any other content to the report you'd like: titles, text. ### Add non-text content to the card `teal.reporter` supports the addition of tables, charts, and more. For more information, explore the API of `teal_report()` to learn about the supported content types. ## Removing support for displaying reproducible code If your module supports a report but you want to disable the button that allows to display the module's reproducible code ("Show R code"), use `disable_src()`: ```{r app_4} app <- init( data = teal_data(IRIS = iris, MTCARS = mtcars), modules = my_module_with_reporting() |> disable_src() ) if (interactive()) { shinyApp(app$ui, app$server) } ``` ```{r shinylive_iframe_4, echo = FALSE, out.width = '150%', out.extra = 'style = "position: relative; z-index:1"', eval = requireNamespace("roxy.shinylive", quietly = TRUE) && knitr::is_html_output() && identical(Sys.getenv("IN_PKGDOWN"), "true")} code <- paste0(c( knitr::knit_code$get("as_interactive"), knitr::knit_code$get("setup"), knitr::knit_code$get("module_3"), knitr::knit_code$get("app_4") ), collapse = "\n") url <- roxy.shinylive::create_shinylive_url(code) knitr::include_url(url, height = "800px") ``` You can use `disable_src()` on multiple modules at the same time and nested modules too. For example on: ```{r eval = FALSE} app <- init( data = teal_data(IRIS = iris, MTCARS = mtcars), modules = c( modules( label = "One nested module disabled", example_module(label = "Module 1"), example_module(label = "Module 2"), example_module(label = "Module 3") |> disable_src() ), modules( label = "Nested modules without source", example_module(label = "Module 1"), example_module(label = "Module 2") ) |> disable_src() ) ) ``` ## Removing reporting If a module has the reporter functionality the teal app developer can disable it with `disable_report()`. ```{r app_5} app <- init( data = teal_data(IRIS = iris, MTCARS = mtcars), modules = my_module_with_reporting() |> disable_report() ) if (interactive()) { shinyApp(app$ui, app$server) } ``` ```{r shinylive_iframe_5, echo = FALSE, out.width = '150%', out.extra = 'style = "position: relative; z-index:1"', eval = requireNamespace("roxy.shinylive", quietly = TRUE) && knitr::is_html_output() && identical(Sys.getenv("IN_PKGDOWN"), "true")} code <- paste0(c( knitr::knit_code$get("as_interactive"), knitr::knit_code$get("setup"), knitr::knit_code$get("module_3"), knitr::knit_code$get("app_5") ), collapse = "\n") url <- roxy.shinylive::create_shinylive_url(code) knitr::include_url(url, height = "800px") ``` To remove reporting from the whole application, set `reporter = NULL` in `init()`. This will completely disable all the reporter related buttons on the application: ![Screenshot of an app without the reporter options](images/custom_module_without_reporter.png) ## Customizing the reporter A template can be set for the report; when a template is used each card added to the report contains the template's default content. Additionally, cards can be added to the report before the application starts. ```{r reporter_6} reporter <- Reporter$new() template_fun <- function(document) { header <- teal_card("Here comes header text.") logo_url <- "https://raw.githubusercontent.com/insightsengineering/teal/refs/heads/main/man/figures/logo.svg" footer <- teal_card(paste0( "Here comes footer text. Report generated with teal ![logo](%s 'teal logo'){height=70}", logo_url )) c(header, document, footer) } reporter$set_template(template_fun) card1 <- teal_card("## Header 2 text", "Regular text") metadata(card1, "title") <- "Welcome card" reporter$append_cards(card1) ``` Once the reporter is created we can use in the teal application.