|
| 1 | +library(shiny) |
| 2 | +library(bslib) |
| 3 | +library(shinyjs) |
| 4 | +library(connectapi) |
| 5 | +library(dplyr) |
| 6 | +library(glue) |
| 7 | +library(lubridate) |
| 8 | +library(tidyr) |
| 9 | + |
| 10 | +shinyOptions( |
| 11 | + cache = cachem::cache_disk("./app_cache/cache/", max_age = 60 * 60 * 8) |
| 12 | +) |
| 13 | + |
| 14 | +source("get_usage.R") |
| 15 | + |
| 16 | +ui <- page_fillable( |
| 17 | + useShinyjs(), |
| 18 | + theme = bs_theme(version = 5), |
| 19 | + |
| 20 | + card( |
| 21 | + card_header("Who is Visiting This Content?"), |
| 22 | + layout_sidebar( |
| 23 | + sidebar = sidebar( |
| 24 | + title = "No Filters Yet", |
| 25 | + open = FALSE, |
| 26 | + |
| 27 | + actionButton("clear_cache", "Clear Cache", icon = icon("refresh")) |
| 28 | + ), |
| 29 | + |
| 30 | + textInput( |
| 31 | + "content_guid", |
| 32 | + "Content GUID" |
| 33 | + ), |
| 34 | + |
| 35 | + h4( |
| 36 | + id = "guid_input_msg", |
| 37 | + "Please enter a content GUID" |
| 38 | + ), |
| 39 | + |
| 40 | + textOutput("summary_message"), |
| 41 | + |
| 42 | + tabsetPanel( |
| 43 | + id = "content_visit_tables", |
| 44 | + tabPanel( |
| 45 | + "List of Visits", |
| 46 | + tableOutput("all_visits") |
| 47 | + ), |
| 48 | + tabPanel( |
| 49 | + "Aggregated Visits", |
| 50 | + tableOutput("aggregated_visits") |
| 51 | + ) |
| 52 | + ) |
| 53 | + ) |
| 54 | + ) |
| 55 | +) |
| 56 | + |
| 57 | +server <- function(input, output, session) { |
| 58 | + # Cache invalidation button ---- |
| 59 | + cache <- cachem::cache_disk("./app_cache/cache/") |
| 60 | + observeEvent(input$clear_cache, { |
| 61 | + print("Cache cleared!") |
| 62 | + cache$reset() # Clears all cached data |
| 63 | + session$reload() # Reload the app to ensure fresh data |
| 64 | + }) |
| 65 | + |
| 66 | + observe({ |
| 67 | + if (nchar(input$content_guid) == 0) { |
| 68 | + show("guid_input_msg") |
| 69 | + hide("content_visit_tables") |
| 70 | + } else { |
| 71 | + hide("guid_input_msg") |
| 72 | + show("content_visit_tables") |
| 73 | + } |
| 74 | + }) |
| 75 | + |
| 76 | + # Loading and processing data ---- |
| 77 | + client <- connect() |
| 78 | + |
| 79 | + # Default dates. "This week" is best "common sense" best represented by six |
| 80 | + # days ago thru the end of today. Without these, content takes too long to |
| 81 | + # display on some servers. |
| 82 | + date_range <- reactive({ |
| 83 | + list( |
| 84 | + from_date = today() - ddays(6), |
| 85 | + to_date = today() |
| 86 | + ) |
| 87 | + }) |
| 88 | + |
| 89 | + content <- reactive({ |
| 90 | + # Grab the entire content data frame here and filter it using the pasted-in |
| 91 | + # GUID to obtain content title and other metadata, rather than making a |
| 92 | + # request to `v1/content/{GUID}`. If this were a prod, standalone dashboard, |
| 93 | + # might be better to call that endpoint. |
| 94 | + get_content(client) |
| 95 | + }) |> bindCache("static_key") |
| 96 | + |
| 97 | + user_names <- reactive({ |
| 98 | + get_users(client) |> |
| 99 | + mutate(full_name = paste(first_name, last_name)) |> |
| 100 | + select(user_guid = guid, full_name, username) |
| 101 | + }) |> bindCache("static_key") |
| 102 | + |
| 103 | + usage_data <- reactive({ |
| 104 | + get_usage( |
| 105 | + client, |
| 106 | + from = date_range()$from_date, |
| 107 | + to = date_range()$to_date + hours(23) + minutes(59) + seconds(59) |
| 108 | + ) |
| 109 | + }) |> bindCache(date_range()$from_date, date_range()$to_date) |
| 110 | + |
| 111 | + # Compute data |
| 112 | + all_visits_data <- reactive({ |
| 113 | + usage_data() |> |
| 114 | + filter(content_guid == input$content_guid) |> |
| 115 | + left_join(user_names(), by = "user_guid") |> |
| 116 | + replace_na(list(full_name = "[Anonymous]")) |> |
| 117 | + arrange(desc(timestamp)) |> |
| 118 | + select(timestamp, full_name, username) |
| 119 | + }) |> bindCache(date_range()$from_date, date_range()$to_date, input$content_guid) |
| 120 | + |
| 121 | + aggregated_visits_data <- reactive({ |
| 122 | + usage_data() |> |
| 123 | + filter(content_guid == input$content_guid) |> |
| 124 | + group_by(user_guid) |> |
| 125 | + summarize(n_visits = n()) |> |
| 126 | + left_join(user_names(), by = "user_guid") |> |
| 127 | + replace_na(list(full_name = "[Anonymous]")) |> |
| 128 | + arrange(desc(n_visits)) |> |
| 129 | + select(n_visits, full_name, username) |
| 130 | + }) |> bindCache(date_range()$from_date, date_range()$to_date, input$content_guid) |
| 131 | + |
| 132 | + summary_message <- reactive({ |
| 133 | + content_title <- content() |> |
| 134 | + filter(guid == input$content_guid) |> |
| 135 | + pull(title) |
| 136 | + hits <- all_visits_data() |
| 137 | + glue( |
| 138 | + "Content '{content_title}' had {nrow(hits)} between ", |
| 139 | + "{min(hits$timestamp)} and {max(hits$timestamp)}." |
| 140 | + ) |
| 141 | + }) |
| 142 | + |
| 143 | + |
| 144 | + output$summary_message <- renderText(summary_message()) |
| 145 | + output$all_visits <- renderTable( |
| 146 | + all_visits_data() |> |
| 147 | + transmute(timestamp = format(timestamp, "%Y-%m-%d %H:%M:%S"), full_name, username) |> |
| 148 | + rename("Time" = timestamp, "Full Name" = full_name, "Username" = username) |
| 149 | + ) |
| 150 | + output$aggregated_visits <- renderTable( |
| 151 | + aggregated_visits_data() |> |
| 152 | + rename("Total Visits" = n_visits, "Full Name" = full_name, "Username" = username) |
| 153 | + ) |
| 154 | +} |
| 155 | + |
| 156 | +shinyApp(ui, server) |
0 commit comments