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/logic/empty_state_utils.R b/app/logic/empty_state_utils.R new file mode 100644 index 0000000..3acbd10 --- /dev/null +++ b/app/logic/empty_state_utils.R @@ -0,0 +1,30 @@ +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" +) { + 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 d221881..4adf1dd 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, @@ -79,18 +80,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({ @@ -101,12 +96,16 @@ server <- function(id) { "job_list", state ) + } else { + removeUI(ns("job_list_pane")) + } - }, ignoreInit = TRUE, ignoreNULL = TRUE) + }, ignoreNULL = FALSE) observeEvent(state$selected_job()$key, { + if (isTruthy(state$selected_job()$key)) { output$logs_pane <- renderUI({ @@ -118,22 +117,27 @@ 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/empty_state.svg", - class = "empty-state-image", - alt = "Select an application and a job to view logs" + + 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 } - }, ignoreInit = FALSE, ignoreNULL = FALSE) + + }, ignoreNULL = FALSE) }) } 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/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 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%; diff --git a/app/view/mod_app_table.R b/app/view/mod_app_table.R index 02d4556..52c6e0c 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 && inherits(app_list, "data.frame")) { + 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 + ) + } }) })