From c55d6f657b2d45932d764c7b4dac96f8237a130b Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Wed, 11 Jun 2025 15:38:45 -0400 Subject: [PATCH 1/4] add options to color availability based on server versions --- extensions/eol-runtime-scanner/app.R | 81 +++++++++++++++++++--------- 1 file changed, 57 insertions(+), 24 deletions(-) diff --git a/extensions/eol-runtime-scanner/app.R b/extensions/eol-runtime-scanner/app.R index 1a45cea1..7fec8b85 100644 --- a/extensions/eol-runtime-scanner/app.R +++ b/extensions/eol-runtime-scanner/app.R @@ -40,6 +40,27 @@ app_mode_lookup <- with( setNames(as.character(ind), values) ) +# Helper function to style versions based on server availability +style_version_by_availability <- function(server_versions) { + function(value) { + value_str <- as.character(value) + if (!is.na(value_str)) { + # Check if version doesn't exist in server versions + if (!(value_str %in% server_versions)) { + return(list( + color = "#7D1A03", # Red color for versions not on server + fontWeight = "bold", + background = "#FFF0F0" # Light red background + )) + } else { + return(list( + color = "#276749" # Green color for versions on server + )) + } + } + } +} + # Shiny app definition ui <- page_sidebar( @@ -108,6 +129,14 @@ ui <- page_sidebar( options = list(placeholder = "All Content Types"), choices = names(app_mode_groups), multiple = TRUE, + ), + + h5("View"), + + checkboxInput( + "highlight_server_availability", + "Highlight content that has not been built against a runtime currently on the server", + value = FALSE ) ), @@ -171,6 +200,10 @@ server <- function(input, output, session) { summarize(hits = n()) }) + server_versions <- reactive({ + get_runtimes(client) + }) + content_table_data <- reactive({ content_type_mask <- if (length(input$content_type_filter) == 0) { names(app_mode_groups) @@ -260,9 +293,18 @@ server <- function(input, output, session) { output$content_table <- renderReactable({ data <- content_matching() - rv <- input$r_versions - pv <- input$py_versions - qv <- input$quarto_versions + + # Extract available server runtime versions by type + server_vers <- server_versions() + r_server_vers <- server_vers |> filter(runtime == "r") |> pull(version) + py_server_vers <- server_vers |> + filter(runtime == "python") |> + pull(version) + quarto_server_vers <- server_vers |> + filter(runtime == "quarto") |> + pull(version) + + # Extract server versions for styling reactable( data, @@ -291,35 +333,26 @@ server <- function(input, output, session) { content_type = colDef(name = "Content Type"), r_version = colDef( name = "R Version", - style = function(value) { - if (!is.null(rv) && value %in% rv) { - list( - color = "#7D1A03", - fontWeight = "bold" - ) - } + style = if (input$highlight_server_availability) { + style_version_by_availability(r_server_vers) + } else { + NULL } ), py_version = colDef( name = "Python Version", - style = function(value) { - if (!is.null(pv) && value %in% pv) { - list( - color = "#7D1A03", - fontWeight = "bold" - ) - } + style = if (input$highlight_server_availability) { + style_version_by_availability(py_server_vers) + } else { + NULL } ), quarto_version = colDef( name = "Quarto Version", - style = function(value) { - if (!is.null(qv) && value %in% qv) { - list( - color = "#7D1A03", - fontWeight = "bold" - ) - } + style = if (input$highlight_server_availability) { + style_version_by_availability(quarto_server_vers) + } else { + NULL } ), From e4aa0316e4e100f556c94f6432d4abef5d2c12a5 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Wed, 11 Jun 2025 17:54:21 -0400 Subject: [PATCH 2/4] Update interface of app --- extensions/eol-runtime-scanner/app.R | 163 ++++++++++++------ extensions/eol-runtime-scanner/www/styles.css | 32 +++- 2 files changed, 146 insertions(+), 49 deletions(-) diff --git a/extensions/eol-runtime-scanner/app.R b/extensions/eol-runtime-scanner/app.R index 7fec8b85..febb6528 100644 --- a/extensions/eol-runtime-scanner/app.R +++ b/extensions/eol-runtime-scanner/app.R @@ -78,46 +78,43 @@ ui <- page_sidebar( checkboxInput( "use_r_cutoff", - "Match R versions…", + label = "R older than…", value = FALSE ), conditionalPanel( condition = "input.use_r_cutoff == true", - sliderTextInput( - "min_r_version", + selectizeInput( + "r_version_cutoff", label = NULL, - choices = "...", - pre = "≤ " + choices = NULL ) ), checkboxInput( "use_py_cutoff", - "Match Python versions…", + label = "Python older than…", value = FALSE ), conditionalPanel( condition = "input.use_py_cutoff == true", - sliderTextInput( - "min_py_version", + selectizeInput( + "py_version_cutoff", label = NULL, - choices = "...", - pre = "≤ " + choices = NULL ) ), checkboxInput( "use_quarto_cutoff", - "Match Quarto versions…", + label = "Quarto older than…", value = FALSE ), conditionalPanel( condition = "input.use_quarto_cutoff == true", - sliderTextInput( - "min_quarto_version", + selectizeInput( + "quarto_version_cutoff", label = NULL, - choices = "...", - pre = "≤ " + choices = NULL ) ), @@ -131,11 +128,24 @@ ui <- page_sidebar( multiple = TRUE, ), + numericInput( + "min_hits_filter", + label = "Minimum Hits", + value = 0, + min = 0, + step = 1 + ), + h5("View"), checkboxInput( - "highlight_server_availability", - "Highlight content that has not been built against a runtime currently on the server", + "show_guid", + label = "Show GUID" + ), + + checkboxInput( + "highlight_stale_builds", + "Highlight stale builds", value = FALSE ) ), @@ -169,22 +179,28 @@ server <- function(input, output, session) { pv <- levels(content()$py_version) qv <- levels(content()$quarto_version) - updateSliderTextInput( + # Use the highest version (last in the ordered factor levels) + updateSelectizeInput( session, - "min_r_version", - choices = I(rv) + "r_version_cutoff", + choices = I(rv), + selected = if(length(rv) > 0) rv[length(rv)] else NULL ) - updateSliderTextInput( + # Use the highest version (last in the ordered factor levels) + updateSelectizeInput( session, - "min_py_version", - choices = I(pv) + "py_version_cutoff", + choices = I(pv), + selected = if(length(pv) > 0) pv[length(pv)] else NULL ) - updateSliderTextInput( + # Use the highest version (last in the ordered factor levels) + updateSelectizeInput( session, - "min_quarto_version", - choices = I(qv) + "quarto_version_cutoff", + choices = I(qv), + selected = if(length(qv) > 0) qv[length(qv)] else NULL ) }, ignoreNULL = TRUE @@ -205,12 +221,16 @@ server <- function(input, output, session) { }) content_table_data <- reactive({ + # Filter by content type content_type_mask <- if (length(input$content_type_filter) == 0) { names(app_mode_groups) } else { input$content_type_filter } + # Get min hits for filtering + min_hits <- input$min_hits_filter + content() |> mutate(content_type = app_mode_lookup[app_mode]) |> filter(content_type %in% content_type_mask) |> @@ -218,13 +238,15 @@ server <- function(input, output, session) { select( title, dashboard_url, + guid, content_type, r_version, py_version, quarto_version, hits ) |> - replace_na(list(hits = 0)) + replace_na(list(hits = 0)) |> + filter(hits >= min_hits) }) content_matching <- reactive({ @@ -238,51 +260,67 @@ server <- function(input, output, session) { # fmt: skip filter( (input$use_r_cutoff & - r_version <= input$min_r_version) | + r_version < input$r_version_cutoff) | (input$use_py_cutoff & - py_version <= input$min_py_version) | + py_version < input$py_version_cutoff) | (input$use_quarto_cutoff & - quarto_version <= input$min_quarto_version) + quarto_version < input$quarto_version_cutoff) ) } }) output$selected_versions_html <- renderUI({ - rv <- input$min_r_version - pv <- input$min_py_version - qv <- input$min_quarto_version + rv <- input$r_version_cutoff + pv <- input$py_version_cutoff + qv <- input$quarto_version_cutoff + + # Get the total count of filtered content + total_count <- nrow(content_matching()) if ( all(!input$use_r_cutoff, !input$use_py_cutoff, !input$use_quarto_cutoff) ) { tagList( tags$p( - "Showing all your content." + glue::glue("Showing {total_count} items.") ), tags$p( - "Please select the versions of R, Python, or Quarto you wish to scan for." + "Please select cutoff versions of R, Python, or Quarto." ) ) } else { + # Calculate counts for each runtime version filter + r_count <- if (input$use_r_cutoff) { + nrow(content_table_data() |> filter(r_version < input$r_version_cutoff)) + } else { 0 } + + py_count <- if (input$use_py_cutoff) { + nrow(content_table_data() |> filter(py_version < input$py_version_cutoff)) + } else { 0 } + + quarto_count <- if (input$use_quarto_cutoff) { + nrow(content_table_data() |> filter(quarto_version < input$quarto_version_cutoff)) + } else { 0 } + li_items <- list() if (input$use_r_cutoff) { li_items[[length(li_items) + 1]] <- tags$li(glue::glue( - "R {rv} or older" + "R older than {rv} ({r_count} items)" )) } if (input$use_py_cutoff) { li_items[[length(li_items) + 1]] <- tags$li(glue::glue( - "Python {pv} or older" + "Python older than {pv} ({py_count} items)" )) } if (input$use_quarto_cutoff) { li_items[[length(li_items) + 1]] <- tags$li(glue::glue( - "Quarto {qv} or older" + "Quarto older than {qv} ({quarto_count} items)" )) } tagList( - tags$p("Showing content with any runtimes matching:"), + tags$p(glue::glue("Showing {total_count} items meeting any of:")), tags$ul( style = "margin-top: 0.25rem; margin-bottom: 0.5rem;", tagList(li_items) @@ -308,9 +346,14 @@ server <- function(input, output, session) { reactable( data, - defaultPageSize = 500, + defaultPageSize = 15, + showPageSizeOptions = TRUE, + pageSizeOptions = c(15, 50, 100), + wrap = FALSE, + class = "content-tbl", columns = list( title = colDef(name = "Title"), + dashboard_url = colDef( name = "", width = 32, @@ -330,33 +373,57 @@ server <- function(input, output, session) { }, html = TRUE ), - content_type = colDef(name = "Content Type"), + + guid = colDef( + name = "GUID", + show = input$show_guid, + class = "number-pre", + cell = function(value) { + div( + style = list(whiteSpace = "normal", wordBreak = "break-all"), + value + ) + } + ), + + content_type = colDef(name = "Content Type", width = 125), + r_version = colDef( - name = "R Version", - style = if (input$highlight_server_availability) { + name = "R", + width = 100, + class = "number-pre", + style = if (input$highlight_stale_builds) { style_version_by_availability(r_server_vers) } else { NULL } ), py_version = colDef( - name = "Python Version", - style = if (input$highlight_server_availability) { + name = "Python", + width = 100, + class = "number-pre", + style = if (input$highlight_stale_builds) { style_version_by_availability(py_server_vers) } else { NULL } ), quarto_version = colDef( - name = "Quarto Version", - style = if (input$highlight_server_availability) { + name = "Quarto", + width = 100, + class = "number-pre", + style = if (input$highlight_stale_builds) { style_version_by_availability(quarto_server_vers) } else { NULL } ), - hits = colDef(name = "Hits in Last Week") + hits = colDef( + name = "Hits (1 Week)", + width = 125, + class = "number-pre", + ) ) ) }) diff --git a/extensions/eol-runtime-scanner/www/styles.css b/extensions/eol-runtime-scanner/www/styles.css index 220ebbe6..ad0427ca 100644 --- a/extensions/eol-runtime-scanner/www/styles.css +++ b/extensions/eol-runtime-scanner/www/styles.css @@ -1,3 +1,33 @@ +/* Reduce spacing between checkbox and conditional panel */ .form-group.shiny-input-container.checkbox+.shiny-panel-conditional>.form-group.shiny-input-container { - margin-top: 0 !important; + margin-top: 0 !important; +} + +/* Adjust the spacing for selectize inputs in conditional panels */ +.form-group.shiny-input-container.checkbox+.shiny-panel-conditional .selectize-control { + margin-top: -10px; +} + +/* Compact selectizeInput in conditional panels */ +.shiny-panel-conditional .selectize-input { + margin-top: -10px; +} + +/* Table styles */ + +/* Styles used by the Most Used Content table */ +.content-tbl { + font-size: 0.875rem; + line-height: 1.125rem; +} + +.content-tbl a { + color: inherit; + text-decoration: none; +} + +.number-pre { + font-family: "Fira Mono", Consolas, Monaco, monospace; + white-space: pre; + font-size: 0.84375rem; } \ No newline at end of file From f3f56435a0d6c7b7c70ca2ed4c50b45c28601011 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Wed, 11 Jun 2025 18:14:48 -0400 Subject: [PATCH 3/4] more updates --- extensions/eol-runtime-scanner/app.R | 21 +++++----- extensions/eol-runtime-scanner/renv.lock | 39 ------------------- extensions/eol-runtime-scanner/www/styles.css | 2 +- 3 files changed, 11 insertions(+), 51 deletions(-) diff --git a/extensions/eol-runtime-scanner/app.R b/extensions/eol-runtime-scanner/app.R index febb6528..123380ae 100644 --- a/extensions/eol-runtime-scanner/app.R +++ b/extensions/eol-runtime-scanner/app.R @@ -7,7 +7,6 @@ library(shinycssloaders) library(lubridate) library(bsicons) library(tidyr) -library(shinyWidgets) source("get_usage.R") source("connect_module.R") @@ -304,23 +303,23 @@ server <- function(input, output, session) { li_items <- list() if (input$use_r_cutoff) { - li_items[[length(li_items) + 1]] <- tags$li(glue::glue( - "R older than {rv} ({r_count} items)" - )) + li_items[[length(li_items) + 1]] <- tags$li(HTML(glue::glue( + "R older than {rv} ({r_count} items)" + ))) } if (input$use_py_cutoff) { - li_items[[length(li_items) + 1]] <- tags$li(glue::glue( - "Python older than {pv} ({py_count} items)" - )) + li_items[[length(li_items) + 1]] <- tags$li(HTML(glue::glue( + "Python older than {pv} ({py_count} items)" + ))) } if (input$use_quarto_cutoff) { - li_items[[length(li_items) + 1]] <- tags$li(glue::glue( - "Quarto older than {qv} ({quarto_count} items)" - )) + li_items[[length(li_items) + 1]] <- tags$li(HTML(glue::glue( + "Quarto older than {qv} ({quarto_count} items)" + ))) } tagList( - tags$p(glue::glue("Showing {total_count} items meeting any of:")), + tags$p(glue::glue("Showing {total_count} items with any of:")), tags$ul( style = "margin-top: 0.25rem; margin-bottom: 0.5rem;", tagList(li_items) diff --git a/extensions/eol-runtime-scanner/renv.lock b/extensions/eol-runtime-scanner/renv.lock index 2d174b39..0986a61e 100644 --- a/extensions/eol-runtime-scanner/renv.lock +++ b/extensions/eol-runtime-scanner/renv.lock @@ -1941,45 +1941,6 @@ "Maintainer": "Winston Chang ", "Repository": "CRAN" }, - "shinyWidgets": { - "Package": "shinyWidgets", - "Version": "0.9.0", - "Source": "Repository", - "Title": "Custom Inputs Widgets for Shiny", - "Authors@R": "c( person(\"Victor\", \"Perrier\", email = \"victor.perrier@dreamrs.fr\", role = c(\"aut\", \"cre\", \"cph\")), person(\"Fanny\", \"Meyer\", role = \"aut\"), person(\"David\", \"Granjon\", role = \"aut\"), person(\"Ian\", \"Fellows\", role = \"ctb\", comment = \"Methods for mutating vertical tabs & updateMultiInput\"), person(\"Wil\", \"Davis\", role = \"ctb\", comment = \"numericRangeInput function\"), person(\"Spencer\", \"Matthews\", role = \"ctb\", comment = \"autoNumeric methods\"), person(family = \"JavaScript and CSS libraries authors\", role = c(\"ctb\", \"cph\"), comment = \"All authors are listed in LICENSE.md\") )", - "Description": "Collection of custom input controls and user interface components for 'Shiny' applications. Give your applications a unique and colorful style !", - "URL": "https://github.com/dreamRs/shinyWidgets, https://dreamrs.github.io/shinyWidgets/", - "BugReports": "https://github.com/dreamRs/shinyWidgets/issues", - "License": "GPL-3", - "Encoding": "UTF-8", - "LazyData": "true", - "RoxygenNote": "7.3.2", - "Depends": [ - "R (>= 3.1.0)" - ], - "Imports": [ - "bslib", - "sass", - "shiny (>= 1.6.0)", - "htmltools (>= 0.5.1)", - "jsonlite", - "grDevices", - "rlang" - ], - "Suggests": [ - "testthat", - "covr", - "ggplot2", - "DT", - "scales", - "shinydashboard", - "shinydashboardPlus" - ], - "NeedsCompilation": "no", - "Author": "Victor Perrier [aut, cre, cph], Fanny Meyer [aut], David Granjon [aut], Ian Fellows [ctb] (Methods for mutating vertical tabs & updateMultiInput), Wil Davis [ctb] (numericRangeInput function), Spencer Matthews [ctb] (autoNumeric methods), JavaScript and CSS libraries authors [ctb, cph] (All authors are listed in LICENSE.md)", - "Maintainer": "Victor Perrier ", - "Repository": "CRAN" - }, "shinycssloaders": { "Package": "shinycssloaders", "Version": "1.1.0", diff --git a/extensions/eol-runtime-scanner/www/styles.css b/extensions/eol-runtime-scanner/www/styles.css index ad0427ca..b4d14863 100644 --- a/extensions/eol-runtime-scanner/www/styles.css +++ b/extensions/eol-runtime-scanner/www/styles.css @@ -29,5 +29,5 @@ .number-pre { font-family: "Fira Mono", Consolas, Monaco, monospace; white-space: pre; - font-size: 0.84375rem; + font-size: 96%; } \ No newline at end of file From 1a5e292d1d0e42a265c1b9874a55e2a37f24a737 Mon Sep 17 00:00:00 2001 From: Toph Allen Date: Tue, 17 Jun 2025 17:46:55 -0400 Subject: [PATCH 4/4] respond to feedback --- extensions/eol-runtime-scanner/app.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/extensions/eol-runtime-scanner/app.R b/extensions/eol-runtime-scanner/app.R index 123380ae..7a1e2d8f 100644 --- a/extensions/eol-runtime-scanner/app.R +++ b/extensions/eol-runtime-scanner/app.R @@ -129,7 +129,7 @@ ui <- page_sidebar( numericInput( "min_hits_filter", - label = "Minimum Hits", + label = "Minimum Views", value = 0, min = 0, step = 1 @@ -139,7 +139,7 @@ ui <- page_sidebar( checkboxInput( "show_guid", - label = "Show GUID" + label = "Show GUIDs" ), checkboxInput( @@ -419,7 +419,7 @@ server <- function(input, output, session) { ), hits = colDef( - name = "Hits (1 Week)", + name = "Views (Last Week)", width = 125, class = "number-pre", )