diff --git a/.Rprofile b/.Rprofile new file mode 100644 index 0000000..3b081a1 --- /dev/null +++ b/.Rprofile @@ -0,0 +1,9 @@ +if (file.exists("renv")) { + source("renv/activate.R") +} else { + # The `renv` directory is automatically skipped when deploying with rsconnect. + message("No 'renv' directory found; renv won't be activated.") +} + +# Allow absolute module imports (relative to the app root). +options(box.path = getwd()) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..88631fd --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,33 @@ +on: + workflow_dispatch: + push: + branches: add-webpage + +name: Quarto Publish + +defaults: + run: + working-directory: ./docs + +jobs: + build-deploy: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Copy README + run: cp ../README.md . + + - name: Set up Quarto + uses: quarto-dev/quarto-actions/setup@v2 + + - name: Render and Publish + uses: quarto-dev/quarto-actions/publish@v2 + with: + target: gh-pages + path: docs + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/rhino-test.yml b/.github/workflows/rhino-test.yml new file mode 100644 index 0000000..678d22a --- /dev/null +++ b/.github/workflows/rhino-test.yml @@ -0,0 +1,72 @@ +name: Rhino Test +on: push +permissions: + contents: read +jobs: + main: + name: Run linters and tests + runs-on: ubuntu-22.04 + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup system dependencies + run: | + packages=( + # List each package on a separate line. + ) + sudo apt-get update + sudo apt-get install --yes "${packages[@]}" + + - name: Setup R + uses: r-lib/actions/setup-r@v2 + with: + r-version: renv + + - name: Setup R dependencies + uses: r-lib/actions/setup-renv@v2 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: 20 + + - name: Lint R + if: always() + shell: Rscript {0} + run: rhino::lint_r() + + - name: Lint JavaScript + if: always() + shell: Rscript {0} + run: rhino::lint_js() + + - name: Lint Sass + if: always() + shell: Rscript {0} + run: rhino::lint_sass() + + - name: Build JavaScript + if: always() + shell: Rscript {0} + run: rhino::build_js() + + - name: Build Sass + if: always() + shell: Rscript {0} + run: rhino::build_sass() + + - name: Run R unit tests + if: always() + shell: Rscript {0} + run: rhino::test_r() + + - name: Run Cypress end-to-end tests + if: always() + uses: cypress-io/github-action@v6 + with: + working-directory: .rhino # Created by earlier commands which use Node.js + start: npm run run-app + project: ../tests + wait-on: 'http://localhost:3333/' + wait-on-timeout: 60 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..039024e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.Rproj.user +.Rhistory +.RData +.Ruserdata +.DS_Store +.Renviron diff --git a/.lintr b/.lintr new file mode 100644 index 0000000..63fade9 --- /dev/null +++ b/.lintr @@ -0,0 +1,5 @@ +linters: + linters_with_defaults( + line_length_linter = line_length_linter(100), + object_usage_linter = NULL # Does not work with `box::use()`. + ) diff --git a/.renvignore b/.renvignore new file mode 100644 index 0000000..4f16dc6 --- /dev/null +++ b/.renvignore @@ -0,0 +1,3 @@ +# Only use `dependencies.R` to infer project dependencies. +* +!dependencies.R diff --git a/.rscignore b/.rscignore new file mode 100644 index 0000000..fdd749b --- /dev/null +++ b/.rscignore @@ -0,0 +1,7 @@ +.github +.lintr +.renvignore +.Renviron +.rhino +.rscignore +tests diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a64f6aa --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +Copyright (C) 2024 Appsilon and Elkem + +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 X CONSORTIUM 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. + +Except as contained in this notice, the name of Appsilon or Elkem shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from Appsilon and Elkem. diff --git a/LogAnalyzer.Rproj b/LogAnalyzer.Rproj new file mode 100644 index 0000000..e83436a --- /dev/null +++ b/LogAnalyzer.Rproj @@ -0,0 +1,16 @@ +Version: 1.0 + +RestoreWorkspace: Default +SaveWorkspace: Default +AlwaysSaveHistory: Default + +EnableCodeIndexing: Yes +UseSpacesForTab: Yes +NumSpacesForTab: 2 +Encoding: UTF-8 + +RnwWeave: Sweave +LaTeX: pdfLaTeX + +AutoAppendNewline: Yes +StripTrailingWhitespace: Yes diff --git a/README.md b/README.md new file mode 100644 index 0000000..2de84ac --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# LogAnalyzer + +The LogAnalyzer open-source app is a simple, plug and play application developed first in collaboration with [Elkem](https://www.elkem.com/). The app provides an ability to get semantically coloured logs for applications deployed on Posit Connect by simply changing the default environment variables and deploying it on Posit Connect. Given the general usefulness of the app, we have decided to share it with the wider community to use and improve upon it. No more sifting through long text files; you can simply find the reds and see where things break. It has never been easier to investigate what goes wrong with your applications. + +# How it works? + +- The LogAnalyzer app uses the Posit Connect API to fetch logs for application run jobs, semantically colouring them so they are easier to read. +- The only thing you need to set this up and use is the `CONNECT_API_KEY` set as an environment variable. The idea is that the key should come from either an admin account or someone with privileges to view all apps. Apps that are not available to a user will not have their logs available to them. +- If you want to test the app locally, you will need to set the `CONNECT_SERVER` as an environment variable. When deployed, the `CONNECT_SERVER` is setup automatically for you. + +![LogAnalyzerDemo](https://github.com/Appsilon/LogAnalyzer/assets/26517718/90d1111d-006b-42db-8d8b-b55ee391cb21) + +# Credits +It was our collaboration with Elkem which led to the creation of this app. The initial idea came from use-cases where we realised we wanted to track all the logs and be able to read them properly since Posit Connect was the de facto deployment environment. When we made this app, we realised there was potential in sharing this with the rest of the community and invite everyone to use it and add it. We appreciate and thank Elkem for their openness to share it with the world. + +You can read more about Appsilon and Elkem's collaboration on our case study [here](https://www.appsilon.com/case-studies/refining-elkems-processes-with-advanced-data-analytics). + +## Appsilon + + + +Appsilon is a **Posit (formerly RStudio) Full Service Certified Partner**.
+Learn more at [appsilon.com](https://appsilon.com). + +Get in touch [opensource@appsilon.com](mailto:opensource@appsilon.com) + +Explore the [Rhinoverse](https://rhinoverse.dev) - a family of R packages built around [Rhino](https://appsilon.github.io/rhino/)! + + + Subscribe for Shiny tutorials, exclusive articles, R/Shiny community events, and more. + diff --git a/app.R b/app.R new file mode 100644 index 0000000..9328819 --- /dev/null +++ b/app.R @@ -0,0 +1,2 @@ +# Rhino / shinyApp entrypoint. Do not edit. +rhino::app() diff --git a/app/js/index.js b/app/js/index.js new file mode 100644 index 0000000..e69de29 diff --git a/app/logic/api_utils.R b/app/logic/api_utils.R new file mode 100644 index 0000000..ea09ead --- /dev/null +++ b/app/logic/api_utils.R @@ -0,0 +1,184 @@ +box::use( + dplyr[filter], + glue[glue], + httr2[ + req_auth_bearer_token, + req_dry_run, + req_perform, + req_user_agent, + request, + resp_body_string + ], + jsonlite[fromJSON], + magrittr[`%>%`], +) + +#' Simple function to get the access token from environment +#' @return Character token, if present +get_access_token <- function() { + token <- Sys.getenv( + "CONNECT_API_KEY", + unset = "NO_TOKEN" + ) + if (token == "NO_TOKEN" || token == "") { + stop("Are you sure your CONNECT_API_KEY is set?") + } else { + token + } +} + +#' Simple function to make an API url +#' @param host Character. Default CONNECT_SERVER set as an envvar +#' @param endpoint Character. Default is "content" +#' @param version Logical. Whether to use versioned API. Default is "v1" +#' @return url for the API +get_api_url <- function( + host = Sys.getenv("CONNECT_SERVER"), + endpoint = "content", + version = "v1" +) { + glue("{host}__api__/{version}/{endpoint}/") +} + +#' Function to get a list of all apps belonging to the token +#' +#' @param app_mode_filter Character. The filter for app_mode in the API +#' response. Default is "shiny". +#' @param endpoint Character. Default is "content" +#' @param dry_run Logical. Whether to dry run the API for debugging. +#' Default is FALSE +#' @export +get_app_list <- function( + app_mode_filter = "shiny", + endpoint = "content", + dry_run = FALSE +) { + + url <- get_api_url( + endpoint = endpoint + ) + + api_request <- request(url) %>% + req_user_agent("LogAnalyzer") %>% + req_auth_bearer_token(get_access_token()) + + if (dry_run) { + api_request %>% + req_dry_run() + } else { + api_request %>% + req_perform() %>% + resp_body_string() %>% + fromJSON() %>% + filter(app_mode == app_mode_filter) + } +} + +#' Function to get a list of all jobs for a specific app +#' +#' @param guid Character. The guid for the app in question +#' @param endpoint Character. Default is "content" +#' @param dry_run Logical. Whether to dry run the API for debugging. +#' Default is FALSE +#' @export +get_job_list <- function( + guid = NULL, + endpoint = "content", + dry_run = FALSE +) { + + url <- get_api_url( + endpoint = endpoint + ) + + api_request <- request( + glue("{url}{guid}/jobs") + ) %>% + req_user_agent("LogAnalyzer") %>% + req_auth_bearer_token(get_access_token()) + + if (dry_run) { + api_request %>% + req_dry_run() + } else { + api_request %>% + req_perform() %>% + resp_body_string() %>% + fromJSON() + } +} + +#' Function to get a list of all logs for a job for a specific app +#' +#' @param guid Character. The guid for the app in question +#' @param job_key Character. The key for the job in question +#' @param endpoint Character. Default is "content" +#' @param tail Logical. Whether to show the tail only for the logs +#' @param dry_run Logical. Whether to dry run the API for debugging. +#' Default is FALSE +#' @export +get_job_logs <- function( + guid = NULL, + job_key = NULL, + endpoint = "content", + tail = FALSE, + dry_run = FALSE +) { + + url <- get_api_url( + endpoint = endpoint + ) + + api_request <- request( + glue("{url}{guid}/jobs/{job_key}/{ifelse(tail, 'tail', 'log')}") + ) %>% + req_user_agent("LogAnalyzer") %>% + req_auth_bearer_token(get_access_token()) + + if (dry_run) { + api_request %>% + req_dry_run() + } else { + logs <- api_request %>% + req_perform() %>% + resp_body_string() %>% + fromJSON() + logs["entries"] %>% + data.frame() + } +} + +#' Function to download the logfile for a specific app +#' +#' @param guid Character. The guid for the app in question +#' @param job_key Character. The key for the job in quesrtion +#' @param endpoint Character. Default is "content" +#' @param dry_run Logical. Whether to dry run the API for debugging. +#' Default is FALSE +#' @export +download_job_logs <- function( + guid = NULL, + job_key = NULL, + endpoint = "content", + dry_run = FALSE +) { + + url <- get_api_url( + endpoint = endpoint + ) + + api_request <- request( + glue("{url}{guid}/jobs/{job_key}/download") + ) %>% + req_user_agent("LogAnalyzer") %>% + req_auth_bearer_token(get_access_token()) + + if (dry_run) { + api_request %>% + req_dry_run() + } else { + api_request %>% + req_perform() %>% + resp_body_string() + } +} diff --git a/app/logic/app_table_utils.R b/app/logic/app_table_utils.R new file mode 100644 index 0000000..125dd99 --- /dev/null +++ b/app/logic/app_table_utils.R @@ -0,0 +1,54 @@ +box::use( + shiny[ + a, + div, + icon, + span, + strong + ], +) + +box::use( + app/logic/general_utils[format_timestamp], +) + +#' Function to process each row for the app table +#' This creates the HTML for the row +#' +#' @export +process_app_data <- function( + app_data +) { + app_info <- strsplit(app_data, "_-_")[[1]] + div( + class = "app-entry", + div( + class = "app-title", + span( + app_info[1], + a( + href = app_info[3], + class = "app-link", + icon( + name = "arrow-up-right-from-square", + class = "app-link-icon" + ), + target = "_blank" + ) + ) + ), + div( + class = "app-metadata", + div( + class = "app-last-deployed", + strong("Last Deployed: "), + format_timestamp(app_info[4]) + ), + span( + class = "app-r-version", + strong("R version: "), + app_info[2] + ) + ) + ) +} diff --git a/app/logic/general_utils.R b/app/logic/general_utils.R new file mode 100644 index 0000000..30e33bf --- /dev/null +++ b/app/logic/general_utils.R @@ -0,0 +1,39 @@ +#' Function to check if a string of log text has error keywords +#' +#' @param text Character. The log string +#' @param wordlist Character vector. List of keywords to scan. Default +#' list is `c("halt", "err", "terminat", "not found")` +#' @param ignore_case Logical. Whether to ignore the case for words +#' Default is TRUE +#' @export +check_text_error <- function( + text, + wordlist = c("halt", "err", "terminat", "not found"), + ignore_case = TRUE +) { + grepl( + paste(wordlist, collapse = "|"), + text, + ignore.case = ignore_case + ) +} + +#' Function to convert timestamp between formats +#' +#' @param timestamp Character. The timestamp string +#' @param from Character. Original format. Default is "%Y-%m-%dT%H:%M:%OSZ" +#' @param to Character. New format. Default is "%Y-%m-%d %H:%M:%S" +#' @export +format_timestamp <- function( + timestamp, + from = "%Y-%m-%dT%H:%M:%OSZ", + to = "%Y-%m-%d %H:%M:%S" +) { + format( + as.POSIXct( + timestamp, + format = from + ), + format = to + ) +} diff --git a/app/logic/job_list_utils.R b/app/logic/job_list_utils.R new file mode 100644 index 0000000..02bceb0 --- /dev/null +++ b/app/logic/job_list_utils.R @@ -0,0 +1,43 @@ +box::use( + shiny[ + div, + strong + ], +) + +box::use( + app/logic/general_utils[format_timestamp], +) + + +#' Function to process each row for the job table +#' This creates the HTML for the row +#' +#' @export +process_job_data <- function( + job_data +) { + job_info <- strsplit(job_data, "_-_")[[1]] + div( + class = "job-entry", + div( + class = "job-id", + job_info[1] + ), + div( + class = "job-key", + strong("Key: "), + job_info[2] + ), + div( + class = "job-start-time", + strong("Start: "), + format_timestamp(job_info[3], "%Y-%m-%dT%H:%M:%S") + ), + div( + class = "job-end-time", + strong("End: "), + format_timestamp(job_info[4], "%Y-%m-%dT%H:%M:%S") + ) + ) +} diff --git a/app/logic/logs_utils.R b/app/logic/logs_utils.R new file mode 100644 index 0000000..ebbec6e --- /dev/null +++ b/app/logic/logs_utils.R @@ -0,0 +1,55 @@ +box::use( + glue[glue], + shiny[ + div, + icon + ], +) + +box::use( + app/logic/general_utils[check_text_error, format_timestamp], +) + +#' Function to process each row for the log table +#' This creates the HTML for the row +#' +#' @export +process_log_data <- function( + log_data +) { + log_info <- strsplit(log_data, "_-_")[[1]] + status <- get_status_info(log_info[1], log_info[3]) + div( + class = glue("log-entry {status[1]}-highlight"), + icon( + name = status[2], + class = glue( + "log-status {status[1]}-text fa-solid" + ), + ), + div( + class = "log-info-block", + div( + class = glue("log-info {status[1]}-text"), + log_info[3] + ), + div( + class = "log-time", + format_timestamp(log_info[2]) + ) + ) + ) +} + +get_status_info <- function( + output_type, + log_data +) { + if (output_type == "stdout") { + c("green", "circle-info") + } else if (output_type == "stderr" && check_text_error(log_data)) { + c("red", "circle-xmark") + } else { + c("yellow", "circle-info") + } +} diff --git a/app/main.R b/app/main.R new file mode 100644 index 0000000..d221881 --- /dev/null +++ b/app/main.R @@ -0,0 +1,139 @@ +# nolint start: box_func_import_count_linter +box::use( + dplyr[select], + magrittr[`%>%`], + shiny[ + div, + fluidPage, + img, + isTruthy, + moduleServer, + NS, + observeEvent, + p, + reactive, + reactiveValues, + removeUI, + renderUI, + tagList, + tags, + uiOutput + ], + shinycssloaders[withSpinner], +) +# nolint end + +box::use( + app/logic/api_utils[get_app_list], + app/view/mod_app_table, + app/view/mod_header, + app/view/mod_job_list, + app/view/mod_logs, +) + +#' @export +ui <- function(id) { + ns <- NS(id) + fluidPage( + class = "dashboard-body", + mod_header$ui("header"), + div( + class = "dashboard-container", + div( + class = "app-table", + mod_app_table$ui(ns("app_table")) + ), + div( + class = "vertical-line" + ), + div( + class = "job-list", + uiOutput(ns("job_list_pane")) + ), + div( + class = "vertical-line" + ), + div( + class = "logs", + uiOutput(ns("logs_pane")) + ) + ) + ) +} + +#' @export +server <- function(id) { + moduleServer(id, function(input, output, session) { + + ns <- session$ns + + mod_header$server("header") + + state <- reactiveValues() + state$selected_app <- reactive({}) + state$selected_job <- reactive({}) + + app_list <- reactive({ + get_app_list() + }) + + mod_app_table$server( + "app_table", + app_list() %>% + select( + guid, + name, + r_version, + dashboard_url, + last_deployed_time + ), + state + ) + + observeEvent(state$selected_app()$guid, { + if (isTruthy(state$selected_app()$guid)) { + + output$job_list_pane <- renderUI({ + mod_job_list$ui(ns("job_list")) + }) + + mod_job_list$server( + "job_list", + state + ) + } else { + removeUI(ns("job_list_pane")) + } + }, ignoreInit = TRUE, ignoreNULL = TRUE) + + observeEvent(state$selected_job()$key, { + if (isTruthy(state$selected_job()$key)) { + + output$logs_pane <- renderUI({ + mod_logs$ui(ns("logs")) + }) + + mod_logs$server( + "logs", + state + ) + } else { + output$logs_pane <- renderUI({ + div( + class = "empty-state-container", + p( + class = "empty-state-text", + "Select an application and a job to view logs" + ), + img( + src = "static/empty_state.svg", + class = "empty-state-image", + alt = "Select an application and a job to view logs" + ) + ) + }) + } + }, ignoreInit = FALSE, ignoreNULL = FALSE) + + }) +} diff --git a/app/static/appsilon-logo.png b/app/static/appsilon-logo.png new file mode 100644 index 0000000..973de3d Binary files /dev/null and b/app/static/appsilon-logo.png differ diff --git a/app/static/css/app.min.css b/app/static/css/app.min.css new file mode 100644 index 0000000..8410136 --- /dev/null +++ b/app/static/css/app.min.css @@ -0,0 +1 @@ +@import"https://fonts.googleapis.com/css2?family=Maven+Pro:wght@400;600&display=swap";.red-text{color:#a50e0e}.green-text{color:#3a5a40}.yellow-text{color:#a58e0e}.red-highlight{background-color:rgba(252,232,230,.3137254902)}.green-highlight{background-color:rgba(224,240,223,.3137254902)}.yellow-highlight{background-color:rgba(240,235,187,.3137254902)}.red-text{color:#a50e0e}.green-text{color:#3a5a40}.yellow-text{color:#a58e0e}.red-highlight{background-color:rgba(252,232,230,.3137254902)}.green-highlight{background-color:rgba(224,240,223,.3137254902)}.yellow-highlight{background-color:rgba(240,235,187,.3137254902)}.logs .rt-td-inner{padding:0 !important}.logs>div{text-align:center}.logs>div .empty-state-container{margin-top:150px}.logs>div .empty-state-container .empty-state-image{width:50%}.logs>div .empty-state-container .empty-state-text{color:gray;margin-bottom:40px}.logs-container{position:relative}.logs-container .log-entry{display:flex;align-items:center;gap:20px;padding:10px;margin:5px 10px}.logs-container .log-entry i{font-size:1.5em}.logs-container .log-entry .log-info-block{display:flex;flex-direction:column;gap:10px}.logs-container .log-entry .log-info-block .log-info{font-weight:600}.logs-container .log-entry .log-info-block .log-time{font-size:.75em}.logs-container .logs-download{position:absolute;z-index:2;right:0;margin:10px;background:0;border-radius:0;padding:5px 10px}.wrapper{background:none !important}.content-wrapper{background:#fff;color:#333;height:90vh}.dashboard-body{display:flex;flex-direction:column;padding:0}.dashboard-body .dashboard-container{display:flex;flex-direction:row;height:100vh}.dashboard-body .reactable{background:rgba(0,0,0,0)}.dashboard-body .rt-search{width:80%;margin:10px 10px 20px;align-self:center;text-align:center;border-radius:0}.dashboard-body .rt-tr-header{display:none !important}.dashboard-body .rt-tr{align-items:center}.dashboard-body .rt-tr-selected{background:rgba(0,0,0,.062745098)}.dashboard-body .app-table{width:30%;height:100%;overflow-y:auto}.dashboard-body .job-list{width:15%;height:100%;overflow-y:auto}.dashboard-body .logs{background:#fff;width:55%;height:100%;overflow-y:auto}.app-entry{display:flex;flex-direction:column;width:100%}.app-entry .app-title{font-size:1.1em}.app-entry .app-link-icon{font-size:.5em;margin-left:10px;margin-bottom:10px}.app-entry .app-metadata{display:flex;flex-direction:column;gap:5px;color:gray;font-size:.75em}.red-text{color:#a50e0e}.green-text{color:#3a5a40}.yellow-text{color:#a58e0e}.red-highlight{background-color:rgba(252,232,230,.3137254902)}.green-highlight{background-color:rgba(224,240,223,.3137254902)}.yellow-highlight{background-color:rgba(240,235,187,.3137254902)}.job-entry .job-key,.job-entry .job-start-time,.job-entry .job-end-time{font-size:.75em;color:gray}.header{display:flex;width:100%;align-items:center;justify-content:space-between;gap:10px;margin-bottom:20px}.header .header-section{display:flex;align-items:center;gap:10px}.header .left img{width:200px}.header .left h2{margin:0;margin-bottom:5px;margin-left:20px}.header .left .vertical-line{height:50px}.header .right .cta-button{background:#0099f9;color:#fff;padding:10px;border-radius:10px;margin:0 10px}*{font-family:"Maven Pro",sans-serif}.vertical-line{border-left:1px #eee solid;height:80%;align-self:center} diff --git a/app/static/empty_state.svg b/app/static/empty_state.svg new file mode 100644 index 0000000..f64d1ee --- /dev/null +++ b/app/static/empty_state.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/static/favicon.ico b/app/static/favicon.ico new file mode 100644 index 0000000..9f938b2 Binary files /dev/null and b/app/static/favicon.ico differ diff --git a/app/static/rhino.png b/app/static/rhino.png new file mode 100644 index 0000000..b46cf47 Binary files /dev/null and b/app/static/rhino.png differ diff --git a/app/styles/_app_table.scss b/app/styles/_app_table.scss new file mode 100644 index 0000000..1904be5 --- /dev/null +++ b/app/styles/_app_table.scss @@ -0,0 +1,23 @@ +.app-entry { + display: flex; + flex-direction: column; + width: 100%; + + .app-title { + font-size: 1.1em; + } + + .app-link-icon { + font-size: 0.5em; + margin-left: 10px; + margin-bottom: 10px; + } + + .app-metadata { + display: flex; + flex-direction: column; + gap: 5px; + color: $grey-text; + font-size: 0.75em; + } +} diff --git a/app/styles/_colors.scss b/app/styles/_colors.scss new file mode 100644 index 0000000..9c7f1cb --- /dev/null +++ b/app/styles/_colors.scss @@ -0,0 +1,40 @@ +$red: #a50e0e; +$red-highlight: #fce8e650; +$green: #3a5a40; +$green-highlight: #e0f0df50; +$yellow: #a58e0e; +$yellow-highlight: #f0ebbb50; +$grey1: #eaeaea; +$grey1-border: #d8d8d8; +$grey2: #f8f8f8; +$grey2-border: #eee; +$black: black; +$white: white; +$grey-text: grey; +$black-text: #333; +$selected-row: #00000010; +$appsilon-blue: #0099f9; + +.red-text { + color: $red; +} + +.green-text { + color: $green; +} + +.yellow-text { + color: $yellow; +} + +.red-highlight { + background-color: $red-highlight; +} + +.green-highlight { + background-color: $green-highlight; +} + +.yellow-highlight { + background-color: $yellow-highlight; +} diff --git a/app/styles/_dashboard.scss b/app/styles/_dashboard.scss new file mode 100644 index 0000000..c99af6a --- /dev/null +++ b/app/styles/_dashboard.scss @@ -0,0 +1,64 @@ +.wrapper { + background: none !important; +} + +.content-wrapper { + background: $white; + color: $black-text; + height: 90vh; +} + +.dashboard-body { + display: flex; + flex-direction: column; + padding: 0; + + .dashboard-container { + display: flex; + flex-direction: row; + height: 100vh; + } + + .reactable { + background: transparent; + } + + .rt-search { + width: 80%; + margin: 10px 10px 20px; + align-self: center; + text-align: center; + border-radius: 0; + } + + .rt-tr-header { + display: none !important; + } + + .rt-tr { + align-items: center; + } + + .rt-tr-selected { + background: $selected-row; + } + + .app-table { + width: 30%; + height: 100%; + overflow-y: auto; + } + + .job-list { + width: 15%; + height: 100%; + overflow-y: auto; + } + + .logs { + background: $white; + width: 55%; + height: 100%; + overflow-y: auto; + } +} diff --git a/app/styles/_header.scss b/app/styles/_header.scss new file mode 100644 index 0000000..b959d4e --- /dev/null +++ b/app/styles/_header.scss @@ -0,0 +1,40 @@ +.header { + display: flex; + width: 100%; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 20px; + + .header-section { + display: flex; + align-items: center; + gap: 10px; + } + + .left { + img { + width: 200px; + } + + h2 { + margin: 0; + margin-bottom: 5px; + margin-left: 20px; + } + + .vertical-line { + height: 50px; + } + } + + .right { + .cta-button { + background: $appsilon-blue; + color: white; + padding: 10px; + border-radius: 10px; + margin: 0 10px; + } + } +} diff --git a/app/styles/_job_list.scss b/app/styles/_job_list.scss new file mode 100644 index 0000000..1ca3cef --- /dev/null +++ b/app/styles/_job_list.scss @@ -0,0 +1,10 @@ +@import "colors"; + +.job-entry { + .job-key, + .job-start-time, + .job-end-time { + font-size: 0.75em; + color: $grey-text; + } +} diff --git a/app/styles/_logs.scss b/app/styles/_logs.scss new file mode 100644 index 0000000..5f4535e --- /dev/null +++ b/app/styles/_logs.scss @@ -0,0 +1,64 @@ +@import "colors"; + +.logs { + .rt-td-inner { + padding: 0 !important; + } + + > div { + text-align: center; + + .empty-state-container { + margin-top: 150px; + + .empty-state-image { + width: 50%; + } + + .empty-state-text { + color: $grey-text; + margin-bottom: 40px; + } + } + } +} + +.logs-container { + position: relative; + + .log-entry { + display: flex; + align-items: center; + gap: 20px; + padding: 10px; + margin: 5px 10px; + + i { + font-size: 1.5em; + } + + .log-info-block { + display: flex; + flex-direction: column; + gap: 10px; + + .log-info { + font-weight: 600; + } + + .log-time { + font-size: 0.75em; + } + } + } + + .logs-download { + position: absolute; + z-index: 2; + right: 0; + margin: 10px; + background: 0; + border-radius: 0; + padding: 5px 10px; + } +} diff --git a/app/styles/main.scss b/app/styles/main.scss new file mode 100644 index 0000000..e32d48c --- /dev/null +++ b/app/styles/main.scss @@ -0,0 +1,17 @@ +@import "https://fonts.googleapis.com/css2?family=Maven+Pro:wght@400;600&display=swap"; +@import "colors"; +@import "logs"; +@import "dashboard"; +@import "app_table"; +@import "job_list"; +@import "header"; + +* { + font-family: "Maven Pro", sans-serif; +} + +.vertical-line { + border-left: 1px $grey2-border solid; + height: 80%; + align-self: center; +} diff --git a/app/view/mod_app_table.R b/app/view/mod_app_table.R new file mode 100644 index 0000000..02d4556 --- /dev/null +++ b/app/view/mod_app_table.R @@ -0,0 +1,92 @@ +box::use( + dplyr[ + mutate, + select + ], + magrittr[`%>%`], + reactable[ + colDef, + getReactableState, + reactable, + reactableOutput, + renderReactable + ], + shiny[ + moduleServer, + NS, + reactive + ], + shinycssloaders[withSpinner], +) + +box::use( + app/logic/app_table_utils[process_app_data], +) + +#' @export +ui <- function(id) { + ns <- NS(id) + withSpinner( + reactableOutput( + ns("app_table") + ), + type = 8, + color = "#333333" + ) +} + +#' @export +server <- function(id, app_list, state) { + moduleServer(id, function(input, output, session) { + + output$app_table <- renderReactable({ + + processed_apps <- app_list %>% + mutate( + name = paste( + name, + r_version, + dashboard_url, + last_deployed_time, + sep = "_-_" + ) + ) %>% + select( + -c( + r_version, + dashboard_url, + last_deployed_time + ) + ) + + reactable( + data = processed_apps, + searchable = TRUE, + borderless = TRUE, + pagination = FALSE, + selection = "single", + columns = list( + guid = colDef( + show = FALSE + ), + name = colDef( + name = "Application", + cell = function(app_data) { + process_app_data(app_data) + } + ) + ) + ) + }) + + state$selected_app <- reactive({ + index <- getReactableState("app_table", "selected") + list( + "guid" = app_list[index, ]$guid, + "name" = app_list[index, ]$name + ) + }) + + }) + +} diff --git a/app/view/mod_header.R b/app/view/mod_header.R new file mode 100644 index 0000000..e94e1d0 --- /dev/null +++ b/app/view/mod_header.R @@ -0,0 +1,48 @@ +box::use( + shiny[ + actionLink, + div, + h2, + img, + moduleServer, + NS + ], +) + +#' @export +ui <- function(id) { + ns <- NS(id) + div( + class = "header", + div( + class = "left header-section", + img( + src = "static/appsilon-logo.png", + alt = "Appsilon logo", + href = "https://demo.appsilon.com" + ), + div( + class = "vertical-line" + ), + h2( + "LogAnalyzer" + ) + ), + div( + class = "right header-section", + actionLink( + "lets-talk", + label = "Let's Talk", + class = "cta-button", + onclick = "window.open('https://appsilon.com/#contact', '_blank');" + ) + ) + ) +} + +#' @export +server <- function(id) { + moduleServer(id, function(input, output, session) { + + }) +} diff --git a/app/view/mod_job_list.R b/app/view/mod_job_list.R new file mode 100644 index 0000000..96aac6e --- /dev/null +++ b/app/view/mod_job_list.R @@ -0,0 +1,95 @@ +box::use( + dplyr[ + mutate, + select + ], + magrittr[`%>%`], + reactable[ + colDef, + getReactableState, + reactable, + reactableOutput, + renderReactable + ], + shiny[ + moduleServer, + NS, + reactive, + req + ], + shinycssloaders[withSpinner], +) + +box::use( + app/logic/api_utils[get_job_list], + app/logic/job_list_utils[process_job_data], +) + +#' @export +ui <- function(id) { + ns <- NS(id) + withSpinner( + reactableOutput( + ns("job_list_table") + ), + type = 8, + color = "#333333" + ) +} + +#' @export +server <- function(id, state) { + moduleServer(id, function(input, output, session) { + + job_list_data <- reactive({ + req(state$selected_app()$guid) + get_job_list(state$selected_app()$guid) + }) + + output$job_list_table <- renderReactable({ + + processed_jobs <- job_list_data() %>% + select(id, key, start_time, end_time) %>% + mutate( + job = paste( + id, + key, + start_time, + end_time, + sep = "_-_" + ) + ) %>% + select( + -c( + id, + key, + start_time, + end_time + ) + ) + + reactable( + data = processed_jobs, + selection = "single", + borderless = TRUE, + pagination = FALSE, + columns = list( + job = colDef( + cell = function(job_data) { + process_job_data(job_data) + } + ) + ) + ) + }) + + state$selected_job <- reactive({ + index <- getReactableState("job_list_table", "selected") + list( + "key" = job_list_data()[index, ]$key, + "id" = job_list_data()[index, ]$id + ) + }) + + }) +} diff --git a/app/view/mod_logs.R b/app/view/mod_logs.R new file mode 100644 index 0000000..cde7551 --- /dev/null +++ b/app/view/mod_logs.R @@ -0,0 +1,133 @@ +# nolint start: box_func_import_count_linter +box::use( + dplyr[mutate], + glue[glue], + magrittr[`%>%`], + reactable[ + colDef, + reactable, + reactableOutput, + renderReactable + ], + shinycssloaders[withSpinner], + shiny[ + div, + downloadButton, + downloadHandler, + icon, + moduleServer, + NS, + observeEvent, + reactive, + renderUI, + req, + uiOutput + ], +) +# nolint end + +box::use( + app/logic/api_utils[download_job_logs, get_job_logs], + app/logic/logs_utils[process_log_data], +) + +#' @export +ui <- function(id) { + ns <- NS(id) + div( + class = "logs-container", + uiOutput( + ns("download_logs") + ), + withSpinner( + reactableOutput( + ns("logs_table") + ), + type = 8, + color = "#333333" + ) + ) +} + +#' @export +server <- function(id, state) { + moduleServer(id, function(input, output, session) { + + ns <- session$ns + + output$download <- downloadHandler( + filename = function() { + glue( + "{state$selected_app()$name}_{state$selected_job()$id}.txt" + ) + }, + content = function(file) { + logs <- download_job_logs( + state$selected_app()$guid, + state$selected_job()$key + ) + writeLines(logs, file) + } + ) + + observeEvent(state$selected_job()$key, { + req(state$selected_job()$key) + output$download_logs <- renderUI({ + downloadButton( + outputId = ns("download"), + label = NULL, + icon = icon("download"), + class = "logs-download" + ) + }) + }) + + logs_data <- reactive({ + req(state$selected_job()$key) + get_job_logs( + state$selected_app()$guid, + state$selected_job()$key + ) + }) + + output$logs_table <- renderReactable({ + + processed_logs <- logs_data() %>% + mutate( + log_line = paste( + entries.source, + entries.timestamp, + entries.data, + sep = "_-_" + ) + ) + + reactable( + data = processed_logs, + searchable = TRUE, + borderless = TRUE, + pagination = FALSE, + defaultSortOrder = "desc", + defaultSorted = c("entries.timestamp"), + columns = list( + entries.source = colDef( + show = FALSE + ), + entries.timestamp = colDef( + show = FALSE + ), + entries.data = colDef( + show = FALSE + ), + log_line = colDef( + name = "Logs", + cell = function(log_data) { + process_log_data(log_data) + } + ) + ) + ) + }) + + }) +} diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..e829f27 --- /dev/null +++ b/config.yml @@ -0,0 +1,3 @@ +default: + rhino_log_level: !expr Sys.getenv("RHINO_LOG_LEVEL", "INFO") + rhino_log_file: !expr Sys.getenv("RHINO_LOG_FILE", NA) diff --git a/dependencies.R b/dependencies.R new file mode 100644 index 0000000..289f81d --- /dev/null +++ b/dependencies.R @@ -0,0 +1,8 @@ +# This file allows packrat (used by rsconnect during deployment) to pick up dependencies. +library(dplyr) +library(httr2) +library(magrittr) +library(reactable) +library(rhino) +library(rsconnect) +library(shinycssloaders) diff --git a/renv.lock b/renv.lock new file mode 100644 index 0000000..2c0c389 --- /dev/null +++ b/renv.lock @@ -0,0 +1,1264 @@ +{ + "R": { + "Version": "4.3.2", + "Repositories": [ + { + "Name": "CRAN", + "URL": "https://packagemanager.posit.co/cran/latest" + } + ] + }, + "Packages": { + "R.cache": { + "Package": "R.cache", + "Version": "0.16.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R.methodsS3", + "R.oo", + "R.utils", + "digest", + "utils" + ], + "Hash": "fe539ca3f8efb7410c3ae2cf5fe6c0f8" + }, + "R.methodsS3": { + "Package": "R.methodsS3", + "Version": "1.8.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "utils" + ], + "Hash": "278c286fd6e9e75d0c2e8f731ea445c8" + }, + "R.oo": { + "Package": "R.oo", + "Version": "1.25.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R.methodsS3", + "methods", + "utils" + ], + "Hash": "a0900a114f4f0194cf4aa8cd4a700681" + }, + "R.utils": { + "Package": "R.utils", + "Version": "2.12.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R.methodsS3", + "R.oo", + "methods", + "tools", + "utils" + ], + "Hash": "325f01db13da12c04d8f6e7be36ff514" + }, + "R6": { + "Package": "R6", + "Version": "2.5.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "470851b6d5d0ac559e9d01bb352b4021" + }, + "Rcpp": { + "Package": "Rcpp", + "Version": "1.0.11", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "methods", + "utils" + ], + "Hash": "ae6cbbe1492f4de79c45fce06f967ce8" + }, + "askpass": { + "Package": "askpass", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "sys" + ], + "Hash": "cad6cf7f1d5f6e906700b9d3e718c796" + }, + "backports": { + "Package": "backports", + "Version": "1.4.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "c39fbec8a30d23e721980b8afb31984c" + }, + "base64enc": { + "Package": "base64enc", + "Version": "0.1-3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "543776ae6848fde2f48ff3816d0628bc" + }, + "box": { + "Package": "box", + "Version": "1.1.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "tools" + ], + "Hash": "ce8187a260e8e3abc2294284badc3b76" + }, + "brio": { + "Package": "brio", + "Version": "1.1.3", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "976cf154dfb043c012d87cddd8bca363" + }, + "bslib": { + "Package": "bslib", + "Version": "0.5.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "base64enc", + "cachem", + "grDevices", + "htmltools", + "jquerylib", + "jsonlite", + "memoise", + "mime", + "rlang", + "sass" + ], + "Hash": "283015ddfbb9d7bf15ea9f0b5698f0d9" + }, + "cachem": { + "Package": "cachem", + "Version": "1.0.8", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "fastmap", + "rlang" + ], + "Hash": "c35768291560ce302c0a6589f92e837d" + }, + "callr": { + "Package": "callr", + "Version": "3.7.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "processx", + "utils" + ], + "Hash": "9b2191ede20fa29828139b9900922e51" + }, + "cli": { + "Package": "cli", + "Version": "3.6.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "utils" + ], + "Hash": "89e6d8219950eac806ae0c489052048a" + }, + "codetools": { + "Package": "codetools", + "Version": "0.2-19", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "c089a619a7fae175d149d89164f8c7d8" + }, + "commonmark": { + "Package": "commonmark", + "Version": "1.9.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "d691c61bff84bd63c383874d2d0c3307" + }, + "config": { + "Package": "config", + "Version": "0.3.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "yaml" + ], + "Hash": "8b7222e9d9eb5178eea545c0c4d33fc2" + }, + "crayon": { + "Package": "crayon", + "Version": "1.5.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "grDevices", + "methods", + "utils" + ], + "Hash": "e8a1e41acf02548751f45c718d55aa6a" + }, + "curl": { + "Package": "curl", + "Version": "5.2.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "ce88d13c0b10fe88a37d9c59dba2d7f9" + }, + "cyclocomp": { + "Package": "cyclocomp", + "Version": "1.1.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "callr", + "crayon", + "desc", + "remotes", + "withr" + ], + "Hash": "cdc4a473222b0112d4df0bcfbed12d44" + }, + "desc": { + "Package": "desc", + "Version": "1.4.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "cli", + "rprojroot", + "utils" + ], + "Hash": "6b9602c7ebbe87101a9c8edb6e8b6d21" + }, + "diffobj": { + "Package": "diffobj", + "Version": "0.3.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "crayon", + "methods", + "stats", + "tools", + "utils" + ], + "Hash": "bcaa8b95f8d7d01a5dedfd959ce88ab8" + }, + "digest": { + "Package": "digest", + "Version": "0.6.33", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "utils" + ], + "Hash": "b18a9cf3c003977b0cc49d5e76ebe48d" + }, + "dplyr": { + "Package": "dplyr", + "Version": "1.1.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "cli", + "generics", + "glue", + "lifecycle", + "magrittr", + "methods", + "pillar", + "rlang", + "tibble", + "tidyselect", + "utils", + "vctrs" + ], + "Hash": "fedd9d00c2944ff00a0e2696ccf048ec" + }, + "ellipsis": { + "Package": "ellipsis", + "Version": "0.3.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "rlang" + ], + "Hash": "bb0eec2fe32e88d9e2836c2f73ea2077" + }, + "evaluate": { + "Package": "evaluate", + "Version": "0.23", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "daf4a1246be12c1fa8c7705a0935c1a0" + }, + "fansi": { + "Package": "fansi", + "Version": "1.0.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "utils" + ], + "Hash": "3e8583a60163b4bc1a80016e63b9959e" + }, + "fastmap": { + "Package": "fastmap", + "Version": "1.1.1", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "f7736a18de97dea803bde0a2daaafb27" + }, + "fontawesome": { + "Package": "fontawesome", + "Version": "0.5.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "htmltools", + "rlang" + ], + "Hash": "c2efdd5f0bcd1ea861c2d4e2a883a67d" + }, + "fs": { + "Package": "fs", + "Version": "1.6.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "47b5f30c720c23999b913a1a635cf0bb" + }, + "generics": { + "Package": "generics", + "Version": "0.1.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "15e9634c0fcd294799e9b2e929ed1b86" + }, + "glue": { + "Package": "glue", + "Version": "1.6.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "4f2596dfb05dac67b9dc558e5c6fba2e" + }, + "highr": { + "Package": "highr", + "Version": "0.10", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "xfun" + ], + "Hash": "06230136b2d2b9ba5805e1963fa6e890" + }, + "htmltools": { + "Package": "htmltools", + "Version": "0.5.7", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "base64enc", + "digest", + "ellipsis", + "fastmap", + "grDevices", + "rlang", + "utils" + ], + "Hash": "2d7b3857980e0e0d0a1fd6f11928ab0f" + }, + "htmlwidgets": { + "Package": "htmlwidgets", + "Version": "1.6.4", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "grDevices", + "htmltools", + "jsonlite", + "knitr", + "rmarkdown", + "yaml" + ], + "Hash": "04291cc45198225444a397606810ac37" + }, + "httpuv": { + "Package": "httpuv", + "Version": "1.6.12", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "Rcpp", + "later", + "promises", + "utils" + ], + "Hash": "c992f75861325961c29a188b45e549f7" + }, + "httr2": { + "Package": "httr2", + "Version": "1.0.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "cli", + "curl", + "glue", + "lifecycle", + "magrittr", + "openssl", + "rappdirs", + "rlang", + "vctrs", + "withr" + ], + "Hash": "e2b30f1fc039a0bab047dd52bb20ef71" + }, + "jquerylib": { + "Package": "jquerylib", + "Version": "0.1.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "htmltools" + ], + "Hash": "5aab57a3bd297eee1c1d862735972182" + }, + "jsonlite": { + "Package": "jsonlite", + "Version": "1.8.7", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "methods" + ], + "Hash": "266a20443ca13c65688b2116d5220f76" + }, + "knitr": { + "Package": "knitr", + "Version": "1.45", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "evaluate", + "highr", + "methods", + "tools", + "xfun", + "yaml" + ], + "Hash": "1ec462871063897135c1bcbe0fc8f07d" + }, + "later": { + "Package": "later", + "Version": "1.3.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "Rcpp", + "rlang" + ], + "Hash": "40401c9cf2bc2259dfe83311c9384710" + }, + "lazyeval": { + "Package": "lazyeval", + "Version": "0.2.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "d908914ae53b04d4c0c0fd72ecc35370" + }, + "lifecycle": { + "Package": "lifecycle", + "Version": "1.0.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "glue", + "rlang" + ], + "Hash": "001cecbeac1cff9301bdc3775ee46a86" + }, + "lintr": { + "Package": "lintr", + "Version": "3.1.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "backports", + "codetools", + "cyclocomp", + "digest", + "glue", + "knitr", + "rex", + "stats", + "utils", + "xml2", + "xmlparsedata" + ], + "Hash": "2b4b803af6017e93b67ddaf0eacba918" + }, + "logger": { + "Package": "logger", + "Version": "0.2.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "utils" + ], + "Hash": "c269b06beb2bbadb0d058c0e6fa4ec3d" + }, + "magrittr": { + "Package": "magrittr", + "Version": "2.0.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "7ce2733a9826b3aeb1775d56fd305472" + }, + "memoise": { + "Package": "memoise", + "Version": "2.0.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "cachem", + "rlang" + ], + "Hash": "e2817ccf4a065c5d9d7f2cfbe7c1d78c" + }, + "mime": { + "Package": "mime", + "Version": "0.12", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "tools" + ], + "Hash": "18e9c28c1d3ca1560ce30658b22ce104" + }, + "openssl": { + "Package": "openssl", + "Version": "2.1.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "askpass" + ], + "Hash": "2a0dc8c6adfb6f032e4d4af82d258ab5" + }, + "packrat": { + "Package": "packrat", + "Version": "0.9.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "tools", + "utils" + ], + "Hash": "55ddd2d4a1959535f18393478b0c14a6" + }, + "pillar": { + "Package": "pillar", + "Version": "1.9.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "cli", + "fansi", + "glue", + "lifecycle", + "rlang", + "utf8", + "utils", + "vctrs" + ], + "Hash": "15da5a8412f317beeee6175fbc76f4bb" + }, + "pkgbuild": { + "Package": "pkgbuild", + "Version": "1.4.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "callr", + "cli", + "crayon", + "desc", + "prettyunits", + "processx", + "rprojroot" + ], + "Hash": "beb25b32a957a22a5c301a9e441190b3" + }, + "pkgconfig": { + "Package": "pkgconfig", + "Version": "2.0.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "utils" + ], + "Hash": "01f28d4278f15c76cddbea05899c5d6f" + }, + "pkgload": { + "Package": "pkgload", + "Version": "1.3.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "crayon", + "desc", + "fs", + "glue", + "methods", + "pkgbuild", + "rlang", + "rprojroot", + "utils", + "withr" + ], + "Hash": "903d68319ae9923fb2e2ee7fa8230b91" + }, + "praise": { + "Package": "praise", + "Version": "1.0.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "a555924add98c99d2f411e37e7d25e9f" + }, + "prettyunits": { + "Package": "prettyunits", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "6b01fc98b1e86c4f705ce9dcfd2f57c7" + }, + "processx": { + "Package": "processx", + "Version": "3.8.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "ps", + "utils" + ], + "Hash": "3efbd8ac1be0296a46c55387aeace0f3" + }, + "promises": { + "Package": "promises", + "Version": "1.2.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R6", + "Rcpp", + "fastmap", + "later", + "magrittr", + "rlang", + "stats" + ], + "Hash": "0d8a15c9d000970ada1ab21405387dee" + }, + "ps": { + "Package": "ps", + "Version": "1.7.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "utils" + ], + "Hash": "709d852d33178db54b17c722e5b1e594" + }, + "purrr": { + "Package": "purrr", + "Version": "1.0.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "lifecycle", + "magrittr", + "rlang", + "vctrs" + ], + "Hash": "1cba04a4e9414bdefc9dcaa99649a8dc" + }, + "rappdirs": { + "Package": "rappdirs", + "Version": "0.3.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "5e3c5dc0b071b21fa128676560dbe94d" + }, + "reactR": { + "Package": "reactR", + "Version": "0.5.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "htmltools" + ], + "Hash": "c9014fd1a435b2d790dd506589cb24e5" + }, + "reactable": { + "Package": "reactable", + "Version": "0.4.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "digest", + "htmltools", + "htmlwidgets", + "jsonlite", + "reactR" + ], + "Hash": "6069eb2a6597963eae0605c1875ff14c" + }, + "rematch2": { + "Package": "rematch2", + "Version": "2.1.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "tibble" + ], + "Hash": "76c9e04c712a05848ae7a23d2f170a40" + }, + "remotes": { + "Package": "remotes", + "Version": "2.4.2.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods", + "stats", + "tools", + "utils" + ], + "Hash": "63d15047eb239f95160112bcadc4fcb9" + }, + "renv": { + "Package": "renv", + "Version": "1.0.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "utils" + ], + "Hash": "41b847654f567341725473431dd0d5ab" + }, + "rex": { + "Package": "rex", + "Version": "1.2.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "lazyeval" + ], + "Hash": "ae34cd56890607370665bee5bd17812f" + }, + "rhino": { + "Package": "rhino", + "Version": "1.7.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "box", + "cli", + "config", + "fs", + "glue", + "lintr", + "logger", + "purrr", + "renv", + "rstudioapi", + "sass", + "shiny", + "styler", + "testthat", + "utils", + "withr", + "xml2", + "yaml" + ], + "Hash": "59ee79b26dd590b08dd1a3111d093832" + }, + "rlang": { + "Package": "rlang", + "Version": "1.1.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "utils" + ], + "Hash": "a85c767b55f0bf9b7ad16c6d7baee5bb" + }, + "rmarkdown": { + "Package": "rmarkdown", + "Version": "2.25", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "bslib", + "evaluate", + "fontawesome", + "htmltools", + "jquerylib", + "jsonlite", + "knitr", + "methods", + "stringr", + "tinytex", + "tools", + "utils", + "xfun", + "yaml" + ], + "Hash": "d65e35823c817f09f4de424fcdfa812a" + }, + "rprojroot": { + "Package": "rprojroot", + "Version": "2.0.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "1de7ab598047a87bba48434ba35d497d" + }, + "rsconnect": { + "Package": "rsconnect", + "Version": "1.2.1", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "curl", + "digest", + "jsonlite", + "lifecycle", + "openssl", + "packrat", + "renv", + "rlang", + "rstudioapi", + "tools", + "yaml" + ], + "Hash": "94bb3a2125b01b13dd2e4a784c2a9639" + }, + "rstudioapi": { + "Package": "rstudioapi", + "Version": "0.15.0", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "5564500e25cffad9e22244ced1379887" + }, + "sass": { + "Package": "sass", + "Version": "0.4.7", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R6", + "fs", + "htmltools", + "rappdirs", + "rlang" + ], + "Hash": "6bd4d33b50ff927191ec9acbf52fd056" + }, + "shiny": { + "Package": "shiny", + "Version": "1.7.5.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "bslib", + "cachem", + "commonmark", + "crayon", + "ellipsis", + "fastmap", + "fontawesome", + "glue", + "grDevices", + "htmltools", + "httpuv", + "jsonlite", + "later", + "lifecycle", + "methods", + "mime", + "promises", + "rlang", + "sourcetools", + "tools", + "utils", + "withr", + "xtable" + ], + "Hash": "5ec01cc255f2138fc2f0dc74d2b1a1a1" + }, + "shinycssloaders": { + "Package": "shinycssloaders", + "Version": "1.0.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "digest", + "glue", + "grDevices", + "shiny" + ], + "Hash": "f39bb3c44a9b496723ec7e86f9a771d8" + }, + "sourcetools": { + "Package": "sourcetools", + "Version": "0.1.7-1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "5f5a7629f956619d519205ec475fe647" + }, + "stringi": { + "Package": "stringi", + "Version": "1.8.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "stats", + "tools", + "utils" + ], + "Hash": "058aebddea264f4c99401515182e656a" + }, + "stringr": { + "Package": "stringr", + "Version": "1.5.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "glue", + "lifecycle", + "magrittr", + "rlang", + "stringi", + "vctrs" + ], + "Hash": "960e2ae9e09656611e0b8214ad543207" + }, + "styler": { + "Package": "styler", + "Version": "1.10.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R.cache", + "cli", + "magrittr", + "purrr", + "rlang", + "rprojroot", + "tools", + "vctrs", + "withr" + ], + "Hash": "d61238fd44fc63c8adf4565efe8eb682" + }, + "sys": { + "Package": "sys", + "Version": "3.4.2", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "3a1be13d68d47a8cd0bfd74739ca1555" + }, + "testthat": { + "Package": "testthat", + "Version": "3.2.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "R6", + "brio", + "callr", + "cli", + "desc", + "digest", + "ellipsis", + "evaluate", + "jsonlite", + "lifecycle", + "magrittr", + "methods", + "pkgload", + "praise", + "processx", + "ps", + "rlang", + "utils", + "waldo", + "withr" + ], + "Hash": "877508719fcb8c9525eccdadf07a5102" + }, + "tibble": { + "Package": "tibble", + "Version": "3.2.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "fansi", + "lifecycle", + "magrittr", + "methods", + "pillar", + "pkgconfig", + "rlang", + "utils", + "vctrs" + ], + "Hash": "a84e2cc86d07289b3b6f5069df7a004c" + }, + "tidyselect": { + "Package": "tidyselect", + "Version": "1.2.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "glue", + "lifecycle", + "rlang", + "vctrs", + "withr" + ], + "Hash": "79540e5fcd9e0435af547d885f184fd5" + }, + "tinytex": { + "Package": "tinytex", + "Version": "0.49", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "xfun" + ], + "Hash": "5ac22900ae0f386e54f1c307eca7d843" + }, + "utf8": { + "Package": "utf8", + "Version": "1.2.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "62b65c52671e6665f803ff02954446e9" + }, + "vctrs": { + "Package": "vctrs", + "Version": "0.6.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "glue", + "lifecycle", + "rlang" + ], + "Hash": "266c1ca411266ba8f365fcc726444b87" + }, + "waldo": { + "Package": "waldo", + "Version": "0.5.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "diffobj", + "fansi", + "glue", + "methods", + "rematch2", + "rlang", + "tibble" + ], + "Hash": "c7d3fd6d29ab077cbac8f0e2751449e6" + }, + "withr": { + "Package": "withr", + "Version": "2.5.2", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "grDevices", + "graphics", + "stats" + ], + "Hash": "4b25e70111b7d644322e9513f403a272" + }, + "xfun": { + "Package": "xfun", + "Version": "0.41", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "stats", + "tools" + ], + "Hash": "460a5e0fe46a80ef87424ad216028014" + }, + "xml2": { + "Package": "xml2", + "Version": "1.3.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "methods" + ], + "Hash": "6c40e5cfcc6aefd88110666e18c31f40" + }, + "xmlparsedata": { + "Package": "xmlparsedata", + "Version": "1.0.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R" + ], + "Hash": "45e4bf3c46476896e821fc0a408fb4fc" + }, + "xtable": { + "Package": "xtable", + "Version": "1.8-4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "stats", + "utils" + ], + "Hash": "b8acdf8af494d9ec19ccb2481a9b11c2" + }, + "yaml": { + "Package": "yaml", + "Version": "2.3.7", + "Source": "Repository", + "Repository": "CRAN", + "Hash": "0d0056cc5383fbc240ccd0cb584bf436" + } + } +} diff --git a/renv/.gitignore b/renv/.gitignore new file mode 100644 index 0000000..0ec0cbb --- /dev/null +++ b/renv/.gitignore @@ -0,0 +1,7 @@ +library/ +local/ +cellar/ +lock/ +python/ +sandbox/ +staging/ diff --git a/renv/activate.R b/renv/activate.R new file mode 100644 index 0000000..cb5401f --- /dev/null +++ b/renv/activate.R @@ -0,0 +1,1180 @@ + +local({ + + # the requested version of renv + version <- "1.0.3" + attr(version, "sha") <- NULL + + # the project directory + project <- getwd() + + # use start-up diagnostics if enabled + diagnostics <- Sys.getenv("RENV_STARTUP_DIAGNOSTICS", unset = "FALSE") + if (diagnostics) { + start <- Sys.time() + profile <- tempfile("renv-startup-", fileext = ".Rprof") + utils::Rprof(profile) + on.exit({ + utils::Rprof(NULL) + elapsed <- signif(difftime(Sys.time(), start, units = "auto"), digits = 2L) + writeLines(sprintf("- renv took %s to run the autoloader.", format(elapsed))) + writeLines(sprintf("- Profile: %s", profile)) + print(utils::summaryRprof(profile)) + }, add = TRUE) + } + + # figure out whether the autoloader is enabled + enabled <- local({ + + # first, check config option + override <- getOption("renv.config.autoloader.enabled") + if (!is.null(override)) + return(override) + + # next, check environment variables + # TODO: prefer using the configuration one in the future + envvars <- c( + "RENV_CONFIG_AUTOLOADER_ENABLED", + "RENV_AUTOLOADER_ENABLED", + "RENV_ACTIVATE_PROJECT" + ) + + for (envvar in envvars) { + envval <- Sys.getenv(envvar, unset = NA) + if (!is.na(envval)) + return(tolower(envval) %in% c("true", "t", "1")) + } + + # enable by default + TRUE + + }) + + if (!enabled) + return(FALSE) + + # avoid recursion + if (identical(getOption("renv.autoloader.running"), TRUE)) { + warning("ignoring recursive attempt to run renv autoloader") + return(invisible(TRUE)) + } + + # signal that we're loading renv during R startup + options(renv.autoloader.running = TRUE) + on.exit(options(renv.autoloader.running = NULL), add = TRUE) + + # signal that we've consented to use renv + options(renv.consent = TRUE) + + # load the 'utils' package eagerly -- this ensures that renv shims, which + # mask 'utils' packages, will come first on the search path + library(utils, lib.loc = .Library) + + # unload renv if it's already been loaded + if ("renv" %in% loadedNamespaces()) + unloadNamespace("renv") + + # load bootstrap tools + `%||%` <- function(x, y) { + if (is.null(x)) y else x + } + + catf <- function(fmt, ..., appendLF = TRUE) { + + quiet <- getOption("renv.bootstrap.quiet", default = FALSE) + if (quiet) + return(invisible()) + + msg <- sprintf(fmt, ...) + cat(msg, file = stdout(), sep = if (appendLF) "\n" else "") + + invisible(msg) + + } + + header <- function(label, + ..., + prefix = "#", + suffix = "-", + n = min(getOption("width"), 78)) + { + label <- sprintf(label, ...) + n <- max(n - nchar(label) - nchar(prefix) - 2L, 8L) + if (n <= 0) + return(paste(prefix, label)) + + tail <- paste(rep.int(suffix, n), collapse = "") + paste0(prefix, " ", label, " ", tail) + + } + + startswith <- function(string, prefix) { + substring(string, 1, nchar(prefix)) == prefix + } + + bootstrap <- function(version, library) { + + friendly <- renv_bootstrap_version_friendly(version) + section <- header(sprintf("Bootstrapping renv %s", friendly)) + catf(section) + + # attempt to download renv + catf("- Downloading renv ... ", appendLF = FALSE) + withCallingHandlers( + tarball <- renv_bootstrap_download(version), + error = function(err) { + catf("FAILED") + stop("failed to download:\n", conditionMessage(err)) + } + ) + catf("OK") + on.exit(unlink(tarball), add = TRUE) + + # now attempt to install + catf("- Installing renv ... ", appendLF = FALSE) + withCallingHandlers( + status <- renv_bootstrap_install(version, tarball, library), + error = function(err) { + catf("FAILED") + stop("failed to install:\n", conditionMessage(err)) + } + ) + catf("OK") + + # add empty line to break up bootstrapping from normal output + catf("") + + return(invisible()) + } + + renv_bootstrap_tests_running <- function() { + getOption("renv.tests.running", default = FALSE) + } + + renv_bootstrap_repos <- function() { + + # get CRAN repository + cran <- getOption("renv.repos.cran", "https://cloud.r-project.org") + + # check for repos override + repos <- Sys.getenv("RENV_CONFIG_REPOS_OVERRIDE", unset = NA) + if (!is.na(repos)) { + + # check for RSPM; if set, use a fallback repository for renv + rspm <- Sys.getenv("RSPM", unset = NA) + if (identical(rspm, repos)) + repos <- c(RSPM = rspm, CRAN = cran) + + return(repos) + + } + + # check for lockfile repositories + repos <- tryCatch(renv_bootstrap_repos_lockfile(), error = identity) + if (!inherits(repos, "error") && length(repos)) + return(repos) + + # retrieve current repos + repos <- getOption("repos") + + # ensure @CRAN@ entries are resolved + repos[repos == "@CRAN@"] <- cran + + # add in renv.bootstrap.repos if set + default <- c(FALLBACK = "https://cloud.r-project.org") + extra <- getOption("renv.bootstrap.repos", default = default) + repos <- c(repos, extra) + + # remove duplicates that might've snuck in + dupes <- duplicated(repos) | duplicated(names(repos)) + repos[!dupes] + + } + + renv_bootstrap_repos_lockfile <- function() { + + lockpath <- Sys.getenv("RENV_PATHS_LOCKFILE", unset = "renv.lock") + if (!file.exists(lockpath)) + return(NULL) + + lockfile <- tryCatch(renv_json_read(lockpath), error = identity) + if (inherits(lockfile, "error")) { + warning(lockfile) + return(NULL) + } + + repos <- lockfile$R$Repositories + if (length(repos) == 0) + return(NULL) + + keys <- vapply(repos, `[[`, "Name", FUN.VALUE = character(1)) + vals <- vapply(repos, `[[`, "URL", FUN.VALUE = character(1)) + names(vals) <- keys + + return(vals) + + } + + renv_bootstrap_download <- function(version) { + + sha <- attr(version, "sha", exact = TRUE) + + methods <- if (!is.null(sha)) { + + # attempting to bootstrap a development version of renv + c( + function() renv_bootstrap_download_tarball(sha), + function() renv_bootstrap_download_github(sha) + ) + + } else { + + # attempting to bootstrap a release version of renv + c( + function() renv_bootstrap_download_tarball(version), + function() renv_bootstrap_download_cran_latest(version), + function() renv_bootstrap_download_cran_archive(version) + ) + + } + + for (method in methods) { + path <- tryCatch(method(), error = identity) + if (is.character(path) && file.exists(path)) + return(path) + } + + stop("All download methods failed") + + } + + renv_bootstrap_download_impl <- function(url, destfile) { + + mode <- "wb" + + # https://bugs.r-project.org/bugzilla/show_bug.cgi?id=17715 + fixup <- + Sys.info()[["sysname"]] == "Windows" && + substring(url, 1L, 5L) == "file:" + + if (fixup) + mode <- "w+b" + + args <- list( + url = url, + destfile = destfile, + mode = mode, + quiet = TRUE + ) + + if ("headers" %in% names(formals(utils::download.file))) + args$headers <- renv_bootstrap_download_custom_headers(url) + + do.call(utils::download.file, args) + + } + + renv_bootstrap_download_custom_headers <- function(url) { + + headers <- getOption("renv.download.headers") + if (is.null(headers)) + return(character()) + + if (!is.function(headers)) + stopf("'renv.download.headers' is not a function") + + headers <- headers(url) + if (length(headers) == 0L) + return(character()) + + if (is.list(headers)) + headers <- unlist(headers, recursive = FALSE, use.names = TRUE) + + ok <- + is.character(headers) && + is.character(names(headers)) && + all(nzchar(names(headers))) + + if (!ok) + stop("invocation of 'renv.download.headers' did not return a named character vector") + + headers + + } + + renv_bootstrap_download_cran_latest <- function(version) { + + spec <- renv_bootstrap_download_cran_latest_find(version) + type <- spec$type + repos <- spec$repos + + baseurl <- utils::contrib.url(repos = repos, type = type) + ext <- if (identical(type, "source")) + ".tar.gz" + else if (Sys.info()[["sysname"]] == "Windows") + ".zip" + else + ".tgz" + name <- sprintf("renv_%s%s", version, ext) + url <- paste(baseurl, name, sep = "/") + + destfile <- file.path(tempdir(), name) + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (inherits(status, "condition")) + return(FALSE) + + # report success and return + destfile + + } + + renv_bootstrap_download_cran_latest_find <- function(version) { + + # check whether binaries are supported on this system + binary <- + getOption("renv.bootstrap.binary", default = TRUE) && + !identical(.Platform$pkgType, "source") && + !identical(getOption("pkgType"), "source") && + Sys.info()[["sysname"]] %in% c("Darwin", "Windows") + + types <- c(if (binary) "binary", "source") + + # iterate over types + repositories + for (type in types) { + for (repos in renv_bootstrap_repos()) { + + # retrieve package database + db <- tryCatch( + as.data.frame( + utils::available.packages(type = type, repos = repos), + stringsAsFactors = FALSE + ), + error = identity + ) + + if (inherits(db, "error")) + next + + # check for compatible entry + entry <- db[db$Package %in% "renv" & db$Version %in% version, ] + if (nrow(entry) == 0) + next + + # found it; return spec to caller + spec <- list(entry = entry, type = type, repos = repos) + return(spec) + + } + } + + # if we got here, we failed to find renv + fmt <- "renv %s is not available from your declared package repositories" + stop(sprintf(fmt, version)) + + } + + renv_bootstrap_download_cran_archive <- function(version) { + + name <- sprintf("renv_%s.tar.gz", version) + repos <- renv_bootstrap_repos() + urls <- file.path(repos, "src/contrib/Archive/renv", name) + destfile <- file.path(tempdir(), name) + + for (url in urls) { + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (identical(status, 0L)) + return(destfile) + + } + + return(FALSE) + + } + + renv_bootstrap_download_tarball <- function(version) { + + # if the user has provided the path to a tarball via + # an environment variable, then use it + tarball <- Sys.getenv("RENV_BOOTSTRAP_TARBALL", unset = NA) + if (is.na(tarball)) + return() + + # allow directories + if (dir.exists(tarball)) { + name <- sprintf("renv_%s.tar.gz", version) + tarball <- file.path(tarball, name) + } + + # bail if it doesn't exist + if (!file.exists(tarball)) { + + # let the user know we weren't able to honour their request + fmt <- "- RENV_BOOTSTRAP_TARBALL is set (%s) but does not exist." + msg <- sprintf(fmt, tarball) + warning(msg) + + # bail + return() + + } + + catf("- Using local tarball '%s'.", tarball) + tarball + + } + + renv_bootstrap_download_github <- function(version) { + + enabled <- Sys.getenv("RENV_BOOTSTRAP_FROM_GITHUB", unset = "TRUE") + if (!identical(enabled, "TRUE")) + return(FALSE) + + # prepare download options + pat <- Sys.getenv("GITHUB_PAT") + if (nzchar(Sys.which("curl")) && nzchar(pat)) { + fmt <- "--location --fail --header \"Authorization: token %s\"" + extra <- sprintf(fmt, pat) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "curl", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } else if (nzchar(Sys.which("wget")) && nzchar(pat)) { + fmt <- "--header=\"Authorization: token %s\"" + extra <- sprintf(fmt, pat) + saved <- options("download.file.method", "download.file.extra") + options(download.file.method = "wget", download.file.extra = extra) + on.exit(do.call(base::options, saved), add = TRUE) + } + + url <- file.path("https://api.github.com/repos/rstudio/renv/tarball", version) + name <- sprintf("renv_%s.tar.gz", version) + destfile <- file.path(tempdir(), name) + + status <- tryCatch( + renv_bootstrap_download_impl(url, destfile), + condition = identity + ) + + if (!identical(status, 0L)) + return(FALSE) + + renv_bootstrap_download_augment(destfile) + + return(destfile) + + } + + # Add Sha to DESCRIPTION. This is stop gap until #890, after which we + # can use renv::install() to fully capture metadata. + renv_bootstrap_download_augment <- function(destfile) { + sha <- renv_bootstrap_git_extract_sha1_tar(destfile) + if (is.null(sha)) { + return() + } + + # Untar + tempdir <- tempfile("renv-github-") + on.exit(unlink(tempdir, recursive = TRUE), add = TRUE) + untar(destfile, exdir = tempdir) + pkgdir <- dir(tempdir, full.names = TRUE)[[1]] + + # Modify description + desc_path <- file.path(pkgdir, "DESCRIPTION") + desc_lines <- readLines(desc_path) + remotes_fields <- c( + "RemoteType: github", + "RemoteHost: api.github.com", + "RemoteRepo: renv", + "RemoteUsername: rstudio", + "RemotePkgRef: rstudio/renv", + paste("RemoteRef: ", sha), + paste("RemoteSha: ", sha) + ) + writeLines(c(desc_lines[desc_lines != ""], remotes_fields), con = desc_path) + + # Re-tar + local({ + old <- setwd(tempdir) + on.exit(setwd(old), add = TRUE) + + tar(destfile, compression = "gzip") + }) + invisible() + } + + # Extract the commit hash from a git archive. Git archives include the SHA1 + # hash as the comment field of the tarball pax extended header + # (see https://www.kernel.org/pub/software/scm/git/docs/git-archive.html) + # For GitHub archives this should be the first header after the default one + # (512 byte) header. + renv_bootstrap_git_extract_sha1_tar <- function(bundle) { + + # open the bundle for reading + # We use gzcon for everything because (from ?gzcon) + # > Reading from a connection which does not supply a 'gzip' magic + # > header is equivalent to reading from the original connection + conn <- gzcon(file(bundle, open = "rb", raw = TRUE)) + on.exit(close(conn)) + + # The default pax header is 512 bytes long and the first pax extended header + # with the comment should be 51 bytes long + # `52 comment=` (11 chars) + 40 byte SHA1 hash + len <- 0x200 + 0x33 + res <- rawToChar(readBin(conn, "raw", n = len)[0x201:len]) + + if (grepl("^52 comment=", res)) { + sub("52 comment=", "", res) + } else { + NULL + } + } + + renv_bootstrap_install <- function(version, tarball, library) { + + # attempt to install it into project library + dir.create(library, showWarnings = FALSE, recursive = TRUE) + output <- renv_bootstrap_install_impl(library, tarball) + + # check for successful install + status <- attr(output, "status") + if (is.null(status) || identical(status, 0L)) + return(status) + + # an error occurred; report it + header <- "installation of renv failed" + lines <- paste(rep.int("=", nchar(header)), collapse = "") + text <- paste(c(header, lines, output), collapse = "\n") + stop(text) + + } + + renv_bootstrap_install_impl <- function(library, tarball) { + + # invoke using system2 so we can capture and report output + bin <- R.home("bin") + exe <- if (Sys.info()[["sysname"]] == "Windows") "R.exe" else "R" + R <- file.path(bin, exe) + + args <- c( + "--vanilla", "CMD", "INSTALL", "--no-multiarch", + "-l", shQuote(path.expand(library)), + shQuote(path.expand(tarball)) + ) + + system2(R, args, stdout = TRUE, stderr = TRUE) + + } + + renv_bootstrap_platform_prefix <- function() { + + # construct version prefix + version <- paste(R.version$major, R.version$minor, sep = ".") + prefix <- paste("R", numeric_version(version)[1, 1:2], sep = "-") + + # include SVN revision for development versions of R + # (to avoid sharing platform-specific artefacts with released versions of R) + devel <- + identical(R.version[["status"]], "Under development (unstable)") || + identical(R.version[["nickname"]], "Unsuffered Consequences") + + if (devel) + prefix <- paste(prefix, R.version[["svn rev"]], sep = "-r") + + # build list of path components + components <- c(prefix, R.version$platform) + + # include prefix if provided by user + prefix <- renv_bootstrap_platform_prefix_impl() + if (!is.na(prefix) && nzchar(prefix)) + components <- c(prefix, components) + + # build prefix + paste(components, collapse = "/") + + } + + renv_bootstrap_platform_prefix_impl <- function() { + + # if an explicit prefix has been supplied, use it + prefix <- Sys.getenv("RENV_PATHS_PREFIX", unset = NA) + if (!is.na(prefix)) + return(prefix) + + # if the user has requested an automatic prefix, generate it + auto <- Sys.getenv("RENV_PATHS_PREFIX_AUTO", unset = NA) + if (auto %in% c("TRUE", "True", "true", "1")) + return(renv_bootstrap_platform_prefix_auto()) + + # empty string on failure + "" + + } + + renv_bootstrap_platform_prefix_auto <- function() { + + prefix <- tryCatch(renv_bootstrap_platform_os(), error = identity) + if (inherits(prefix, "error") || prefix %in% "unknown") { + + msg <- paste( + "failed to infer current operating system", + "please file a bug report at https://github.com/rstudio/renv/issues", + sep = "; " + ) + + warning(msg) + + } + + prefix + + } + + renv_bootstrap_platform_os <- function() { + + sysinfo <- Sys.info() + sysname <- sysinfo[["sysname"]] + + # handle Windows + macOS up front + if (sysname == "Windows") + return("windows") + else if (sysname == "Darwin") + return("macos") + + # check for os-release files + for (file in c("/etc/os-release", "/usr/lib/os-release")) + if (file.exists(file)) + return(renv_bootstrap_platform_os_via_os_release(file, sysinfo)) + + # check for redhat-release files + if (file.exists("/etc/redhat-release")) + return(renv_bootstrap_platform_os_via_redhat_release()) + + "unknown" + + } + + renv_bootstrap_platform_os_via_os_release <- function(file, sysinfo) { + + # read /etc/os-release + release <- utils::read.table( + file = file, + sep = "=", + quote = c("\"", "'"), + col.names = c("Key", "Value"), + comment.char = "#", + stringsAsFactors = FALSE + ) + + vars <- as.list(release$Value) + names(vars) <- release$Key + + # get os name + os <- tolower(sysinfo[["sysname"]]) + + # read id + id <- "unknown" + for (field in c("ID", "ID_LIKE")) { + if (field %in% names(vars) && nzchar(vars[[field]])) { + id <- vars[[field]] + break + } + } + + # read version + version <- "unknown" + for (field in c("UBUNTU_CODENAME", "VERSION_CODENAME", "VERSION_ID", "BUILD_ID")) { + if (field %in% names(vars) && nzchar(vars[[field]])) { + version <- vars[[field]] + break + } + } + + # join together + paste(c(os, id, version), collapse = "-") + + } + + renv_bootstrap_platform_os_via_redhat_release <- function() { + + # read /etc/redhat-release + contents <- readLines("/etc/redhat-release", warn = FALSE) + + # infer id + id <- if (grepl("centos", contents, ignore.case = TRUE)) + "centos" + else if (grepl("redhat", contents, ignore.case = TRUE)) + "redhat" + else + "unknown" + + # try to find a version component (very hacky) + version <- "unknown" + + parts <- strsplit(contents, "[[:space:]]")[[1L]] + for (part in parts) { + + nv <- tryCatch(numeric_version(part), error = identity) + if (inherits(nv, "error")) + next + + version <- nv[1, 1] + break + + } + + paste(c("linux", id, version), collapse = "-") + + } + + renv_bootstrap_library_root_name <- function(project) { + + # use project name as-is if requested + asis <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT_ASIS", unset = "FALSE") + if (asis) + return(basename(project)) + + # otherwise, disambiguate based on project's path + id <- substring(renv_bootstrap_hash_text(project), 1L, 8L) + paste(basename(project), id, sep = "-") + + } + + renv_bootstrap_library_root <- function(project) { + + prefix <- renv_bootstrap_profile_prefix() + + path <- Sys.getenv("RENV_PATHS_LIBRARY", unset = NA) + if (!is.na(path)) + return(paste(c(path, prefix), collapse = "/")) + + path <- renv_bootstrap_library_root_impl(project) + if (!is.null(path)) { + name <- renv_bootstrap_library_root_name(project) + return(paste(c(path, prefix, name), collapse = "/")) + } + + renv_bootstrap_paths_renv("library", project = project) + + } + + renv_bootstrap_library_root_impl <- function(project) { + + root <- Sys.getenv("RENV_PATHS_LIBRARY_ROOT", unset = NA) + if (!is.na(root)) + return(root) + + type <- renv_bootstrap_project_type(project) + if (identical(type, "package")) { + userdir <- renv_bootstrap_user_dir() + return(file.path(userdir, "library")) + } + + } + + renv_bootstrap_validate_version <- function(version, description = NULL) { + + # resolve description file + # + # avoid passing lib.loc to `packageDescription()` below, since R will + # use the loaded version of the package by default anyhow. note that + # this function should only be called after 'renv' is loaded + # https://github.com/rstudio/renv/issues/1625 + description <- description %||% packageDescription("renv") + + # check whether requested version 'version' matches loaded version of renv + sha <- attr(version, "sha", exact = TRUE) + valid <- if (!is.null(sha)) + renv_bootstrap_validate_version_dev(sha, description) + else + renv_bootstrap_validate_version_release(version, description) + + if (valid) + return(TRUE) + + # the loaded version of renv doesn't match the requested version; + # give the user instructions on how to proceed + remote <- if (!is.null(description[["RemoteSha"]])) { + paste("rstudio/renv", description[["RemoteSha"]], sep = "@") + } else { + paste("renv", description[["Version"]], sep = "@") + } + + # display both loaded version + sha if available + friendly <- renv_bootstrap_version_friendly( + version = description[["Version"]], + sha = description[["RemoteSha"]] + ) + + fmt <- paste( + "renv %1$s was loaded from project library, but this project is configured to use renv %2$s.", + "- Use `renv::record(\"%3$s\")` to record renv %1$s in the lockfile.", + "- Use `renv::restore(packages = \"renv\")` to install renv %2$s into the project library.", + sep = "\n" + ) + catf(fmt, friendly, renv_bootstrap_version_friendly(version), remote) + + FALSE + + } + + renv_bootstrap_validate_version_dev <- function(version, description) { + expected <- description[["RemoteSha"]] + is.character(expected) && startswith(expected, version) + } + + renv_bootstrap_validate_version_release <- function(version, description) { + expected <- description[["Version"]] + is.character(expected) && identical(expected, version) + } + + renv_bootstrap_hash_text <- function(text) { + + hashfile <- tempfile("renv-hash-") + on.exit(unlink(hashfile), add = TRUE) + + writeLines(text, con = hashfile) + tools::md5sum(hashfile) + + } + + renv_bootstrap_load <- function(project, libpath, version) { + + # try to load renv from the project library + if (!requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) + return(FALSE) + + # warn if the version of renv loaded does not match + renv_bootstrap_validate_version(version) + + # execute renv load hooks, if any + hooks <- getHook("renv::autoload") + for (hook in hooks) + if (is.function(hook)) + tryCatch(hook(), error = warnify) + + # load the project + renv::load(project) + + TRUE + + } + + renv_bootstrap_profile_load <- function(project) { + + # if RENV_PROFILE is already set, just use that + profile <- Sys.getenv("RENV_PROFILE", unset = NA) + if (!is.na(profile) && nzchar(profile)) + return(profile) + + # check for a profile file (nothing to do if it doesn't exist) + path <- renv_bootstrap_paths_renv("profile", profile = FALSE, project = project) + if (!file.exists(path)) + return(NULL) + + # read the profile, and set it if it exists + contents <- readLines(path, warn = FALSE) + if (length(contents) == 0L) + return(NULL) + + # set RENV_PROFILE + profile <- contents[[1L]] + if (!profile %in% c("", "default")) + Sys.setenv(RENV_PROFILE = profile) + + profile + + } + + renv_bootstrap_profile_prefix <- function() { + profile <- renv_bootstrap_profile_get() + if (!is.null(profile)) + return(file.path("profiles", profile, "renv")) + } + + renv_bootstrap_profile_get <- function() { + profile <- Sys.getenv("RENV_PROFILE", unset = "") + renv_bootstrap_profile_normalize(profile) + } + + renv_bootstrap_profile_set <- function(profile) { + profile <- renv_bootstrap_profile_normalize(profile) + if (is.null(profile)) + Sys.unsetenv("RENV_PROFILE") + else + Sys.setenv(RENV_PROFILE = profile) + } + + renv_bootstrap_profile_normalize <- function(profile) { + + if (is.null(profile) || profile %in% c("", "default")) + return(NULL) + + profile + + } + + renv_bootstrap_path_absolute <- function(path) { + + substr(path, 1L, 1L) %in% c("~", "/", "\\") || ( + substr(path, 1L, 1L) %in% c(letters, LETTERS) && + substr(path, 2L, 3L) %in% c(":/", ":\\") + ) + + } + + renv_bootstrap_paths_renv <- function(..., profile = TRUE, project = NULL) { + renv <- Sys.getenv("RENV_PATHS_RENV", unset = "renv") + root <- if (renv_bootstrap_path_absolute(renv)) NULL else project + prefix <- if (profile) renv_bootstrap_profile_prefix() + components <- c(root, renv, prefix, ...) + paste(components, collapse = "/") + } + + renv_bootstrap_project_type <- function(path) { + + descpath <- file.path(path, "DESCRIPTION") + if (!file.exists(descpath)) + return("unknown") + + desc <- tryCatch( + read.dcf(descpath, all = TRUE), + error = identity + ) + + if (inherits(desc, "error")) + return("unknown") + + type <- desc$Type + if (!is.null(type)) + return(tolower(type)) + + package <- desc$Package + if (!is.null(package)) + return("package") + + "unknown" + + } + + renv_bootstrap_user_dir <- function() { + dir <- renv_bootstrap_user_dir_impl() + path.expand(chartr("\\", "/", dir)) + } + + renv_bootstrap_user_dir_impl <- function() { + + # use local override if set + override <- getOption("renv.userdir.override") + if (!is.null(override)) + return(override) + + # use R_user_dir if available + tools <- asNamespace("tools") + if (is.function(tools$R_user_dir)) + return(tools$R_user_dir("renv", "cache")) + + # try using our own backfill for older versions of R + envvars <- c("R_USER_CACHE_DIR", "XDG_CACHE_HOME") + for (envvar in envvars) { + root <- Sys.getenv(envvar, unset = NA) + if (!is.na(root)) + return(file.path(root, "R/renv")) + } + + # use platform-specific default fallbacks + if (Sys.info()[["sysname"]] == "Windows") + file.path(Sys.getenv("LOCALAPPDATA"), "R/cache/R/renv") + else if (Sys.info()[["sysname"]] == "Darwin") + "~/Library/Caches/org.R-project.R/R/renv" + else + "~/.cache/R/renv" + + } + + renv_bootstrap_version_friendly <- function(version, shafmt = NULL, sha = NULL) { + sha <- sha %||% attr(version, "sha", exact = TRUE) + parts <- c(version, sprintf(shafmt %||% " [sha: %s]", substring(sha, 1L, 7L))) + paste(parts, collapse = "") + } + + renv_bootstrap_exec <- function(project, libpath, version) { + if (!renv_bootstrap_load(project, libpath, version)) + renv_bootstrap_run(version, libpath) + } + + renv_bootstrap_run <- function(version, libpath) { + + # perform bootstrap + bootstrap(version, libpath) + + # exit early if we're just testing bootstrap + if (!is.na(Sys.getenv("RENV_BOOTSTRAP_INSTALL_ONLY", unset = NA))) + return(TRUE) + + # try again to load + if (requireNamespace("renv", lib.loc = libpath, quietly = TRUE)) { + return(renv::load(project = getwd())) + } + + # failed to download or load renv; warn the user + msg <- c( + "Failed to find an renv installation: the project will not be loaded.", + "Use `renv::activate()` to re-initialize the project." + ) + + warning(paste(msg, collapse = "\n"), call. = FALSE) + + } + + renv_json_read <- function(file = NULL, text = NULL) { + + jlerr <- NULL + + # if jsonlite is loaded, use that instead + if ("jsonlite" %in% loadedNamespaces()) { + + json <- catch(renv_json_read_jsonlite(file, text)) + if (!inherits(json, "error")) + return(json) + + jlerr <- json + + } + + # otherwise, fall back to the default JSON reader + json <- catch(renv_json_read_default(file, text)) + if (!inherits(json, "error")) + return(json) + + # report an error + if (!is.null(jlerr)) + stop(jlerr) + else + stop(json) + + } + + renv_json_read_jsonlite <- function(file = NULL, text = NULL) { + text <- paste(text %||% read(file), collapse = "\n") + jsonlite::fromJSON(txt = text, simplifyVector = FALSE) + } + + renv_json_read_default <- function(file = NULL, text = NULL) { + + # find strings in the JSON + text <- paste(text %||% read(file), collapse = "\n") + pattern <- '["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]' + locs <- gregexpr(pattern, text, perl = TRUE)[[1]] + + # if any are found, replace them with placeholders + replaced <- text + strings <- character() + replacements <- character() + + if (!identical(c(locs), -1L)) { + + # get the string values + starts <- locs + ends <- locs + attr(locs, "match.length") - 1L + strings <- substring(text, starts, ends) + + # only keep those requiring escaping + strings <- grep("[[\\]{}:]", strings, perl = TRUE, value = TRUE) + + # compute replacements + replacements <- sprintf('"\032%i\032"', seq_along(strings)) + + # replace the strings + mapply(function(string, replacement) { + replaced <<- sub(string, replacement, replaced, fixed = TRUE) + }, strings, replacements) + + } + + # transform the JSON into something the R parser understands + transformed <- replaced + transformed <- gsub("{}", "`names<-`(list(), character())", transformed, fixed = TRUE) + transformed <- gsub("[[{]", "list(", transformed, perl = TRUE) + transformed <- gsub("[]}]", ")", transformed, perl = TRUE) + transformed <- gsub(":", "=", transformed, fixed = TRUE) + text <- paste(transformed, collapse = "\n") + + # parse it + json <- parse(text = text, keep.source = FALSE, srcfile = NULL)[[1L]] + + # construct map between source strings, replaced strings + map <- as.character(parse(text = strings)) + names(map) <- as.character(parse(text = replacements)) + + # convert to list + map <- as.list(map) + + # remap strings in object + remapped <- renv_json_remap(json, map) + + # evaluate + eval(remapped, envir = baseenv()) + + } + + renv_json_remap <- function(json, map) { + + # fix names + if (!is.null(names(json))) { + lhs <- match(names(json), names(map), nomatch = 0L) + rhs <- match(names(map), names(json), nomatch = 0L) + names(json)[rhs] <- map[lhs] + } + + # fix values + if (is.character(json)) + return(map[[json]] %||% json) + + # handle true, false, null + if (is.name(json)) { + text <- as.character(json) + if (text == "true") + return(TRUE) + else if (text == "false") + return(FALSE) + else if (text == "null") + return(NULL) + } + + # recurse + if (is.recursive(json)) { + for (i in seq_along(json)) { + json[i] <- list(renv_json_remap(json[[i]], map)) + } + } + + json + + } + + # load the renv profile, if any + renv_bootstrap_profile_load(project) + + # construct path to library root + root <- renv_bootstrap_library_root(project) + + # construct library prefix for platform + prefix <- renv_bootstrap_platform_prefix() + + # construct full libpath + libpath <- file.path(root, prefix) + + # run bootstrap code + renv_bootstrap_exec(project, libpath, version) + + invisible() + +}) diff --git a/renv/settings.json b/renv/settings.json new file mode 100644 index 0000000..ffdbb32 --- /dev/null +++ b/renv/settings.json @@ -0,0 +1,19 @@ +{ + "bioconductor.version": null, + "external.libraries": [], + "ignored.packages": [], + "package.dependency.fields": [ + "Imports", + "Depends", + "LinkingTo" + ], + "ppm.enabled": null, + "ppm.ignored.urls": [], + "r.version": null, + "snapshot.type": "implicit", + "use.cache": true, + "vcs.ignore.cellar": true, + "vcs.ignore.library": true, + "vcs.ignore.local": true, + "vcs.manage.ignores": true +} diff --git a/rhino.yml b/rhino.yml new file mode 100644 index 0000000..fea1327 --- /dev/null +++ b/rhino.yml @@ -0,0 +1 @@ +sass: node diff --git a/strings.yml b/strings.yml new file mode 100644 index 0000000..d4c9b7c --- /dev/null +++ b/strings.yml @@ -0,0 +1,23 @@ +about: + intro: "Shiny Drug Flow helping pharmaceutical manufacturers to track the quality and performance of their manufactured batches" + about_project: "The main goal for the App Sprint was to create a life sciences dedicated application for visualizing the whole manufacturing process for a pharmaceutical ingredient that allows the end-users to identify whether or not a given batch falls within the quality standards." + dataset_info: "The app uses data shared in an article linked below. Laboratory.csv file downloaded from the Dataset link is used by API created in Rust for fetching the data." + missing_note: "Incoming Raw Materials and Manufacturing app views are not implemented yet." +references: + link1: + name: "Article" + link: "https://www.nature.com/articles/s41597-022-01203-x" + link2: + name: "Dataset" + link: "https://doi.org/10.6084/m9.figshare.c.5645578.v3" +powered_by: + rhino: + name: "Rhino" + link: "https://appsilon.github.io/rhino/" + img_name: "rhino.png" + desc: "Rhino is an Open-Source Package developed by Appsilon to + help the R community make more professional Shiny Apps. Rhino allows you to + create Shiny apps The Appsilon Way - like a fullstack software engineer. + Apply best software engineering practices, modularize your code, + test it well, make UI beautiful, and think about user adoption + from the very beginning." diff --git a/tests/cypress.config.js b/tests/cypress.config.js new file mode 100644 index 0000000..5c23de0 --- /dev/null +++ b/tests/cypress.config.js @@ -0,0 +1,7 @@ +module.exports = { + e2e: { + setupNodeEvents(on, config) {}, + baseUrl: 'http://localhost:3333', + supportFile: false, + }, +} diff --git a/tests/cypress/.gitignore b/tests/cypress/.gitignore new file mode 100644 index 0000000..e0cd7dc --- /dev/null +++ b/tests/cypress/.gitignore @@ -0,0 +1,2 @@ +/screenshots/ +/videos/ diff --git a/tests/cypress/e2e/app.cy.js b/tests/cypress/e2e/app.cy.js new file mode 100644 index 0000000..b9a58df --- /dev/null +++ b/tests/cypress/e2e/app.cy.js @@ -0,0 +1,7 @@ +describe('app', () => { + beforeEach(() => { + cy.visit('/') + }) + + it('starts', () => {}) +}) diff --git a/tests/testthat/test-main.R b/tests/testthat/test-main.R new file mode 100644 index 0000000..e69de29