diff --git a/.lintr b/.lintr index 63fade9..da7cb04 100644 --- a/.lintr +++ b/.lintr @@ -1,5 +1,5 @@ linters: linters_with_defaults( - line_length_linter = line_length_linter(100), - object_usage_linter = NULL # Does not work with `box::use()`. + defaults = box.linters::rhino_default_linters, + line_length_linter = line_length_linter(100) ) diff --git a/README.md b/README.md index aaea915..235a415 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,14 @@ The LogAnalyzer open-source app is a simple, plug and play application developed - I get `"Oops! Can't read apps from Posit Connect."` on the rightmost image? - This may mean that the Posit Connect API's response did not send proper data. - So far, one documented reason for this is that OAuth on Posit Connect instances may prevent the `/content` endpoint from sending app data. +- How do I rebrand the application? + - You can edit the branding in the `config.yml` file. You'll find the `colors` key which will build the CSS. + - The three status colors and their highlights e.g. `red` and `red-highlight` color the logs for you. + - The `primary` color is the primary theme for your application. This also affects SVGs. + - The blacks, whites and greys fill in the rest of the UI elements such as text, separator et al. +- How do I recolor the SVGs? + - This requires some creativity. We recommend replacing the primary color hex which you can find in the `.svg` file as `fill="#hexcde"` to `PRIMARY`. + - We use this as a default value in the function that replaces it but you are welcome to use another value and modify the function. # Credits diff --git a/app/logic/empty_state_utils.R b/app/logic/empty_state_utils.R index 3acbd10..5718197 100644 --- a/app/logic/empty_state_utils.R +++ b/app/logic/empty_state_utils.R @@ -1,19 +1,23 @@ box::use( + base64enc[ + base64encode + ], shiny[ div, img, p, - renderUI ], ) #' @description Function to generate an empty state UI #' @param text Text to display in the empty state #' @param image_path Path to the image to display in the empty state +#' @param color Color to use for the image #' @export generate_empty_state_ui <- function( text = "Select an application and a job to view logs", - image_path = "static/illustrations/empty_state.svg" + image_path = "static/illustrations/empty_state.svg", + color = "#0099f9" ) { div( class = "empty-state-container", @@ -21,10 +25,40 @@ generate_empty_state_ui <- function( class = "empty-state-text", text ), - img( - src = image_path, - class = "empty-state-image", + replace_svg_fill( + color = color, + svg_path = image_path, alt = text ) ) } + +#' Function to replace fill color in SVG +#' @param color Character. The color to replace +#' @param svg_path Character. The path to the SVG file +#' @param placeholder Character. The placeholder. Default is "PRIMARY" +#' @param alt Character. The alt text for the image +#' @return an image tag with the SVG content +replace_svg_fill <- function( + color, + svg_path = "", + placeholder = "PRIMARY", + alt = "", + class = "empty-state-image" +) { + svg_content <- readLines(svg_path) + svg_content <- paste(svg_content, collapse = "\n") + svg_content <- gsub( + placeholder, + color, + svg_content + ) + img( + class = class, + src = paste0( + "data:image/svg+xml;base64,", + base64encode(charToRaw(svg_content)) + ), + alt = alt + ) +} diff --git a/app/logic/general_utils.R b/app/logic/general_utils.R index 30e33bf..1f5fa47 100644 --- a/app/logic/general_utils.R +++ b/app/logic/general_utils.R @@ -1,3 +1,9 @@ +box::use( + purrr[ + map_chr + ], +) + #' Function to check if a string of log text has error keywords #' #' @param text Character. The log string @@ -37,3 +43,24 @@ format_timestamp <- function( format = to ) } + +#' Generate CSS variables from config.yml +#' @param config the config file +#' @return a string of CSS variables within :root {} +#' @export +generate_css_variables <- function( + config +) { + css_lines <- map_chr( + names(config$colors), + function(name) { + color_value <- config$colors[[name]] + sprintf(" --%s: %s;", name, color_value) + } + ) + paste0( + ":root {\n", + paste(css_lines, collapse = "\n"), + "\n}" + ) +} diff --git a/app/logic/llm_utils.R b/app/logic/llm_utils.R new file mode 100644 index 0000000..482f1bb --- /dev/null +++ b/app/logic/llm_utils.R @@ -0,0 +1,128 @@ +box::use( + config[ + get + ], + ellmer, #nolint: we do use this package just in a separate notation [[ ]] + glue[ + glue + ], +) + +#' Check if LLM is enabled +#' +#' This function checks the LLM configuration and returns TRUE if enabled, FALSE otherwise. +#' @return Logical indicating if LLM is enabled +#' @export +is_llm_enabled <- function() { + get("llm")$enabled %||% FALSE +} + +#' Get valid LLM providers +#' @return A character vector of valid LLM providers +get_valid_providers <- function( +) { + ellmer_functions <- ls(ellmer)[ + grepl( + pattern = "^chat_", + x = ls(ellmer) + ) + ] + sub( + pattern = "^chat_", + replacement = "", + x = ellmer_functions + ) +} + +#' Check if the provider is valid +#' @param provider The LLM provider to check +#' @param valid_providers A character vector of valid LLM providers +#' @return Logical indicating if the provider is valid +verify_provider <- function( + provider, + valid_providers = get_valid_providers() +) { + if (!provider %in% valid_providers) { + stop( + glue( + "Invalid LLM provider '{provider}'. ", + "Valid providers are: {paste(valid_providers, collapse = ', ')}" + ) + ) + } + TRUE +} + +#' Get LLM configuration +#' +#' Returns the LLM configuration if LLM is enabled. Otherwise, throws an error. +#' @return A list containing the LLM configuration +get_llm_config <- function() { + if (!is_llm_enabled()) { + stop("Oops! LLM is not enabled in config.yml!") + } + verify_provider( + get("llm")$provider, + get_valid_providers() + ) + get("llm") +} + +#' Get the LLM function based on the provider +#' +#' Extracts the chat function dynamically from the `ellmer` module. +#' @param llm_config Optional configuration for the LLM +#' @return A function to create a chat object +get_llm_function <- function( + llm_config = get_llm_config() +) { + ellmer[[glue("chat_{llm_config$provider}")]] +} + +#' Create a chat object +#' +#' Uses the configured LLM provider and model to create a chat object. +#' @param llm_config Optional configuration for the LLM +#' @return A chat object +#' @export +create_chat_object <- function( + llm_config = get_llm_config() +) { + fun <- get_llm_function(llm_config) + fun( + api_key = llm_config$api_key, + model = llm_config$model, + system_prompt = llm_config$system_prompt, + seed = 42, + api_args = list( + temperature = 0 + ) + ) +} + +#' Invoke LLM help with the logs data.frame +#' @param logs_data A data frame containing log data +#' @return A response from the LLM +#' @export +get_llm_help <- function( + logs_data +) { + chat <- create_chat_object() + chat$chat( + concatenate_logs( + logs_data + ) + ) +} + +#' Concatenate logs +#' @param processed_logs A data frame containing log data +#' @return A string with concatenated log entries +concatenate_logs <- function( + processed_logs +) { + paste( + processed_logs$entries.data, + collapse = "\n" + ) +} diff --git a/app/logic/logs_utils.R b/app/logic/logs_utils.R index ebbec6e..c5aae50 100644 --- a/app/logic/logs_utils.R +++ b/app/logic/logs_utils.R @@ -18,19 +18,18 @@ 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"), + class = glue("log-entry {log_info[4]}-highlight"), icon( - name = status[2], + name = log_info[5], class = glue( - "log-status {status[1]}-text fa-solid" + "log-status {log_info[4]}-text fa-solid" ), ), div( class = "log-info-block", div( - class = glue("log-info {status[1]}-text"), + class = glue("log-info {log_info[4]}-text"), log_info[3] ), div( @@ -41,15 +40,18 @@ process_log_data <- function( ) } +#' @export get_status_info <- function( output_type, log_data ) { if (output_type == "stdout") { - c("green", "circle-info") + status_list <- list("green", "circle-info") } else if (output_type == "stderr" && check_text_error(log_data)) { - c("red", "circle-xmark") + status_list <- list("red", "circle-xmark") } else { - c("yellow", "circle-info") + status_list <- list("yellow", "circle-info") } + names(status_list) <- c("entries.status", "entries.icon") + status_list } diff --git a/app/main.R b/app/main.R index 4adf1dd..62a0121 100644 --- a/app/main.R +++ b/app/main.R @@ -1,62 +1,52 @@ -# 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], + config[get], + shiny, ) -# nolint end box::use( app/logic/api_utils[get_app_list], app/logic/empty_state_utils[generate_empty_state_ui], + app/logic/general_utils[generate_css_variables], app/view/mod_app_table, app/view/mod_header, app/view/mod_job_list, app/view/mod_logs, ) +# Load Branding +branding <- get("branding") +branding_css_variables <- generate_css_variables( + branding +) + #' @export ui <- function(id) { - ns <- NS(id) - fluidPage( + ns <- shiny$NS(id) + shiny$fluidPage( class = "dashboard-body", + shiny$tags$head( + shiny$tags$style(shiny$HTML(branding_css_variables)) + ), mod_header$ui("header"), - div( + shiny$div( class = "dashboard-container", - div( + shiny$div( class = "app-table", mod_app_table$ui(ns("app_table")) ), - div( + shiny$div( class = "vertical-line" ), - div( + shiny$div( class = "job-list", - uiOutput(ns("job_list_pane")) + shiny$uiOutput(ns("job_list_pane")) ), - div( + shiny$div( class = "vertical-line" ), - div( + shiny$div( class = "logs", - uiOutput(ns("logs_pane")) + shiny$uiOutput(ns("logs_pane")) ) ) ) @@ -64,80 +54,57 @@ ui <- function(id) { #' @export server <- function(id) { - moduleServer(id, function(input, output, session) { + shiny$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 <- get_app_list() - app_list <- reactive({ - get_app_list() - }) + mod_header$server("header") - mod_app_table$server( + selected_app_ <- mod_app_table$server( "app_table", - app_list(), - state + app_list + )$selected_app_ + + selected_job_ <- mod_job_list$server( + "job_list", + selected_app_ + )$selected_job_ + + mod_logs$server( + "logs", + selected_app_, + selected_job_ ) - observeEvent(state$selected_app()$guid, { - - if (isTruthy(state$selected_app()$guid)) { + output$job_list_pane <- shiny$renderUI({ + if (!shiny$isTruthy(selected_app_()$guid)) { + NULL + } - output$job_list_pane <- renderUI({ - mod_job_list$ui(ns("job_list")) - }) + mod_job_list$ui(ns("job_list")) + }) - mod_job_list$server( - "job_list", - state + output$logs_pane <- shiny$renderUI({ + if (!is.data.frame(app_list) || nrow(app_list) == 0) { + generate_empty_state_ui( + text = "Oops! Can't read apps from Posit Connect.", + image_path = "app/static/illustrations/missing_apps.svg", + color = branding$colors$primary ) - - } else { - - removeUI(ns("job_list_pane")) - } - }, ignoreNULL = FALSE) - 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 + if (!shiny$isTruthy(selected_job_()$key)) { + generate_empty_state_ui( + text = "Select an application and a job to view logs.", + image_path = "app/static/illustrations/empty_state.svg", + color = branding$colors$primary ) - } else { - - if (!inherits(app_list(), "data.frame")) { - empty_state <- renderUI({ - generate_empty_state_ui( - text = "Oops! Can't read apps from Posit Connect.", - image_path = "static/illustrations/missing_apps.svg" - ) - }) - } else { - empty_state <- renderUI({ - generate_empty_state_ui( - text = "Select an application and a job to view logs.", - image_path = "static/illustrations/empty_state.svg" - ) - }) - } - - output$logs_pane <- empty_state } - }, ignoreNULL = FALSE) + mod_logs$ui(ns("logs")) + }) }) } diff --git a/app/static/css/app.min.css b/app/static/css/app.min.css index 1060d16..fca6d13 100644 --- a/app/static/css/app.min.css +++ b/app/static/css/app.min.css @@ -1 +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}body{overflow:hidden}.vertical-line{border-left:1px #eee solid;height:80%;align-self:center} +@import "https://fonts.googleapis.com/css2?family=Maven+Pro:wght@400;600&display=swap";.red-text{color:var(--red)}.green-text{color:var(--green)}.yellow-text{color:var(--yellow)}.red-highlight{background-color:var(--red-highlight)}.green-highlight{background-color:var(--green-highlight)}.yellow-highlight{background-color:var(--yellow-highlight)}.red-text{color:var(--red)}.green-text{color:var(--green)}.yellow-text{color:var(--yellow)}.red-highlight{background-color:var(--red-highlight)}.green-highlight{background-color:var(--green-highlight)}.yellow-highlight{background-color:var(--yellow-highlight)}.logs .rt-td-inner{padding:0 !important}.logs .rt-search{width:85% !important;align-self:flex-start !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:var(--grey-text);margin-bottom:40px}.logs-llm-placeholder img{width:50%}.logs-llm-modal .modal-dialog{text-align:justify}.logs-llm-modal .modal-dialog h2,.logs-llm-modal .modal-dialog h3{color:var(--primary)}.logs-llm-modal .modal-dialog .btn{background:var(--red-highlight);border-radius:0;border:1px solid var(--red)}.logs-llm-modal .modal-dialog .modal-content{border-radius:0}.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:0.75em}.logs-container .logs-options{position:absolute;z-index:2;right:0;margin:10px}.logs-container .logs-options .logs-options-button{background:0;border-radius:0;padding:5px 10px}.wrapper{background:none !important}.content-wrapper{background:var(--white);color:var(--black-text);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:transparent}.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:var(--selected-row)}.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:var(--white);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:0.5em;margin-left:10px;margin-bottom:10px}.app-entry .app-metadata{display:flex;flex-direction:column;gap:5px;color:var(--grey-text);font-size:0.75em}.red-text{color:var(--red)}.green-text{color:var(--green)}.yellow-text{color:var(--yellow)}.red-highlight{background-color:var(--red-highlight)}.green-highlight{background-color:var(--green-highlight)}.yellow-highlight{background-color:var(--yellow-highlight)}.job-entry .job-key,.job-entry .job-start-time,.job-entry .job-end-time{font-size:0.75em;color:var(--grey-text)}.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:var(--primary);color:white;padding:10px;border-radius:10px;margin:0 10px}*{font-family:"Maven Pro", sans-serif}body{overflow:hidden}.vertical-line{border-left:1px var(--grey2-border) solid;height:80%;align-self:center} diff --git a/app/static/illustrations/empty_state.svg b/app/static/illustrations/empty_state.svg index f64d1ee..722a490 100644 --- a/app/static/illustrations/empty_state.svg +++ b/app/static/illustrations/empty_state.svg @@ -7,7 +7,7 @@ - + @@ -16,7 +16,7 @@ - + diff --git a/app/static/illustrations/missing_apps.svg b/app/static/illustrations/missing_apps.svg index df10112..65e230a 100644 --- a/app/static/illustrations/missing_apps.svg +++ b/app/static/illustrations/missing_apps.svg @@ -1 +1 @@ -server down \ No newline at end of file +server down diff --git a/app/static/llm_system_prompt.txt b/app/static/llm_system_prompt.txt new file mode 100644 index 0000000..f191670 --- /dev/null +++ b/app/static/llm_system_prompt.txt @@ -0,0 +1,17 @@ +Context: You are an extremely sophisticated, R/Shiny log analyzer and debugger. +Task: You will be given only the problematic logs. Your job is to analyse them. +Result: Generate a div with formatting as given below. +Structure: +-

LogAnalyzer AI Help

+-
+-

Caution: This help is AI generated. Verify the information before taking any action.

+-

Problem Explanation

+- [Add a one- or two-liner explanation here] +-

Suggestions (bullet list of 3 suggestions, be detailed, but no subpoints) +Formatting: +This will be embedded in a modalDialog in a Shiny app. Following this to the last letter is important. Breaking these will get you fired. +- Ensure function, library and related names use tags. +- All code blocks and one-liners should be formatted properly using tags. +- All emphasis should happen with and not _ or *. +- All bold should happen with and not ** or __. +- Return raw HTML. No \```html wrapper. diff --git a/app/static/appsilon-logo.png b/app/static/logo.png similarity index 100% rename from app/static/appsilon-logo.png rename to app/static/logo.png diff --git a/app/styles/_app_table.scss b/app/styles/_app_table.scss index 1904be5..676d8d1 100644 --- a/app/styles/_app_table.scss +++ b/app/styles/_app_table.scss @@ -17,7 +17,7 @@ display: flex; flex-direction: column; gap: 5px; - color: $grey-text; + color: var(--grey-text); font-size: 0.75em; } } diff --git a/app/styles/_colors.scss b/app/styles/_colors.scss index 9c7f1cb..0be7e97 100644 --- a/app/styles/_colors.scss +++ b/app/styles/_colors.scss @@ -1,40 +1,23 @@ -$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; + color: var(--red); } .green-text { - color: $green; + color: var(--green); } .yellow-text { - color: $yellow; + color: var(--yellow); } .red-highlight { - background-color: $red-highlight; + background-color: var(--red-highlight); } .green-highlight { - background-color: $green-highlight; + background-color: var(--green-highlight); } .yellow-highlight { - background-color: $yellow-highlight; + background-color: var(--yellow-highlight); } diff --git a/app/styles/_dashboard.scss b/app/styles/_dashboard.scss index c99af6a..69a4270 100644 --- a/app/styles/_dashboard.scss +++ b/app/styles/_dashboard.scss @@ -3,8 +3,8 @@ } .content-wrapper { - background: $white; - color: $black-text; + background: var(--white); + color: var(--black-text); height: 90vh; } @@ -40,7 +40,7 @@ } .rt-tr-selected { - background: $selected-row; + background: var(--selected-row); } .app-table { @@ -56,7 +56,7 @@ } .logs { - background: $white; + background: var(--white); width: 55%; height: 100%; overflow-y: auto; diff --git a/app/styles/_header.scss b/app/styles/_header.scss index b959d4e..15e5459 100644 --- a/app/styles/_header.scss +++ b/app/styles/_header.scss @@ -30,7 +30,7 @@ .right { .cta-button { - background: $appsilon-blue; + background: var(--primary); color: white; padding: 10px; border-radius: 10px; diff --git a/app/styles/_job_list.scss b/app/styles/_job_list.scss index 1ca3cef..63ed958 100644 --- a/app/styles/_job_list.scss +++ b/app/styles/_job_list.scss @@ -5,6 +5,6 @@ .job-start-time, .job-end-time { font-size: 0.75em; - color: $grey-text; + color: var(--grey-text); } } diff --git a/app/styles/_logs.scss b/app/styles/_logs.scss index 5f4535e..1532853 100644 --- a/app/styles/_logs.scss +++ b/app/styles/_logs.scss @@ -5,6 +5,11 @@ padding: 0 !important; } + .rt-search { + width: 85% !important; + align-self: flex-start !important; + } + > div { text-align: center; @@ -16,13 +21,34 @@ } .empty-state-text { - color: $grey-text; + color: var(--grey-text); margin-bottom: 40px; } } } } +.logs-llm-modal { + .modal-dialog { + text-align: justify; + + h2, + h3 { + color: var(--primary); + } + + .btn { + background: var(--red-highlight); + border-radius: 0; + border: 1px solid var(--red); + } + + .modal-content { + border-radius: 0; + } + } +} + .logs-container { position: relative; @@ -52,13 +78,16 @@ } } - .logs-download { + .logs-options { position: absolute; z-index: 2; right: 0; margin: 10px; - background: 0; - border-radius: 0; - padding: 5px 10px; + + .logs-options-button { + background: 0; + border-radius: 0; + padding: 5px 10px; + } } } diff --git a/app/styles/main.scss b/app/styles/main.scss index 17d49ff..d2497f6 100644 --- a/app/styles/main.scss +++ b/app/styles/main.scss @@ -15,7 +15,7 @@ body { } .vertical-line { - border-left: 1px $grey2-border solid; + border-left: 1px var(--grey2-border) solid; height: 80%; align-self: center; } diff --git a/app/view/mod_app_table.R b/app/view/mod_app_table.R index 52c6e0c..181251b 100644 --- a/app/view/mod_app_table.R +++ b/app/view/mod_app_table.R @@ -38,12 +38,12 @@ ui <- function(id) { } #' @export -server <- function(id, app_list, state) { +server <- function(id, app_list) { moduleServer(id, function(input, output, session) { output$app_table <- renderReactable({ - if (length(app_list) > 0 && inherits(app_list, "data.frame")) { + if (nrow(app_list) > 0 && inherits(app_list, "data.frame")) { processed_apps <- app_list %>% select( guid, @@ -80,6 +80,7 @@ server <- function(id, app_list, state) { searchable = TRUE, borderless = TRUE, pagination = FALSE, + onClick = "select", selection = "single", columns = list( guid = colDef( @@ -98,15 +99,17 @@ server <- function(id, app_list, state) { ) }) - state$selected_app <- reactive({ - index <- getReactableState("app_table", "selected") - if (isTruthy(index) && length(app_list > 0)) { - list( - "guid" = app_list[index, ]$guid, - "name" = app_list[index, ]$name - ) - } - }) + list( + selected_app_ = reactive({ + index <- getReactableState("app_table", "selected") + if (isTruthy(index) && nrow(app_list) > 0) { + 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 index e94e1d0..ae5891e 100644 --- a/app/view/mod_header.R +++ b/app/view/mod_header.R @@ -1,5 +1,9 @@ box::use( + config[ + get + ], shiny[ + a, actionLink, div, h2, @@ -12,14 +16,18 @@ box::use( #' @export ui <- function(id) { ns <- NS(id) + branding <- get("branding") div( class = "header", div( class = "left header-section", - img( - src = "static/appsilon-logo.png", - alt = "Appsilon logo", - href = "https://demo.appsilon.com" + a( + img( + src = branding$logo$src, + alt = branding$logo$alt + ), + href = branding$logo$href, + target = "_blank" ), div( class = "vertical-line" @@ -31,7 +39,7 @@ ui <- function(id) { div( class = "right header-section", actionLink( - "lets-talk", + ns("lets-talk"), label = "Let's Talk", class = "cta-button", onclick = "window.open('https://appsilon.com/#contact', '_blank');" diff --git a/app/view/mod_job_list.R b/app/view/mod_job_list.R index c4a6fbf..c1cc2e4 100644 --- a/app/view/mod_job_list.R +++ b/app/view/mod_job_list.R @@ -1,5 +1,4 @@ box::use( - magrittr[`%>%`], reactable[ colDef, getReactableState, @@ -39,12 +38,12 @@ ui <- function(id) { } #' @export -server <- function(id, state) { +server <- function(id, selected_app_) { moduleServer(id, function(input, output, session) { job_list_data <- reactive({ - req(state$selected_app()$guid) - get_job_list(state$selected_app()$guid) + req(selected_app_()$guid) + get_job_list(selected_app_()$guid) }) output$job_list_table <- renderReactable({ @@ -58,6 +57,7 @@ server <- function(id, state) { selection = "single", borderless = TRUE, pagination = FALSE, + onClick = "select", columns = list( job = colDef( cell = function(job_data) { @@ -72,15 +72,17 @@ server <- function(id, state) { }) - state$selected_job <- reactive({ - index <- getReactableState("job_list_table", "selected") - if (isTruthy(index) && length(job_list_data()) > 0) { - list( - "key" = job_list_data()[index, ]$key, - "id" = job_list_data()[index, ]$id - ) - } - }) + list( + selected_job_ = reactive({ + index <- getReactableState("job_list_table", "selected") + if (isTruthy(index) && nrow(job_list_data()) > 0) { + 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 index cde7551..4644574 100644 --- a/app/view/mod_logs.R +++ b/app/view/mod_logs.R @@ -1,42 +1,47 @@ -# nolint start: box_func_import_count_linter box::use( - dplyr[mutate], + dplyr[ + as_tibble, + bind_cols, + filter, + mutate, + tibble + ], glue[glue], magrittr[`%>%`], + purrr[ + pmap_dfr + ], reactable[ colDef, reactable, reactableOutput, renderReactable ], + shiny, 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], + app/logic/api_utils[ + download_job_logs, + get_job_logs + ], + app/logic/llm_utils[ + get_llm_help, + is_llm_enabled + ], + app/logic/logs_utils[ + get_status_info, + process_log_data + ], ) #' @export ui <- function(id) { - ns <- NS(id) - div( + ns <- shiny$NS(id) + shiny$div( class = "logs-container", - uiOutput( + shiny$uiOutput( ns("download_logs") ), withSpinner( @@ -50,60 +55,92 @@ ui <- function(id) { } #' @export -server <- function(id, state) { - moduleServer(id, function(input, output, session) { +server <- function( + id, + selected_app_, + selected_job_ +) { + shiny$moduleServer(id, function(input, output, session) { ns <- session$ns - output$download <- downloadHandler( + output$download <- shiny$downloadHandler( filename = function() { glue( - "{state$selected_app()$name}_{state$selected_job()$id}.txt" + "{selected_app_()$name}_{selected_job_()$id}.txt" ) }, content = function(file) { logs <- download_job_logs( - state$selected_app()$guid, - state$selected_job()$key + selected_app_()$guid, + selected_job_()$key ) writeLines(logs, file) } ) - observeEvent(state$selected_job()$key, { - req(state$selected_job()$key) - output$download_logs <- renderUI({ - downloadButton( + output$download_logs <- shiny$renderUI({ + if (is.null(selected_job_()$key)) { + NULL + } + + shiny$div( + class = "logs-options", + if (is_llm_enabled()) { + shiny$actionButton( + inputId = ns("llm"), + label = NULL, + icon = shiny$icon("robot"), + class = "llm logs-options-button" + ) + }, + shiny$downloadButton( outputId = ns("download"), label = NULL, - icon = icon("download"), - class = "logs-download" + icon = shiny$icon("download"), + class = "download logs-options-button" ) - }) + ) }) - logs_data <- reactive({ - req(state$selected_job()$key) + logs_data <- shiny$reactive({ + shiny$req(selected_job_()$key) get_job_logs( - state$selected_app()$guid, - state$selected_job()$key + selected_app_()$guid, + selected_job_()$key ) }) - output$logs_table <- renderReactable({ - - processed_logs <- logs_data() %>% + processed_logs <- shiny$reactive({ + logs_data() %>% + pmap_dfr( + ~ { + get_status_info(..1, ..3) |> + as_tibble() |> + bind_cols( + tibble( + entries.source = ..1, + entries.timestamp = ..2, + entries.data = ..3 + ) + ) + } + ) %>% mutate( log_line = paste( entries.source, entries.timestamp, entries.data, + entries.status, + entries.icon, sep = "_-_" ) ) + }) + output$logs_table <- renderReactable({ reactable( - data = processed_logs, + data = processed_logs(), searchable = TRUE, borderless = TRUE, pagination = FALSE, @@ -119,6 +156,12 @@ server <- function(id, state) { entries.data = colDef( show = FALSE ), + entries.status = colDef( + show = FALSE + ), + entries.icon = colDef( + show = FALSE + ), log_line = colDef( name = "Logs", cell = function(log_data) { @@ -129,5 +172,34 @@ server <- function(id, state) { ) }) + if (is_llm_enabled()) { + llm_result <- shiny$eventReactive(input$llm, { + shiny$req(processed_logs()) + get_llm_help( + processed_logs() %>% + filter( + entries.status %in% c("red", "yellow") + ) + ) + }) + + shiny$observeEvent(llm_result(), { + shiny$removeModal() + shiny$showModal( + shiny$modalDialog( + easyClose = TRUE, + size = "m", + footer = shiny$modalButton( + shiny$icon( + "xmark", + class = "red-text" + ) + ), + shiny$HTML(llm_result()) + ) %>% + shiny$tagAppendAttributes(class = "logs-llm-modal") + ) + }) + } }) } diff --git a/config.yml b/config.yml index 8e5f085..462f155 100644 --- a/config.yml +++ b/config.yml @@ -1,5 +1,32 @@ default: rhino_log_level: !expr Sys.getenv("RHINO_LOG_LEVEL", "INFO") rhino_log_file: !expr Sys.getenv("RHINO_LOG_FILE", NA) + llm: + enabled: FALSE + provider: "openai" + model: "gpt-4o" + api_key: !expr Sys.getenv("LLM_API_KEY", NA) + system_prompt: !expr readLines("app/static/llm_system_prompt.txt") |> paste(collapse = " ") app_role: "owner" - + branding: + logo: + src: "static/logo.png" + alt: "Appsilon logo" + href: "https://demo.appsilon.com" + colors: + primary: "#0099f9" + 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" diff --git a/dependencies.R b/dependencies.R index 289f81d..a2db989 100644 --- a/dependencies.R +++ b/dependencies.R @@ -1,8 +1,11 @@ # This file allows packrat (used by rsconnect during deployment) to pick up dependencies. library(dplyr) +library(ellmer) library(httr2) library(magrittr) library(reactable) library(rhino) library(rsconnect) library(shinycssloaders) +library(treesitter) +library(treesitter.r) diff --git a/renv.lock b/renv.lock index 2c0c389..ec8eafa 100644 --- a/renv.lock +++ b/renv.lock @@ -1,6 +1,6 @@ { "R": { - "Version": "4.3.2", + "Version": "4.5.0", "Repositories": [ { "Name": "CRAN", @@ -9,6 +9,17 @@ ] }, "Packages": { + "PKI": { + "Package": "PKI", + "Version": "0.1-14", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "base64enc" + ], + "Hash": "f5b9c6b2f62f1fa3dd53fd1ddccbb241" + }, "R.cache": { "Package": "R.cache", "Version": "0.16.0", @@ -37,22 +48,22 @@ }, "R.oo": { "Package": "R.oo", - "Version": "1.25.0", + "Version": "1.27.0", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "R.methodsS3", "methods", "utils" ], - "Hash": "a0900a114f4f0194cf4aa8cd4a700681" + "Hash": "6ac79ff194202248cf946fe3a5d6d498" }, "R.utils": { "Package": "R.utils", - "Version": "2.12.2", + "Version": "2.13.0", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "R.methodsS3", @@ -61,48 +72,59 @@ "tools", "utils" ], - "Hash": "325f01db13da12c04d8f6e7be36ff514" + "Hash": "d32373d88da809f8974d5307481862b0" }, "R6": { "Package": "R6", - "Version": "2.5.1", + "Version": "2.6.1", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R" ], - "Hash": "470851b6d5d0ac559e9d01bb352b4021" + "Hash": "d4335fe7207f1c01ab8c41762f5840d4" }, "Rcpp": { "Package": "Rcpp", - "Version": "1.0.11", + "Version": "1.0.14", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "methods", "utils" ], - "Hash": "ae6cbbe1492f4de79c45fce06f967ce8" + "Hash": "e7bdd9ee90e96921ca8a0f1972d66682" + }, + "S7": { + "Package": "S7", + "Version": "0.2.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "utils" + ], + "Hash": "5deb66b3ae702137e1f4162c11861e76" }, "askpass": { "Package": "askpass", - "Version": "1.2.0", + "Version": "1.2.1", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "sys" ], - "Hash": "cad6cf7f1d5f6e906700b9d3e718c796" + "Hash": "c39f4155b3ceb1a9a2799d700fbd4b6a" }, "backports": { "Package": "backports", - "Version": "1.4.1", + "Version": "1.5.0", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R" ], - "Hash": "c39fbec8a30d23e721980b8afb31984c" + "Hash": "e1e1b9d75c37401117b636b7ae50827a" }, "base64enc": { "Package": "base64enc", @@ -116,93 +138,132 @@ }, "box": { "Package": "box", - "Version": "1.1.3", + "Version": "1.2.0", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "tools" ], - "Hash": "ce8187a260e8e3abc2294284badc3b76" + "Hash": "d94049c1d9446b0abb413fde9e82a505" + }, + "box.linters": { + "Package": "box.linters", + "Version": "0.10.5", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "cli", + "fs", + "glue", + "lintr", + "purrr", + "rlang", + "stringr", + "withr", + "xfun", + "xml2", + "xmlparsedata" + ], + "Hash": "8e26a8f1052518f7b69c967117f918be" + }, + "box.lsp": { + "Package": "box.lsp", + "Version": "0.1.3", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "box", + "cli", + "fs", + "rlang" + ], + "Hash": "c39c37f080928f9fd464eb582120d698" }, "brio": { "Package": "brio", - "Version": "1.1.3", + "Version": "1.1.5", "Source": "Repository", - "Repository": "CRAN", - "Hash": "976cf154dfb043c012d87cddd8bca363" + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "c1ee497a6d999947c2c224ae46799b1a" }, "bslib": { "Package": "bslib", - "Version": "0.5.1", + "Version": "0.9.0", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "base64enc", "cachem", + "fastmap", "grDevices", "htmltools", "jquerylib", "jsonlite", + "lifecycle", "memoise", "mime", "rlang", "sass" ], - "Hash": "283015ddfbb9d7bf15ea9f0b5698f0d9" + "Hash": "70a6489cc254171fb9b4a7f130f44dca" }, "cachem": { "Package": "cachem", - "Version": "1.0.8", + "Version": "1.1.0", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "fastmap", "rlang" ], - "Hash": "c35768291560ce302c0a6589f92e837d" + "Hash": "cd9a672193789068eb5a2aad65a0dedf" }, "callr": { "Package": "callr", - "Version": "3.7.3", + "Version": "3.7.6", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "R6", "processx", "utils" ], - "Hash": "9b2191ede20fa29828139b9900922e51" + "Hash": "d7e13f49c19103ece9e58ad2d83a7354" }, "cli": { "Package": "cli", - "Version": "3.6.1", + "Version": "3.6.4", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "utils" ], - "Hash": "89e6d8219950eac806ae0c489052048a" + "Hash": "491c34d3d9dd0d2fe13d9f278bb90795" }, "codetools": { "Package": "codetools", - "Version": "0.2-19", + "Version": "0.2-20", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "R" ], - "Hash": "c089a619a7fae175d149d89164f8c7d8" + "Hash": "61e097f35917d342622f21cdc79c256e" }, "commonmark": { "Package": "commonmark", - "Version": "1.9.0", + "Version": "1.9.5", "Source": "Repository", - "Repository": "CRAN", - "Hash": "d691c61bff84bd63c383874d2d0c3307" + "Repository": "RSPM", + "Hash": "4ac08754c8ed35996b7c343fbb22885a" }, "config": { "Package": "config", @@ -214,61 +275,57 @@ ], "Hash": "8b7222e9d9eb5178eea545c0c4d33fc2" }, + "coro": { + "Package": "coro", + "Version": "1.1.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "rlang" + ], + "Hash": "4998c8836ff95c90993a4eb8d853df71" + }, "crayon": { "Package": "crayon", - "Version": "1.5.2", + "Version": "1.5.3", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "grDevices", "methods", "utils" ], - "Hash": "e8a1e41acf02548751f45c718d55aa6a" + "Hash": "859d96e65ef198fd43e82b9628d593ef" }, "curl": { "Package": "curl", - "Version": "5.2.0", + "Version": "6.2.2", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R" ], - "Hash": "ce88d13c0b10fe88a37d9c59dba2d7f9" - }, - "cyclocomp": { - "Package": "cyclocomp", - "Version": "1.1.1", - "Source": "Repository", - "Repository": "CRAN", - "Requirements": [ - "callr", - "crayon", - "desc", - "remotes", - "withr" - ], - "Hash": "cdc4a473222b0112d4df0bcfbed12d44" + "Hash": "e4f9e10b18f453a1b7eaf38247dad4fe" }, "desc": { "Package": "desc", - "Version": "1.4.2", + "Version": "1.4.3", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "R6", "cli", - "rprojroot", "utils" ], - "Hash": "6b9602c7ebbe87101a9c8edb6e8b6d21" + "Hash": "99b79fcbd6c4d1ce087f5c5c758b384f" }, "diffobj": { "Package": "diffobj", - "Version": "0.3.5", + "Version": "0.3.6", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "crayon", @@ -277,18 +334,18 @@ "tools", "utils" ], - "Hash": "bcaa8b95f8d7d01a5dedfd959ce88ab8" + "Hash": "e036ce354ab60e705ac5f40bac87e8cb" }, "digest": { "Package": "digest", - "Version": "0.6.33", + "Version": "0.6.37", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "utils" ], - "Hash": "b18a9cf3c003977b0cc49d5e76ebe48d" + "Hash": "33698c4b3127fc9f506654607fb73676" }, "dplyr": { "Package": "dplyr", @@ -313,69 +370,77 @@ ], "Hash": "fedd9d00c2944ff00a0e2696ccf048ec" }, - "ellipsis": { - "Package": "ellipsis", - "Version": "0.3.2", + "ellmer": { + "Package": "ellmer", + "Version": "0.1.1", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ - "R", + "R6", + "S7", + "cli", + "coro", + "glue", + "httr2", + "jsonlite", + "later", + "lifecycle", + "promises", "rlang" ], - "Hash": "bb0eec2fe32e88d9e2836c2f73ea2077" + "Hash": "754d3a8cbb25b05be2058892f579422f" }, "evaluate": { "Package": "evaluate", - "Version": "0.23", + "Version": "1.0.3", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ - "R", - "methods" + "R" ], - "Hash": "daf4a1246be12c1fa8c7705a0935c1a0" + "Hash": "e9651417729bbe7472e32b5027370e79" }, "fansi": { "Package": "fansi", - "Version": "1.0.5", + "Version": "1.0.6", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "grDevices", "utils" ], - "Hash": "3e8583a60163b4bc1a80016e63b9959e" + "Hash": "962174cf2aeb5b9eea581522286a911f" }, "fastmap": { "Package": "fastmap", - "Version": "1.1.1", + "Version": "1.2.0", "Source": "Repository", - "Repository": "CRAN", - "Hash": "f7736a18de97dea803bde0a2daaafb27" + "Repository": "RSPM", + "Hash": "aa5e1cd11c2d15497494c5292d7ffcc8" }, "fontawesome": { "Package": "fontawesome", - "Version": "0.5.2", + "Version": "0.5.3", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "htmltools", "rlang" ], - "Hash": "c2efdd5f0bcd1ea861c2d4e2a883a67d" + "Hash": "bd1297f9b5b1fc1372d19e2c4cd82215" }, "fs": { "Package": "fs", - "Version": "1.6.3", + "Version": "1.6.6", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "methods" ], - "Hash": "47b5f30c720c23999b913a1a635cf0bb" + "Hash": "7eb1e342eee7e0a7449c49cdaa526d39" }, "generics": { "Package": "generics", @@ -390,42 +455,41 @@ }, "glue": { "Package": "glue", - "Version": "1.6.2", + "Version": "1.8.0", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "methods" ], - "Hash": "4f2596dfb05dac67b9dc558e5c6fba2e" + "Hash": "5899f1eaa825580172bb56c08266f37c" }, "highr": { "Package": "highr", - "Version": "0.10", + "Version": "0.11", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "xfun" ], - "Hash": "06230136b2d2b9ba5805e1963fa6e890" + "Hash": "d65ba49117ca223614f71b60d85b8ab7" }, "htmltools": { "Package": "htmltools", - "Version": "0.5.7", + "Version": "0.5.8.1", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "R", "base64enc", "digest", - "ellipsis", "fastmap", "grDevices", "rlang", "utils" ], - "Hash": "2d7b3857980e0e0d0a1fd6f11928ab0f" + "Hash": "81d371a9cc60640e74e4ab6ac46dcedc" }, "htmlwidgets": { "Package": "htmlwidgets", @@ -444,9 +508,9 @@ }, "httpuv": { "Package": "httpuv", - "Version": "1.6.12", + "Version": "1.6.16", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "R6", @@ -455,11 +519,11 @@ "promises", "utils" ], - "Hash": "c992f75861325961c29a188b45e549f7" + "Hash": "6c3c8728e40326de6529a5c46e377e5c" }, "httr2": { "Package": "httr2", - "Version": "1.0.0", + "Version": "1.1.2", "Source": "Repository", "Repository": "RSPM", "Requirements": [ @@ -476,7 +540,7 @@ "vctrs", "withr" ], - "Hash": "e2b30f1fc039a0bab047dd52bb20ef71" + "Hash": "ade531519694081d91036b509eb30594" }, "jquerylib": { "Package": "jquerylib", @@ -490,19 +554,19 @@ }, "jsonlite": { "Package": "jsonlite", - "Version": "1.8.7", + "Version": "2.0.0", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "methods" ], - "Hash": "266a20443ca13c65688b2116d5220f76" + "Hash": "b0776f526d36d8bd4a3344a88fe165c4" }, "knitr": { "Package": "knitr", - "Version": "1.45", + "Version": "1.50", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "evaluate", @@ -512,18 +576,18 @@ "xfun", "yaml" ], - "Hash": "1ec462871063897135c1bcbe0fc8f07d" + "Hash": "5a07d8ec459d7b80bd4acca5f4a6e062" }, "later": { "Package": "later", - "Version": "1.3.1", + "Version": "1.4.2", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "Rcpp", "rlang" ], - "Hash": "40401c9cf2bc2259dfe83311c9384710" + "Hash": "9591aabef9cf988f05bde9324fd3ad99" }, "lazyeval": { "Package": "lazyeval", @@ -537,27 +601,27 @@ }, "lifecycle": { "Package": "lifecycle", - "Version": "1.0.3", + "Version": "1.0.4", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "cli", "glue", "rlang" ], - "Hash": "001cecbeac1cff9301bdc3775ee46a86" + "Hash": "b8552d117e1b808b09a832f589b79035" }, "lintr": { "Package": "lintr", - "Version": "3.1.0", + "Version": "3.2.0", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "backports", + "cli", "codetools", - "cyclocomp", "digest", "glue", "knitr", @@ -567,17 +631,18 @@ "xml2", "xmlparsedata" ], - "Hash": "2b4b803af6017e93b67ddaf0eacba918" + "Hash": "45b44c7f1040cce8f34ec0a9c92d47f3" }, "logger": { "Package": "logger", - "Version": "0.2.2", + "Version": "0.4.0", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ + "R", "utils" ], - "Hash": "c269b06beb2bbadb0d058c0e6fa4ec3d" + "Hash": "f25d781d5bc7757e08cf38c741a5ad1c" }, "magrittr": { "Package": "magrittr", @@ -602,23 +667,23 @@ }, "mime": { "Package": "mime", - "Version": "0.12", + "Version": "0.13", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "tools" ], - "Hash": "18e9c28c1d3ca1560ce30658b22ce104" + "Hash": "0ec19f34c72fab674d8f2b4b1c6410e1" }, "openssl": { "Package": "openssl", - "Version": "2.1.1", + "Version": "2.3.2", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "askpass" ], - "Hash": "2a0dc8c6adfb6f032e4d4af82d258ab5" + "Hash": "bc54d87ebf858b28de18df4bca6528d3" }, "packrat": { "Package": "packrat", @@ -634,12 +699,11 @@ }, "pillar": { "Package": "pillar", - "Version": "1.9.0", + "Version": "1.10.2", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "cli", - "fansi", "glue", "lifecycle", "rlang", @@ -647,25 +711,22 @@ "utils", "vctrs" ], - "Hash": "15da5a8412f317beeee6175fbc76f4bb" + "Hash": "1098920a19b5cd5a15bacdc74a89979d" }, "pkgbuild": { "Package": "pkgbuild", - "Version": "1.4.2", + "Version": "1.4.7", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "R6", "callr", "cli", - "crayon", "desc", - "prettyunits", - "processx", - "rprojroot" + "processx" ], - "Hash": "beb25b32a957a22a5c301a9e441190b3" + "Hash": "ae47817501cafc99f57586b6e5241134" }, "pkgconfig": { "Package": "pkgconfig", @@ -679,24 +740,25 @@ }, "pkgload": { "Package": "pkgload", - "Version": "1.3.3", + "Version": "1.4.0", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "cli", - "crayon", "desc", "fs", "glue", + "lifecycle", "methods", "pkgbuild", + "processx", "rlang", "rprojroot", "utils", "withr" ], - "Hash": "903d68319ae9923fb2e2ee7fa8230b91" + "Hash": "2ec30ffbeec83da57655b850cf2d3e0e" }, "praise": { "Package": "praise", @@ -705,34 +767,24 @@ "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", + "Version": "3.8.6", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "R6", "ps", "utils" ], - "Hash": "3efbd8ac1be0296a46c55387aeace0f3" + "Hash": "720161b280b0a35f4d1490ead2fe81d0" }, "promises": { "Package": "promises", - "Version": "1.2.1", + "Version": "1.3.2", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R6", "Rcpp", @@ -742,24 +794,24 @@ "rlang", "stats" ], - "Hash": "0d8a15c9d000970ada1ab21405387dee" + "Hash": "c84fd4f75ea1f5434735e08b7f50fbca" }, "ps": { "Package": "ps", - "Version": "1.7.5", + "Version": "1.9.1", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "utils" ], - "Hash": "709d852d33178db54b17c722e5b1e594" + "Hash": "093688087b0bacce6ba2f661f36328e2" }, "purrr": { "Package": "purrr", - "Version": "1.0.2", + "Version": "1.0.4", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "cli", @@ -768,7 +820,7 @@ "rlang", "vctrs" ], - "Hash": "1cba04a4e9414bdefc9dcaa99649a8dc" + "Hash": "cc8b5d43f90551fa6df0a6be5d640a4f" }, "rappdirs": { "Package": "rappdirs", @@ -782,13 +834,13 @@ }, "reactR": { "Package": "reactR", - "Version": "0.5.0", + "Version": "0.6.1", "Source": "Repository", "Repository": "CRAN", "Requirements": [ "htmltools" ], - "Hash": "c9014fd1a435b2d790dd506589cb24e5" + "Hash": "b8e3d93f508045812f47136c7c44c251" }, "reactable": { "Package": "reactable", @@ -805,30 +857,6 @@ ], "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", @@ -851,12 +879,15 @@ }, "rhino": { "Package": "rhino", - "Version": "1.7.0", + "Version": "1.11.0", "Source": "Repository", "Repository": "RSPM", "Requirements": [ "R", "box", + "box.linters", + "box.lsp", + "callr", "cli", "config", "fs", @@ -872,27 +903,26 @@ "testthat", "utils", "withr", - "xml2", "yaml" ], - "Hash": "59ee79b26dd590b08dd1a3111d093832" + "Hash": "65daa659ff338dcf4f1ff79ddecadb00" }, "rlang": { "Package": "rlang", - "Version": "1.1.1", + "Version": "1.1.6", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "utils" ], - "Hash": "a85c767b55f0bf9b7ad16c6d7baee5bb" + "Hash": "892124978869b74935dc3934c42bfe5a" }, "rmarkdown": { "Package": "rmarkdown", - "Version": "2.25", + "Version": "2.29", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "bslib", @@ -903,31 +933,31 @@ "jsonlite", "knitr", "methods", - "stringr", "tinytex", "tools", "utils", "xfun", "yaml" ], - "Hash": "d65e35823c817f09f4de424fcdfa812a" + "Hash": "df99277f63d01c34e95e3d2f06a79736" }, "rprojroot": { "Package": "rprojroot", - "Version": "2.0.3", + "Version": "2.0.4", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R" ], - "Hash": "1de7ab598047a87bba48434ba35d497d" + "Hash": "4c8415e0ec1e29f3f4f6fc108bef0144" }, "rsconnect": { "Package": "rsconnect", - "Version": "1.2.1", + "Version": "1.3.4", "Source": "Repository", "Repository": "RSPM", "Requirements": [ + "PKI", "R", "cli", "curl", @@ -942,20 +972,20 @@ "tools", "yaml" ], - "Hash": "94bb3a2125b01b13dd2e4a784c2a9639" + "Hash": "315454d2f50d117ef58691d0bf50fe63" }, "rstudioapi": { "Package": "rstudioapi", - "Version": "0.15.0", + "Version": "0.17.1", "Source": "Repository", - "Repository": "CRAN", - "Hash": "5564500e25cffad9e22244ced1379887" + "Repository": "RSPM", + "Hash": "5f90cd73946d706cfe26024294236113" }, "sass": { "Package": "sass", - "Version": "0.4.7", + "Version": "0.4.10", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R6", "fs", @@ -963,13 +993,13 @@ "rappdirs", "rlang" ], - "Hash": "6bd4d33b50ff927191ec9acbf52fd056" + "Hash": "3fb78d066fb92299b1d13f6a7c9a90a8" }, "shiny": { "Package": "shiny", - "Version": "1.7.5.1", + "Version": "1.10.0", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "R6", @@ -977,7 +1007,6 @@ "cachem", "commonmark", "crayon", - "ellipsis", "fastmap", "fontawesome", "glue", @@ -997,21 +1026,22 @@ "withr", "xtable" ], - "Hash": "5ec01cc255f2138fc2f0dc74d2b1a1a1" + "Hash": "4b4477baa9a939c5577e5ddb4bf01f28" }, "shinycssloaders": { "Package": "shinycssloaders", - "Version": "1.0.0", + "Version": "1.1.0", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "digest", "glue", "grDevices", + "htmltools", "shiny" ], - "Hash": "f39bb3c44a9b496723ec7e86f9a771d8" + "Hash": "2b45a467a30d6a88a1892a738c0900cf" }, "sourcetools": { "Package": "sourcetools", @@ -1025,16 +1055,16 @@ }, "stringi": { "Package": "stringi", - "Version": "1.8.3", + "Version": "1.8.7", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "stats", "tools", "utils" ], - "Hash": "058aebddea264f4c99401515182e656a" + "Hash": "2b56088e23bdd58f89aebf43a0913457" }, "stringr": { "Package": "stringr", @@ -1055,9 +1085,9 @@ }, "styler": { "Package": "styler", - "Version": "1.10.2", + "Version": "1.10.3", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "R.cache", @@ -1070,20 +1100,20 @@ "vctrs", "withr" ], - "Hash": "d61238fd44fc63c8adf4565efe8eb682" + "Hash": "93a2b1beac2437bdcc4724f8bf867e2c" }, "sys": { "Package": "sys", - "Version": "3.4.2", + "Version": "3.4.3", "Source": "Repository", - "Repository": "CRAN", - "Hash": "3a1be13d68d47a8cd0bfd74739ca1555" + "Repository": "RSPM", + "Hash": "de342ebfebdbf40477d0758d05426646" }, "testthat": { "Package": "testthat", - "Version": "3.2.0", + "Version": "3.2.3", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "R6", @@ -1092,7 +1122,6 @@ "cli", "desc", "digest", - "ellipsis", "evaluate", "jsonlite", "lifecycle", @@ -1107,7 +1136,7 @@ "waldo", "withr" ], - "Hash": "877508719fcb8c9525eccdadf07a5102" + "Hash": "42f889439ccb14c55fc3d75c9c755056" }, "tibble": { "Package": "tibble", @@ -1130,9 +1159,9 @@ }, "tidyselect": { "Package": "tidyselect", - "Version": "1.2.0", + "Version": "1.2.1", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "cli", @@ -1142,17 +1171,41 @@ "vctrs", "withr" ], - "Hash": "79540e5fcd9e0435af547d885f184fd5" + "Hash": "829f27b9c4919c16b593794a6344d6c0" }, "tinytex": { "Package": "tinytex", - "Version": "0.49", + "Version": "0.57", "Source": "Repository", "Repository": "RSPM", "Requirements": [ "xfun" ], - "Hash": "5ac22900ae0f386e54f1c307eca7d843" + "Hash": "02d65e0c0415bf36a7ddc0d2ba50a840" + }, + "treesitter": { + "Package": "treesitter", + "Version": "0.2.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R", + "R6", + "cli", + "rlang", + "vctrs" + ], + "Hash": "cc0472495bc995fabbb58e2de941c384" + }, + "treesitter.r": { + "Package": "treesitter.r", + "Version": "1.1.0", + "Source": "Repository", + "Repository": "RSPM", + "Requirements": [ + "R" + ], + "Hash": "067c058dbd496c4cd696449870b11d52" }, "utf8": { "Package": "utf8", @@ -1166,9 +1219,9 @@ }, "vctrs": { "Package": "vctrs", - "Version": "0.6.4", + "Version": "0.6.5", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "cli", @@ -1176,60 +1229,60 @@ "lifecycle", "rlang" ], - "Hash": "266c1ca411266ba8f365fcc726444b87" + "Hash": "c03fa420630029418f7e6da3667aac4a" }, "waldo": { "Package": "waldo", - "Version": "0.5.2", + "Version": "0.6.1", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "cli", "diffobj", - "fansi", "glue", "methods", - "rematch2", - "rlang", - "tibble" + "rlang" ], - "Hash": "c7d3fd6d29ab077cbac8f0e2751449e6" + "Hash": "52f574062a7b66e56926988c3fbdb3b7" }, "withr": { "Package": "withr", - "Version": "2.5.2", + "Version": "3.0.2", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", "grDevices", - "graphics", - "stats" + "graphics" ], - "Hash": "4b25e70111b7d644322e9513f403a272" + "Hash": "cc2d62c76458d425210d1eb1478b30b4" }, "xfun": { "Package": "xfun", - "Version": "0.41", + "Version": "0.52", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ + "R", + "grDevices", "stats", "tools" ], - "Hash": "460a5e0fe46a80ef87424ad216028014" + "Hash": "652ce36fe7d57688e6786819b09d9190" }, "xml2": { "Package": "xml2", - "Version": "1.3.5", + "Version": "1.3.8", "Source": "Repository", - "Repository": "CRAN", + "Repository": "RSPM", "Requirements": [ "R", - "methods" + "cli", + "methods", + "rlang" ], - "Hash": "6c40e5cfcc6aefd88110666e18c31f40" + "Hash": "f5130b2f3d461964bac93cc618013231" }, "xmlparsedata": { "Package": "xmlparsedata", @@ -1255,10 +1308,10 @@ }, "yaml": { "Package": "yaml", - "Version": "2.3.7", + "Version": "2.3.10", "Source": "Repository", - "Repository": "CRAN", - "Hash": "0d0056cc5383fbc240ccd0cb584bf436" + "Repository": "RSPM", + "Hash": "51dab85c6c98e50a18d7551e9d49f76c" } } } diff --git a/tests/testthat/test-general_utils.R b/tests/testthat/test-general_utils.R new file mode 100644 index 0000000..a298404 --- /dev/null +++ b/tests/testthat/test-general_utils.R @@ -0,0 +1,82 @@ +box::use( + testthat[ + describe, + expect_identical, + expect_true, + it + ], +) + +box::use( + app/logic/general_utils[ + check_text_error, + format_timestamp, + generate_css_variables, + ], +) + +describe("check_text_error()", { + it("returns TRUE for error keywords", { + expect_true( + check_text_error( + "Error: job failed" + ) + ) + expect_true( + check_text_error( + "Halt: job terminated" + ) + ) + expect_true( + check_text_error( + "Error: file not found" + ) + ) + }) + + it("returns FALSE for non-error keywords", { + expect_true( + !check_text_error( + "Job completed successfully" + ) + ) + expect_true( + !check_text_error( + "Processing" + ) + ) + }) +}) + +describe("format_timestamp()", { + it("correctly formats timestamp", { + expect_identical( + format_timestamp( + "2023-10-01T12:34:56Z" + ), + "2023-10-01 12:34:56" + ) + expect_identical( + format_timestamp( + "2023-10-01T12:34:56.789Z" + ), + "2023-10-01 12:34:56" + ) + }) +}) + +describe("generate_css_variables()", { + it("generates CSS variables correctly", { + config <- list( + colors = list( + primary = "#FF5733", + secondary = "#33FF57" + ) + ) + expected_css <- ":root {\n --primary: #FF5733;\n --secondary: #33FF57;\n}" + expect_identical( + generate_css_variables(config), + expected_css + ) + }) +})