Skip to content

Commit

Permalink
Merge pull request #17 from The-Strategy-Unit/15-content
Browse files Browse the repository at this point in the history
  • Loading branch information
matt-dray authored Jan 27, 2025
2 parents aa93cf6 + 6671969 commit 28aa6a7
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 121 deletions.
105 changes: 95 additions & 10 deletions R/api.R
Original file line number Diff line number Diff line change
@@ -1,45 +1,130 @@
# Make connection to Connect client
get_client <- function(
server_name = Sys.getenv("CONNECT_SERVER"),
api_key = Sys.getenv("CONNECT_API_KEY")
) {
connectapi::connect(server_name, api_key)
}

# Return a dataframe with guid and group name
get_groups <- function(client = get_client()) {
client |>
connectapi::get_groups() |>
dplyr::select(guid, name)
}

get_group_membership <- function(client = get_client(), groups = get_groups()) {
groups[["guid"]] |>
# Return a list of dataframes of user information, one named element per group
get_group_membership <- function(client = get_client()) {

groups <- get_groups(client)

groups_members <- groups[["guid"]] |>
purrr::set_names(groups[["name"]]) |>
purrr::map(\(guid) connectapi::get_group_members(client, guid))

# Retain empty groups by giving them '[None]' as a user
for (group in names(groups_members)) {
df_is_empty <- nrow(groups_members[[group]]) == 0
if (df_is_empty) { # i.e. no users were returned
groups_members[[group]] <- tibble::tibble(username = "[none]")
}
}

groups_members

}

get_user_groups <- function(group_membership = get_group_membership()) {
# Return a dataframe with one row per username and group membership
get_user_groups <- function(client = get_client()) {

group_membership <- get_group_membership(client)

group_membership |>
purrr::list_rbind(names_to = "group") |>
dplyr::select(username, group)

}

get_all_users <- function(client = get_client()) {
connectapi::get_users(client) |>
# Return a dataframe of user details (username, name, user_role, active_time)
get_all_users <- function(
client = get_client(),
include_guid = FALSE
) {

users <- connectapi::get_users(client) |>
dplyr::mutate(
name = glue::glue("{first_name} {last_name}") |>
stringr::str_squish()
) |>
dplyr::select(username, name, user_role, active_time)
dplyr::select(guid, username, name, user_role, active_time)

if (!include_guid) dplyr::select(users, -guid) else return(users)

}

get_all_users_groups <- function(
all_users = get_all_users(),
user_groups = get_user_groups()
) {
# Returns a dataframe of user details with one row per user and group
get_all_users_groups <- function(client = get_client()) {

all_users <- get_all_users(client)
user_groups <- get_user_groups(client)

all_users |>
dplyr::full_join(
user_groups,
by = "username",
relationship = "many-to-many"
) |>
dplyr::mutate(
last_login = as.character(format(active_time, "%Y-%m-%d")),
.after = user_role
) |>
dplyr::select(-active_time) |>
dplyr::mutate(
dplyr::across(
tidyselect::everything(),
\(column) tidyr::replace_na(column, "[none]")
)
) |>
dplyr::arrange(tolower(username), tolower(group))

}

# Returns a dataframe of content details, one row per content item
get_content <- function(client = get_client()) {
client |>
connectapi::get_content() |>
tidyr::hoist(owner, "username") |>
dplyr::mutate(
has_tags = purrr::map(tags, \(x) !is.null(x)),
has_tags = unlist(has_tags),
app_mode = dplyr::if_else(
content_category != "",
glue::glue("{app_mode} ({content_category})"),
app_mode
)
) |>
dplyr::select(
content_id = id,
name,
title,
description,
mode = app_mode,
has_tags,
last_deployed_time,
owner_username = username,
content_url,
dashboard_url
) |>
dplyr::arrange(dplyr::desc(as.numeric(content_id))) |>
dplyr::mutate(
last_deployed_time = format(last_deployed_time, "%Y-%m-%d %H:%M"),
dplyr::across(
dplyr::where(is.character),
\(col) dplyr::na_if(col, "")
),
dplyr::across(
tidyselect::everything(),
\(column) tidyr::replace_na(column, "[none]")
)
)
}
29 changes: 29 additions & 0 deletions R/utils.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Exit rendering if environment variables are missing (prevents error emails)
check_env_vars <- function(required_env_vars) {
if (any(Sys.getenv(required_env_vars) == "")) {
cat("One of the following environment variables was not set, so exiting \n\n")
cat(paste("*", required_env_vars, collapse = "\n"), "\n\n")
knitr::knit_exit()
}
}

# Create a {DT} datatable with common settings
create_dt <- function(dat, type = c("users", "content")) {
dat |>
DT::datatable(
filter = "top",
rownames = FALSE,
escape = FALSE, # for URLs
extensions = "Buttons",
options = list(
dom = "Bftipr",
autoWidth = TRUE,
buttons = list(
list(
extend = "csv",
title = glue::glue("{Sys.Date()}_su-posit-connect_{type}-lookup")
)
)
)
)
}
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

[<img src='https://img.shields.io/badge/Posit_Connect-deployed-447099?style=flat&labelColor=white&logo=Posit&logoColor=447099'>](https://connect.strategyunitwm.nhs.uk/posit-connect-people/)

A simple interface for a quick understanding of users and groups in Posit Connect. Overcomes missing functionality in the platform itself.
A simple interface for a quick understanding of users and content on our Posit Connect server.
Overcomes some missing functionality in the platform itself.

To run locally, you'll need to add an `.Renviron` file to the project root, which contains the keys specified in the provided `.Renviron.example` file. Contact the repo owner for the required values.
The report itself is [deployed to Connect](https://connect.strategyunitwm.nhs.uk/posit-connect-people/) on schedule (login and permissions required).

To run locally, you'll need to add an `.Renviron` file to the project root, which contains the keys specified in the provided `.Renviron.example` file.
Contact the Data Science team for the required values.
127 changes: 48 additions & 79 deletions index.qmd
Original file line number Diff line number Diff line change
@@ -1,110 +1,79 @@
---
title: 'Lookup: Posit Connect People'
title: "Lookup: Posit Connect People and Content"
date: "last-modified"
date-format: D MMM YYYY HH:mm
format: html
author: Data Science Team, The Strategy Unit
format:
html:
page-layout: full
execute:
echo: false
resource_files:
- R/api.R
- R/utils.R
---

```{r}
#| label: check-env-vars
#| results: "asis"
required_env_vars <- c("CONNECT_SERVER", "CONNECT_API_KEY")
if (any(Sys.getenv(required_env_vars) == "")) {
cat("One of the following environment variables was not set, so exiting \n\n")
cat(paste("*", required_env_vars, collapse = "\n"), "\n\n")
knitr::knit_exit()
}
list.files("R", "\\.R$", , TRUE) |> purrr::walk(source)
check_env_vars(c("CONNECT_SERVER", "CONNECT_API_KEY"))
```

```{r}
#| label: prepare-workspace
#| label: prepare-variables
#| include: false
list.files("R", "\\.R$", , TRUE) |> purrr::walk(source)
server_name <- Sys.getenv("CONNECT_SERVER")
people_path <- glue::glue("{server_name}connect/#/people/")
dat <- get_all_users_groups() |>
tidyr::replace_na(list(username = "[No user]", group = "[No group]"))
```
content_path <- glue::glue("{server_name}connect/#/content/")
## Purpose
client <- get_client()
users_groups <- get_all_users_groups(client)
content <- get_content(client)
A quick lookup of [users and groups](`r people_path`) on the server `r server_name`. Find [the source on GitHub](https://github.com/The-Strategy-Unit/posit-connect-people/).
n_users <- users_groups |>
dplyr::filter(username != "[none]") |>
dplyr::distinct() |>
nrow()
## By user
n_groups <- users_groups |>
dplyr::filter(group != "[none]") |>
dplyr::distinct() |>
nrow()
```{r}
#| label: by-user
dat |>
dplyr::select(username, group) |>
dplyr::arrange(tolower(username), tolower(group)) |>
reactable::reactable(
groupBy = "username",
columns = list(
username = reactable::colDef(
filterable = TRUE,
filterInput = function(values, name) {
htmltools::tags$select(
# Set to undefined to clear the filter
onchange = sprintf("Reactable.setFilter('cars-select', '%s', event.target.value || undefined)", name),
# "All" has an empty value to clear the filter, and is the default option
htmltools::tags$option(value = "", "All"),
lapply(unique(values), htmltools::tags$option),
"aria-label" = sprintf("Filter %s", name),
style = "width: 100%; height: 28px;"
)
}
)
)
)
n_content <- nrow(content)
```

## By group
## Purpose

This page contains tabular lookups of [users](`r people_path`) and [content](`r content_path`) on the server [`r server_name`](`r server_name`). Find [the source on GitHub](https://github.com/The-Strategy-Unit/posit-connect-people/).

## Users and groups

This table shows one row per user and group. There are `r n_users` users and `r n_groups` groups. Note that it's possible for users not to have a group and vice versa. You can search, sort and filter the data and click the 'CSV' button to download the current view.

```{r}
#| label: by-group
dat |>
dplyr::select(group, username) |>
dplyr::arrange(tolower(group), tolower(username)) |>
reactable::reactable(
groupBy = "group",
columns = list(
group = reactable::colDef(
filterable = TRUE,
filterInput = function(values, name) {
htmltools::tags$select(
# Set to undefined to clear the filter
onchange = sprintf("Reactable.setFilter('cars-select', '%s', event.target.value || undefined)", name),
# "All" has an empty value to clear the filter, and is the default option
htmltools::tags$option(value = "", "All"),
lapply(unique(values), htmltools::tags$option),
"aria-label" = sprintf("Filter %s", name),
style = "width: 100%; height: 28px;"
)
}
)
)
)
#| label: users-table
users_groups |>
dplyr::mutate(dplyr::across(dplyr::where(is.character), as.factor)) |>
create_dt("users")
```

## All data
## Content

This table shows one row per content item. There are `r n_content` content items. You can search, sort and filter the data and click the 'CSV' button to download the current view. Click the links in the `*_url` columns to open the item with or without the Posit Connect dashboard framing the content.

```{r}
#| label: all-data
htmltools::browsable(
htmltools::tagList(
htmltools::tags$button(
"Download CSV",
onclick = "Reactable.downloadDataCSV('data-table', 'posit-connect-people.csv')"
),
reactable::reactable(
dat |> dplyr::arrange(tolower(username), tolower(group)),
filterable = TRUE,
elementId = "data-table",
)
)
)
#| label: content-table
content |>
dplyr::mutate(
content_url = glue::glue("<a href='{content_url}'>Link</a>"),
dashboard_url = glue::glue("<a href='{dashboard_url}'>Link</a>"),
dplyr::across(dplyr::where(is.character), as.factor)
) |>
create_dt("content")
```
Loading

0 comments on commit 28aa6a7

Please sign in to comment.