From 0ecce97c2dd99b38aa6b43fe7a172b362a44b534 Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Tue, 11 Jun 2024 15:41:28 +0530 Subject: [PATCH 01/10] fix: handle situation when the Connect instance has no apps --- app/main.R | 10 +------- app/view/mod_app_table.R | 51 ++++++++++++++++++++++++++++------------ 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/app/main.R b/app/main.R index d221881..c021a57 100644 --- a/app/main.R +++ b/app/main.R @@ -79,20 +79,12 @@ server <- function(id) { mod_app_table$server( "app_table", - app_list() %>% - select( - guid, - name, - r_version, - dashboard_url, - last_deployed_time - ), + app_list(), state ) observeEvent(state$selected_app()$guid, { if (isTruthy(state$selected_app()$guid)) { - output$job_list_pane <- renderUI({ mod_job_list$ui(ns("job_list")) }) diff --git a/app/view/mod_app_table.R b/app/view/mod_app_table.R index 02d4556..7cc9e2e 100644 --- a/app/view/mod_app_table.R +++ b/app/view/mod_app_table.R @@ -8,10 +8,12 @@ box::use( colDef, getReactableState, reactable, + reactableLang, reactableOutput, renderReactable ], shiny[ + isTruthy, moduleServer, NS, reactive @@ -41,23 +43,37 @@ server <- function(id, app_list, state) { output$app_table <- renderReactable({ - processed_apps <- app_list %>% - mutate( - name = paste( + if (length(app_list) > 0) { + processed_apps <- app_list %>% + select( + guid, name, - r_version, - dashboard_url, - last_deployed_time, - sep = "_-_" - ) - ) %>% - select( - -c( r_version, dashboard_url, last_deployed_time + ) %>% + mutate( + name = paste( + name, + r_version, + dashboard_url, + last_deployed_time, + sep = "_-_" + ) + ) %>% + select( + -c( + r_version, + dashboard_url, + last_deployed_time + ) ) + } else { + processed_apps <- data.frame( + guid = character(), + name = character() ) + } reactable( data = processed_apps, @@ -75,16 +91,21 @@ server <- function(id, app_list, state) { process_app_data(app_data) } ) + ), + language = reactableLang( + noData = "No apps found." ) ) }) state$selected_app <- reactive({ index <- getReactableState("app_table", "selected") - list( - "guid" = app_list[index, ]$guid, - "name" = app_list[index, ]$name - ) + if (isTruthy(index) && length(app_list > 0)) { + list( + "guid" = app_list[index, ]$guid, + "name" = app_list[index, ]$name + ) + } }) }) From 332c218a45d7b4075b4197c59210f25615b860de Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Tue, 11 Jun 2024 15:45:18 +0530 Subject: [PATCH 02/10] fix: add new illustration; move illustrations into separate subfolder --- app/main.R | 2 +- app/static/{ => illustrations}/empty_state.svg | 0 app/static/illustrations/missing_apps.svg | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) rename app/static/{ => illustrations}/empty_state.svg (100%) create mode 100644 app/static/illustrations/missing_apps.svg diff --git a/app/main.R b/app/main.R index c021a57..4b901aa 100644 --- a/app/main.R +++ b/app/main.R @@ -118,7 +118,7 @@ server <- function(id) { "Select an application and a job to view logs" ), img( - src = "static/empty_state.svg", + src = "static/illustrations/empty_state.svg", class = "empty-state-image", alt = "Select an application and a job to view logs" ) diff --git a/app/static/empty_state.svg b/app/static/illustrations/empty_state.svg similarity index 100% rename from app/static/empty_state.svg rename to app/static/illustrations/empty_state.svg diff --git a/app/static/illustrations/missing_apps.svg b/app/static/illustrations/missing_apps.svg new file mode 100644 index 0000000..df10112 --- /dev/null +++ b/app/static/illustrations/missing_apps.svg @@ -0,0 +1 @@ +server down \ No newline at end of file From c0b277281ca13b9cac0cd05efe061010031da8a9 Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Tue, 11 Jun 2024 16:03:47 +0530 Subject: [PATCH 03/10] feat: add second empty state for state where apps are not in a data.frame feat: create new empty_state_utils.R with function for generating new empty states --- app/logic/empty_state_utils.R | 33 ++++++++++++++++++++++++++++++++ app/main.R | 36 +++++++++++++++++++++-------------- app/view/mod_app_table.R | 2 +- 3 files changed, 56 insertions(+), 15 deletions(-) create mode 100644 app/logic/empty_state_utils.R diff --git a/app/logic/empty_state_utils.R b/app/logic/empty_state_utils.R new file mode 100644 index 0000000..f5d6646 --- /dev/null +++ b/app/logic/empty_state_utils.R @@ -0,0 +1,33 @@ +box::use( + 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 +#' @export +generate_empty_state_ui <- function( + text = "Select an application and a job to view logs", + image_path = "static/illustrations/empty_state.svg" +) + { + renderUI({ + div( + class = "empty-state-container", + p( + class = "empty-state-text", + text + ), + img( + src = image_path, + class = "empty-state-image", + alt = text + ) + ) + }) +} diff --git a/app/main.R b/app/main.R index 4b901aa..e54de83 100644 --- a/app/main.R +++ b/app/main.R @@ -25,6 +25,7 @@ box::use( box::use( app/logic/api_utils[get_app_list], + app/logic/empty_state_utils[generate_empty_state_ui], app/view/mod_app_table, app/view/mod_header, app/view/mod_job_list, @@ -84,7 +85,9 @@ server <- function(id) { ) observeEvent(state$selected_app()$guid, { + if (isTruthy(state$selected_app()$guid)) { + output$job_list_pane <- renderUI({ mod_job_list$ui(ns("job_list")) }) @@ -93,12 +96,16 @@ server <- function(id) { "job_list", state ) + } else { + removeUI(ns("job_list_pane")) + } - }, ignoreInit = TRUE, ignoreNULL = TRUE) + }, ignoreInit = FALSE, ignoreNULL = FALSE) observeEvent(state$selected_job()$key, { + if (isTruthy(state$selected_job()$key)) { output$logs_pane <- renderUI({ @@ -110,21 +117,22 @@ server <- function(id) { state ) } else { - output$logs_pane <- renderUI({ - div( - class = "empty-state-container", - p( - class = "empty-state-text", - "Select an application and a job to view logs" - ), - img( - src = "static/illustrations/empty_state.svg", - class = "empty-state-image", - alt = "Select an application and a job to view logs" - ) + + if (class(app_list()) != "data.frame") { + empty_state <- generate_empty_state_ui( + text = "Oops! Can't read apps from Posit Connect.", + image_path = "static/illustrations/missing_apps.svg" ) - }) + } else { + empty_state <- 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 } + }, ignoreInit = FALSE, ignoreNULL = FALSE) }) diff --git a/app/view/mod_app_table.R b/app/view/mod_app_table.R index 7cc9e2e..fbf93bd 100644 --- a/app/view/mod_app_table.R +++ b/app/view/mod_app_table.R @@ -43,7 +43,7 @@ server <- function(id, app_list, state) { output$app_table <- renderReactable({ - if (length(app_list) > 0) { + if (length(app_list) > 0 && class(app_list) == "data.frame") { processed_apps <- app_list %>% select( guid, From 424e98a0f9a069273cd0b36a56fcd799dc74b7bf Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Tue, 11 Jun 2024 16:04:03 +0530 Subject: [PATCH 04/10] fix: remove ghost vertical scroll bar from the ui --- app/static/css/app.min.css | 2 +- app/styles/main.scss | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/static/css/app.min.css b/app/static/css/app.min.css index 8410136..1060d16 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}.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:#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} diff --git a/app/styles/main.scss b/app/styles/main.scss index e32d48c..17d49ff 100644 --- a/app/styles/main.scss +++ b/app/styles/main.scss @@ -10,6 +10,10 @@ font-family: "Maven Pro", sans-serif; } +body { + overflow: hidden; +} + .vertical-line { border-left: 1px $grey2-border solid; height: 80%; From f65a2f6d30427232b49d3359f5bec7f4cadaf85d Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Tue, 11 Jun 2024 16:09:58 +0530 Subject: [PATCH 05/10] chore: fix strings on app chore: update README.md with FAQs --- README.md | 6 ++++++ app/main.R | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 44f7257..aaea915 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,12 @@ The LogAnalyzer open-source app is a simple, plug and play application developed - `[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 `""`. +# FAQs + +- 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. + # 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. diff --git a/app/main.R b/app/main.R index e54de83..0ada29e 100644 --- a/app/main.R +++ b/app/main.R @@ -125,7 +125,7 @@ server <- function(id) { ) } else { empty_state <- generate_empty_state_ui( - text = "Select an application and a job to view logs", + text = "Select an application and a job to view logs.", image_path = "static/illustrations/empty_state.svg" ) } From 02ba4d8f56916ae2b3bd3773744714332a2d30bc Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Tue, 11 Jun 2024 16:14:08 +0530 Subject: [PATCH 06/10] chore: code linting --- app/logic/empty_state_utils.R | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/logic/empty_state_utils.R b/app/logic/empty_state_utils.R index f5d6646..2671eaf 100644 --- a/app/logic/empty_state_utils.R +++ b/app/logic/empty_state_utils.R @@ -14,8 +14,7 @@ box::use( generate_empty_state_ui <- function( text = "Select an application and a job to view logs", image_path = "static/illustrations/empty_state.svg" -) - { +) { renderUI({ div( class = "empty-state-container", From 1c9ae9da64f7550e4963f470925cca141ed0765e Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Wed, 12 Jun 2024 17:37:52 +0530 Subject: [PATCH 07/10] fix: move renderUI outside of generate_empty_state_ui() --- app/logic/empty_state_utils.R | 24 +++++++++++------------- app/main.R | 20 ++++++++++++-------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/app/logic/empty_state_utils.R b/app/logic/empty_state_utils.R index 2671eaf..da6e1df 100644 --- a/app/logic/empty_state_utils.R +++ b/app/logic/empty_state_utils.R @@ -15,18 +15,16 @@ generate_empty_state_ui <- function( text = "Select an application and a job to view logs", image_path = "static/illustrations/empty_state.svg" ) { - renderUI({ - div( - class = "empty-state-container", - p( - class = "empty-state-text", - text - ), - img( - src = image_path, - class = "empty-state-image", - alt = text - ) + div( + class = "empty-state-container", + p( + class = "empty-state-text", + text + ), + img( + src = image_path, + class = "empty-state-image", + alt = text ) - }) + ) } diff --git a/app/main.R b/app/main.R index 0ada29e..fc2ec62 100644 --- a/app/main.R +++ b/app/main.R @@ -119,15 +119,19 @@ server <- function(id) { } else { if (class(app_list()) != "data.frame") { - empty_state <- generate_empty_state_ui( - text = "Oops! Can't read apps from Posit Connect.", - image_path = "static/illustrations/missing_apps.svg" - ) + 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 <- generate_empty_state_ui( - text = "Select an application and a job to view logs.", - image_path = "static/illustrations/empty_state.svg" - ) + 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 From 33f64c7076db11256a968dd53c889b4b33e4eeaa Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Wed, 12 Jun 2024 17:38:10 +0530 Subject: [PATCH 08/10] fix: add trailing comma to box::use() --- app/logic/empty_state_utils.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/logic/empty_state_utils.R b/app/logic/empty_state_utils.R index da6e1df..3acbd10 100644 --- a/app/logic/empty_state_utils.R +++ b/app/logic/empty_state_utils.R @@ -4,7 +4,7 @@ box::use( img, p, renderUI - ] + ], ) #' @description Function to generate an empty state UI From ec075a9df7da6a6b39657c76e3512e64da1823f3 Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Wed, 12 Jun 2024 17:38:43 +0530 Subject: [PATCH 09/10] chore: remove default value for ignoreInit --- app/main.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/main.R b/app/main.R index fc2ec62..8578f3b 100644 --- a/app/main.R +++ b/app/main.R @@ -102,7 +102,7 @@ server <- function(id) { removeUI(ns("job_list_pane")) } - }, ignoreInit = FALSE, ignoreNULL = FALSE) + }, ignoreNULL = FALSE) observeEvent(state$selected_job()$key, { @@ -137,7 +137,7 @@ server <- function(id) { output$logs_pane <- empty_state } - }, ignoreInit = FALSE, ignoreNULL = FALSE) + }, ignoreNULL = FALSE) }) } From 2398cc4560bb32a605a0465e8dee3a63daea93a9 Mon Sep 17 00:00:00 2001 From: deepanshkhurana Date: Wed, 12 Jun 2024 17:40:15 +0530 Subject: [PATCH 10/10] fix: use inherits instead of class --- app/main.R | 2 +- app/view/mod_app_table.R | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/main.R b/app/main.R index 8578f3b..4adf1dd 100644 --- a/app/main.R +++ b/app/main.R @@ -118,7 +118,7 @@ server <- function(id) { ) } else { - if (class(app_list()) != "data.frame") { + if (!inherits(app_list(), "data.frame")) { empty_state <- renderUI({ generate_empty_state_ui( text = "Oops! Can't read apps from Posit Connect.", diff --git a/app/view/mod_app_table.R b/app/view/mod_app_table.R index fbf93bd..52c6e0c 100644 --- a/app/view/mod_app_table.R +++ b/app/view/mod_app_table.R @@ -43,7 +43,7 @@ server <- function(id, app_list, state) { output$app_table <- renderReactable({ - if (length(app_list) > 0 && class(app_list) == "data.frame") { + if (length(app_list) > 0 && inherits(app_list, "data.frame")) { processed_apps <- app_list %>% select( guid,