From 82ec63a5ee1ef7eb0c28f1b398dbf381efc4b43f Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Tue, 14 May 2024 15:47:12 +0530 Subject: [PATCH 01/13] fix: add support for quarto-shiny and python-shiny (tested with apps that work) --- app/logic/api_utils.R | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/logic/api_utils.R b/app/logic/api_utils.R index ea09ead..28eca43 100644 --- a/app/logic/api_utils.R +++ b/app/logic/api_utils.R @@ -42,14 +42,14 @@ get_api_url <- function( #' 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 app_mode_filter Character list. The filter for app_mode in the API +#' response. Default is list("shiny", "python-shiny", "quarto-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", + app_mode_filter = list("shiny", "python-shiny", "quarto-shiny"), endpoint = "content", dry_run = FALSE ) { @@ -70,7 +70,7 @@ get_app_list <- function( req_perform() %>% resp_body_string() %>% fromJSON() %>% - filter(app_mode == app_mode_filter) + filter(app_mode %in% app_mode_filter) } } From 7ea901fb5566f3cd65998d02253a8bea22166716 Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Tue, 14 May 2024 15:52:57 +0530 Subject: [PATCH 02/13] fix: add filter to only show apps someone owns or has access to --- app/logic/api_utils.R | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/logic/api_utils.R b/app/logic/api_utils.R index 28eca43..1ba0262 100644 --- a/app/logic/api_utils.R +++ b/app/logic/api_utils.R @@ -70,7 +70,10 @@ get_app_list <- function( req_perform() %>% resp_body_string() %>% fromJSON() %>% - filter(app_mode %in% app_mode_filter) + filter( + app_mode %in% app_mode_filter, + app_role == "owner" + ) } } From 8aa0766693448abba0bff7c4e6c3c04e34450490 Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Tue, 14 May 2024 15:59:41 +0530 Subject: [PATCH 03/13] fix: handle scenario when no jobs are found for an app --- app/view/mod_job_list.R | 60 +++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/app/view/mod_job_list.R b/app/view/mod_job_list.R index 96aac6e..f4500e0 100644 --- a/app/view/mod_job_list.R +++ b/app/view/mod_job_list.R @@ -9,13 +9,15 @@ box::use( getReactableState, reactable, reactableOutput, - renderReactable + renderReactable, + reactableLang ], shiny[ moduleServer, NS, reactive, - req + req, + isTruthy ], shinycssloaders[withSpinner], ) @@ -48,25 +50,31 @@ server <- function(id, state) { 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 + if (length(job_list_data())) { + 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 + ) ) + } else { + processed_jobs <- data.frame( + job = character() ) + } reactable( data = processed_jobs, @@ -79,16 +87,22 @@ server <- function(id, state) { process_job_data(job_data) } ) + ), + language = reactableLang( + noData = "No jobs found." ) ) + }) state$selected_job <- reactive({ index <- getReactableState("job_list_table", "selected") - list( - "key" = job_list_data()[index, ]$key, - "id" = job_list_data()[index, ]$id - ) + if (isTruthy(index)) { + list( + "key" = job_list_data()[index, ]$key, + "id" = job_list_data()[index, ]$id + ) + } }) }) From 903ca2f59a271160c99494b9eaa370d2c4c6249f Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Tue, 14 May 2024 18:02:16 +0530 Subject: [PATCH 04/13] fix: switching between two apps (one with jobs and one without) now works --- app/view/mod_job_list.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/view/mod_job_list.R b/app/view/mod_job_list.R index f4500e0..a06e643 100644 --- a/app/view/mod_job_list.R +++ b/app/view/mod_job_list.R @@ -97,7 +97,7 @@ server <- function(id, state) { state$selected_job <- reactive({ index <- getReactableState("job_list_table", "selected") - if (isTruthy(index)) { + if (isTruthy(index) && length(job_list_data()) > 0) { list( "key" = job_list_data()[index, ]$key, "id" = job_list_data()[index, ]$id From a7b21c276bd66aad23c6572c873dd723218f0a28 Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Mon, 27 May 2024 19:19:40 +0530 Subject: [PATCH 05/13] fix: order for imported functions --- app/view/mod_job_list.R | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/view/mod_job_list.R b/app/view/mod_job_list.R index a06e643..e016268 100644 --- a/app/view/mod_job_list.R +++ b/app/view/mod_job_list.R @@ -8,16 +8,16 @@ box::use( colDef, getReactableState, reactable, + reactableLang, reactableOutput, - renderReactable, - reactableLang + renderReactable ], shiny[ + isTruthy, moduleServer, NS, reactive, - req, - isTruthy + req ], shinycssloaders[withSpinner], ) From 4cbb208b97578abce86c4884c239db8b9773f63b Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Mon, 27 May 2024 20:06:12 +0530 Subject: [PATCH 06/13] feat: add control for app_role for Posit API --- app/logic/api_utils.R | 17 ++++++++++++++--- config.yml | 2 ++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/logic/api_utils.R b/app/logic/api_utils.R index 1ba0262..ca202fd 100644 --- a/app/logic/api_utils.R +++ b/app/logic/api_utils.R @@ -11,6 +11,8 @@ box::use( ], jsonlite[fromJSON], magrittr[`%>%`], + shiny[isTruthy], + yaml[read_yaml], ) #' Simple function to get the access token from environment @@ -45,12 +47,16 @@ get_api_url <- function( #' @param app_mode_filter Character list. The filter for app_mode in the API #' response. Default is list("shiny", "python-shiny", "quarto-shiny"). #' @param endpoint Character. Default is "content" +#' @param app_role Character. Read from the config.yml file. The possible value +#' for this can be "owner" or "viewer". You can leave it blank in the config as +#' "" to include both. #' @param dry_run Logical. Whether to dry run the API for debugging. #' Default is FALSE #' @export get_app_list <- function( app_mode_filter = list("shiny", "python-shiny", "quarto-shiny"), endpoint = "content", + app_role = read_yaml("config.yml", eval.expr = TRUE)$posit_api$app_role, dry_run = FALSE ) { @@ -66,14 +72,19 @@ get_app_list <- function( api_request %>% req_dry_run() } else { - api_request %>% + response <- api_request %>% req_perform() %>% resp_body_string() %>% fromJSON() %>% filter( - app_mode %in% app_mode_filter, - app_role == "owner" + app_mode %in% app_mode_filter ) + + if (isTruthy(app_role)) { + response <- response[response$app_role == app_role, ] + } else { + response + } } } diff --git a/config.yml b/config.yml index e829f27..0e6d13b 100644 --- a/config.yml +++ b/config.yml @@ -1,3 +1,5 @@ default: rhino_log_level: !expr Sys.getenv("RHINO_LOG_LEVEL", "INFO") rhino_log_file: !expr Sys.getenv("RHINO_LOG_FILE", NA) +posit_api: + app_role: "owner" From 55ebc75e8237bbdd8da791bf8d56280897611096 Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Mon, 27 May 2024 20:38:12 +0530 Subject: [PATCH 07/13] feat: extract functionality for job lists into helper functions --- app/logic/job_list_utils.R | 42 ++++++++++++++++++++++++++++++++++++-- app/view/mod_job_list.R | 35 ++++++++----------------------- 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/app/logic/job_list_utils.R b/app/logic/job_list_utils.R index 02bceb0..2687425 100644 --- a/app/logic/job_list_utils.R +++ b/app/logic/job_list_utils.R @@ -1,4 +1,11 @@ box::use( + dplyr[ + mutate, + select + ], + magrittr[ + `%>%` + ], shiny[ div, strong @@ -9,12 +16,43 @@ box::use( app/logic/general_utils[format_timestamp], ) +#' Process the dataframe for job list +#' @param job_list_data the job list data to process for the mod_job_list +#' @export +#' +process_job_data <- function(job_list_data) { + if (length(job_list_data)) { + 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 + ) + ) + } else { + data.frame( + job = character() + ) + } +} #' Function to process each row for the job table #' This creates the HTML for the row -#' +#' @param job_data the job_data for a single job #' @export -process_job_data <- function( +render_job_data <- function( job_data ) { job_info <- strsplit(job_data, "_-_")[[1]] diff --git a/app/view/mod_job_list.R b/app/view/mod_job_list.R index e016268..96f154a 100644 --- a/app/view/mod_job_list.R +++ b/app/view/mod_job_list.R @@ -24,7 +24,10 @@ box::use( box::use( app/logic/api_utils[get_job_list], - app/logic/job_list_utils[process_job_data], + app/logic/job_list_utils[ + process_job_data, + render_job_data + ], ) #' @export @@ -50,31 +53,9 @@ server <- function(id, state) { output$job_list_table <- renderReactable({ - if (length(job_list_data())) { - 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 - ) - ) - } else { - processed_jobs <- data.frame( - job = character() - ) - } + processed_jobs <- process_job_data( + job_list_data() + ) reactable( data = processed_jobs, @@ -84,7 +65,7 @@ server <- function(id, state) { columns = list( job = colDef( cell = function(job_data) { - process_job_data(job_data) + render_job_data(job_data) } ) ), From 13aa6629dd4836030c354268c689ddac341f2eb1 Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Mon, 27 May 2024 21:22:13 +0530 Subject: [PATCH 08/13] test: add unit tests for job_list_utils --- tests/testthat/test-job_list_utils.R | 53 ++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/testthat/test-job_list_utils.R diff --git a/tests/testthat/test-job_list_utils.R b/tests/testthat/test-job_list_utils.R new file mode 100644 index 0000000..bb79567 --- /dev/null +++ b/tests/testthat/test-job_list_utils.R @@ -0,0 +1,53 @@ +box::use( + app/logic/job_list_utils[ + process_job_data + ] +) + +describe("process_job_data()", { + + it("returns processed jobs when job_list_data is not empty", { + job_list <- data.frame( + id = c(1, 2), + key = c("key1", "key2"), + start_time = c("2021-01-01 00:00:00", "2021-01-02 00:00:00"), + end_time = c("2021-01-01 12:00:00", "2021-01-02 12:00:00") + ) + + expected_result <- data.frame( + job = c("1_-_key1_-_2021-01-01 00:00:00_-_2021-01-01 12:00:00", + "2_-_key2_-_2021-01-02 00:00:00_-_2021-01-02 12:00:00") + ) + + result <- process_job_data(job_list) + expect_equal(result, expected_result) + }) + + it("returns an empty dataframe when job_list_data is empty", { + job_list <- data.frame( + id = integer(), + key = character(), + start_time = character(), + end_time = character() + ) + + expected_result <- data.frame( + job = character() + ) + + result <- process_job_data(job_list) + expect_equal(result, expected_result) + }) + + it("handles NULL input gracefully", { + job_list <- NULL + + expected_result <- data.frame( + job = character() + ) + + result <- process_job_data(job_list) + expect_equal(result, expected_result) + }) + +}) From c7d0f064317946670bef41994d846f53d0a9a4ae Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Mon, 27 May 2024 21:22:21 +0530 Subject: [PATCH 09/13] chore: remove unused strings.yml --- strings.yml | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 strings.yml diff --git a/strings.yml b/strings.yml deleted file mode 100644 index d4c9b7c..0000000 --- a/strings.yml +++ /dev/null @@ -1,23 +0,0 @@ -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." From 4c4bca46b692b7951e63fc6c59679f246ed4b09a Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Tue, 28 May 2024 15:58:07 +0530 Subject: [PATCH 10/13] fix: add testthat imports to test file --- tests/testthat/test-job_list_utils.R | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/testthat/test-job_list_utils.R b/tests/testthat/test-job_list_utils.R index bb79567..89f36e3 100644 --- a/tests/testthat/test-job_list_utils.R +++ b/tests/testthat/test-job_list_utils.R @@ -1,7 +1,15 @@ +box::use( + testthat[ + describe, + expect_equal, + it + ], +) + box::use( app/logic/job_list_utils[ process_job_data - ] + ], ) describe("process_job_data()", { From 249e664678376f9b52df80ca3f0fccf619be2782 Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Tue, 28 May 2024 16:00:33 +0530 Subject: [PATCH 11/13] fix: use config.yml correctly with config package --- app/logic/api_utils.R | 4 ++-- config.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/logic/api_utils.R b/app/logic/api_utils.R index ca202fd..b128fdf 100644 --- a/app/logic/api_utils.R +++ b/app/logic/api_utils.R @@ -1,4 +1,5 @@ box::use( + config[get], dplyr[filter], glue[glue], httr2[ @@ -12,7 +13,6 @@ box::use( jsonlite[fromJSON], magrittr[`%>%`], shiny[isTruthy], - yaml[read_yaml], ) #' Simple function to get the access token from environment @@ -56,7 +56,7 @@ get_api_url <- function( get_app_list <- function( app_mode_filter = list("shiny", "python-shiny", "quarto-shiny"), endpoint = "content", - app_role = read_yaml("config.yml", eval.expr = TRUE)$posit_api$app_role, + app_role = get("app_role"), dry_run = FALSE ) { diff --git a/config.yml b/config.yml index 0e6d13b..8e5f085 100644 --- a/config.yml +++ b/config.yml @@ -1,5 +1,5 @@ default: rhino_log_level: !expr Sys.getenv("RHINO_LOG_LEVEL", "INFO") rhino_log_file: !expr Sys.getenv("RHINO_LOG_FILE", NA) -posit_api: app_role: "owner" + From 0b43878e4003f94f1102c1544637a12b296d4c29 Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Tue, 28 May 2024 16:03:59 +0530 Subject: [PATCH 12/13] chore: update README.md with a new Configuration section --- README.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 5a51fae..44f7257 100644 --- a/README.md +++ b/README.md @@ -4,28 +4,30 @@ The LogAnalyzer open-source app is a simple, plug and play application developed # 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. +- 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. ![](img/app_preview.gif) +# Configuration + +- `[app/logic/api_utils.R` - `get_app_list()]` Posit Connect differentiates apps with two `app_role` values: `owner` and `viewer`. You can toggle between these using the `config.yml` file. The set value is `owner`. If you want to use both together, you can simply set the value to a blank character `""`. + # 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. + +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). +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) +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. - + Subscribe for Shiny tutorials, exclusive articles, R/Shiny community events, and more. From 56014ecbe04662ed0031a6d4a94e4e901d09c2c6 Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Wed, 29 May 2024 13:28:11 +0530 Subject: [PATCH 13/13] fix: remove unused function imports --- app/view/mod_job_list.R | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/view/mod_job_list.R b/app/view/mod_job_list.R index 96f154a..c4a6fbf 100644 --- a/app/view/mod_job_list.R +++ b/app/view/mod_job_list.R @@ -1,8 +1,4 @@ box::use( - dplyr[ - mutate, - select - ], magrittr[`%>%`], reactable[ colDef,