diff --git a/.github/workflows/deploy-app.yml b/.github/workflows/deploy-app.yml new file mode 100644 index 0000000..c35e518 --- /dev/null +++ b/.github/workflows/deploy-app.yml @@ -0,0 +1,87 @@ +# Basic example of a GitHub Actions workflow that builds a Shiny app and deploys +# it to GitHub Pages. +# +# The agreed upon contract is: +# +# - Inspect the root directory for package dependencies +# - Install R and the found packages +# - Export the Shiny app directory to `./site` +# - On push events, deploy the exported app to GitHub Pages +# +# If this contract is not met or could be easily improved for others, +# please open a new Issue https://github.com/posit-dev/r-shinylive/ +# +# The _magic_ of this workflow is in the `shinylive::export()` function, which +# creates a static version of the Shiny app into the folder `./site`. +# The exported app folder is then uploaded and deployed to GitHub Pages. +# +# When deploying to GitHub Pages, be sure to have the appropriate write +# permissions for your token (`pages` and `id-token`). + +name: Deploy app + +on: + push: + branches: [main] + workflow_call: + inputs: + cache-version: + type: string + default: "1" + required: false + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: rstudio/shiny-workflows/setup-r-package@v1 + with: + packages: | + renv + shinylive + sessioninfo + cache-version: ${{ github.event.inputs.cache-version }} + + - name: Find package dependencies + shell: Rscript {0} + id: packages + run: | + # Find package dependencies using {renv} and install with {pak} + pak::pak( + unique(renv::dependencies(".")$Package) + ) + + - name: Build site + shell: Rscript {0} + run: | + shinylive::export(".", "site") + + - name: Upload site artifact + if: github.ref == 'refs/heads/main' + uses: actions/upload-pages-artifact@v3 + with: + path: "site" + + deploy: + if: github.ref == 'refs/heads/main' + needs: build + + # Grant GITHUB_TOKEN the permissions required to make a Pages deployment + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + + # Deploy to the github-pages environment + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + # Specify runner + deployment step + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c4affa --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +site +.Rproj.user diff --git a/DESCRIPTION b/DESCRIPTION new file mode 100644 index 0000000..57bcb8f --- /dev/null +++ b/DESCRIPTION @@ -0,0 +1,7 @@ +Title: WebR binary R package repository +Author: George Stagg +AuthorUrl: https://docs.r-wasm.org/ +License: MIT +DisplayMode: Normal +Tags: webr +Type: Shiny diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..21aa396 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +# MIT License + +Copyright (c) 2023 George Stagg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..271aeec --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# WebR binary R package repository dashboard + +This repository hosts the source code for the Shinylive app running at https://repo.r-wasm.org/. + +The app provides a front page view of the WebR binary R package repository, showing the number of available packages, the number of packages with all dependencies available, and a searchable list of R packages compiled for WebAssembly. + +The app is also available at https://r-wasm.github.io/webr-repo-dashboard/. diff --git a/app.R b/app.R new file mode 100644 index 0000000..d62b2c3 --- /dev/null +++ b/app.R @@ -0,0 +1,152 @@ +library(shiny) +library(bslib) +library(dplyr) +library(DT) + +source_base <- "https://cran.r-project.org/web/packages/" +contrib_base <- "https://repo.r-wasm.org/bin/emscripten/contrib/" +versions <- c("4.2.x" = "4.2", "4.3.x" = "4.3", "4.3.3" = "4.3.3", "4.4.x" = "4.4") + +ui <- page_sidebar( + title = h1("WebR binary R package repository"), + sidebar = sidebar( + title = "Options", + open = FALSE, + selectInput("version", "Select R version", + choices = versions, + selected = versions[[length(versions)]] + ), + ), + p( + class = "lead", + "This CRAN-like repository contains R packages compiled to WebAssembly for use with webR. Set this page's URL as the named", + code("repos"), "argument when using the", code("webr::install()"), + "command to use this repository as the source for downloading binary R packages." + ), + p( + "By default, ", code("webr::install()"), "will use the public repository hosted at", + a(href = "https://repo.r-wasm.org/", "https://repo.r-wasm.org/"), + ". See the", a(href = "https://docs.r-wasm.org/webr/latest/packages.html", "webR documentation"), + "for further information about webR." + ), + h2("Repository statistics"), + layout_columns( + fill = FALSE, + value_box( + title = h2("Built R packages"), + value = textOutput("built"), + showcase = bsicons::bs_icon("hammer"), + "Packages that have been built for WebAssembly and are available for download from this repository." + ), + value_box( + title = "Available R packages", + value = textOutput("available"), + showcase = bsicons::bs_icon("check-circle"), + "Packages for which all of the package dependencies have also been built for WebAssembly and are available for download from this repo." + ), + ), + h2("Packages"), + DTOutput("webr_pkgs") +) + +server <- function(input, output) { + res <- reactive({ + withProgress( + { + repo_info <- as.data.frame(available.packages( + contriburl = paste0(contrib_base, input$version), + filters = c("OS_type", "subarch", "duplicates") + )) + avail_pkgs <- c( + rownames(repo_info), + c( + "base", "compiler", "datasets", "graphics", "grDevices", + "grid", "methods", "splines", "stats", "stats4", + "tools", "utils", "parallel", "webr" + ) + ) + incProgress(2 / 5) + + deps <- tools::package_dependencies( + packages = rownames(repo_info), + db = repo_info, recursive = TRUE + ) + incProgress(2 / 5) + + deps <- tibble( + Package = names(deps), + Available = deps |> purrr::map(\(x) all(x %in% avail_pkgs)), + Depends = deps, + Missing = deps |> purrr::map(\(x) x[!(x %in% avail_pkgs)]), + ) + incProgress(1 / 5) + + package_table <- repo_info |> + select(c("Package", "Version", "Repository")) |> + left_join(deps, by = "Package") |> + arrange(Package) + + list( + table = package_table, + n_built = dim(package_table)[1], + n_avail = sum(as.numeric(deps$Available)) + ) + }, + message = "Loading package lists and crunching dependencies", + detail = "This may take a little while...", + value = 0 + ) + }) + + output$built <- renderText(res()$n_built) + output$available <- renderText(res()$n_avail) + output$webr_pkgs <- renderDT( + datatable( + res()$table, + rownames = FALSE, + selection = "none", + options = list( + ordering = FALSE, + search = list(regex = TRUE), + columns = JS("[ + null, + null, + { searchable: false, visible: false }, + { title: 'All depends available?' }, + { + searchable: false, + title: 'Depends
Missing dependencies are shown in bold.', + }, + { searchable: false, visible: false }, + ]"), + rowCallback = JS(paste0( + " + function(row, data) { + if (data[3][0]) { + $('td:eq(2)', row).html('Yes'); + } else { + $('td:eq(2)', row).html('No'); + } + $('td:eq(0)', row).html( + ", + if (is.null(source_base)) { + "data[0]" + } else { + paste0("`${data[0]}`") + }, + " + ); + $('td:eq(3)', row).html(data[4].map((v) => { + if (data[5].includes(v)) + return '' + v + ''; + return v; + }).join(', ')); + } + " + )) + ) + ) + ) +} + +shinyApp(ui = ui, server = server)