Skip to content

Commit

Permalink
feat: add functioning / barebones version of the app
Browse files Browse the repository at this point in the history
  • Loading branch information
DeepanshKhurana committed Feb 21, 2024
1 parent 55946e4 commit de7ce9b
Show file tree
Hide file tree
Showing 20 changed files with 1,070 additions and 14 deletions.
184 changes: 184 additions & 0 deletions app/logic/api_utils.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
box::use(
httr2[
request,
req_auth_bearer_token,
req_dry_run,
req_perform,
resp_body_string,
req_user_agent
],
magrittr[`%>%`],
jsonlite[fromJSON],
dplyr[filter],
glue[glue]
)

#' Simple function to get the access token from environment
#' @return Character token, if present
get_access_token <- function() {
token <- Sys.getenv(
"CONNECT_API_KEY",
unset = "NO_TOKEN"
)
if (token == "NO_TOKEN" || token == "") {
stop("Are you sure your CONNECT_API_KEY is set?")
} else {
token
}
}

#' Simple function to make an API url
#' @param host Character. Default CONNECT_SERVER set as an envvar
#' @param endpoint Character. Default is "content"
#' @param versioned Logical. Whether to use versioned API. Default is FALSE
#' @return url for the API
get_api_url <- function(
host = Sys.getenv("CONNECT_SERVER"),
endpoint = "content",
version = "v1"
) {
glue("{host}__api__/{version}/{endpoint}/")
}

#' 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 url Character. The URL for API endpoint
#' @param dry_run Logical. Whether to dry run the API for debugging.
#' Default is FALSE
#' @export
get_app_list <- function(
app_mode_filter = "shiny",
endpoint = "content",
dry_run = FALSE
) {

url <- get_api_url(
endpoint = endpoint
)

api_request <- request(url) %>%
req_user_agent("LogAnalyzer") %>%
req_auth_bearer_token(get_access_token())

if (dry_run) {
api_request %>%
req_dry_run()
} else {
api_request %>%
req_perform() %>%
resp_body_string() %>%
fromJSON() %>%
filter(app_mode == app_mode_filter)
}
}

#' Function to get a list of all jobs for a specific app
#'
#' @param guid Character. The guid for the app in question
#' @param url Character. The URL for API endpoint
#' @param dry_run Logical. Whether to dry run the API for debugging.
#' Default is FALSE
#' @export
get_job_list <- function(
guid = NULL,
endpoint = "content",
dry_run = FALSE
) {

url <- get_api_url(
endpoint = endpoint
)

api_request <- request(
glue("{url}{guid}/jobs")
) %>%
req_user_agent("LogAnalyzer") %>%
req_auth_bearer_token(get_access_token())

if (dry_run) {
api_request %>%
req_dry_run()
} else {
api_request %>%
req_perform() %>%
resp_body_string() %>%
fromJSON()
}
}

#' Function to get a list of all logs for a job for a specific app
#'
#' @param guid Character. The guid for the app in question
#' @param job_key Character. The key for the job in question
#' @param url Character. The URL for API endpoint
#' @param tail Logical. Whether to show the tail only for the logs
#' @param dry_run Logical. Whether to dry run the API for debugging.
#' Default is FALSE
#' @export
get_job_logs <- function(
guid = NULL,
job_key = NULL,
endpoint = "content",
tail = FALSE,
dry_run = FALSE
) {

url <- get_api_url(
endpoint = endpoint
)

api_request <- request(
glue("{url}{guid}/jobs/{job_key}/{ifelse(tail, 'tail', 'log')}")
) %>%
req_user_agent("LogAnalyzer") %>%
req_auth_bearer_token(get_access_token())

if (dry_run) {
api_request %>%
req_dry_run()
} else {
logs <- api_request %>%
req_perform() %>%
resp_body_string() %>%
fromJSON()
logs["entries"] %>%
data.frame()
}
}

#' Function to download the logfile for a specific app
#'
#' @param guid Character. The guid for the app in question
#' @param job_key Character. The key for the job in quesrtion
#' @param url Character. The URL for API endpoint
#' @param dry_run Logical. Whether to dry run the API for debugging.
#' Default is FALSE
#' @export
download_job_logs <- function(
guid = NULL,
job_key = NULL,
endpoint = "content",
dry_run = FALSE
) {

url <- get_api_url(
endpoint = endpoint
)

api_request <- request(
glue("{url}{guid}/jobs/{job_key}/download")
) %>%
req_user_agent("LogAnalyzer") %>%
req_auth_bearer_token(get_access_token())

if (dry_run) {
api_request %>%
req_dry_run()
} else {
api_request %>%
req_perform() %>%
resp_body_string()
}
}
55 changes: 55 additions & 0 deletions app/logic/app_table_utils.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
box::use(
shiny[
div,
span,
icon,
a,
strong
],
glue[glue]
)

box::use(
app/logic/general_utils[format_timestamp]
)

#' Function to process each row for the app table
#' This creates the HTML for the row
#'
#' @export
process_app_data <- function(
app_data
) {
app_info <- strsplit(app_data, "_-_")[[1]]
div(
class = "app-entry",
div(
class = "app-title",
span(
app_info[1],
a(
href = app_info[3],
class = "app-link",
icon(
name = "arrow-up-right-from-square",
class = "app-link-icon"
),
target = "_blank"
)
)
),
div(
class = "app-metadata",
div(
class = "app-last-deployed",
strong("Last Deployed: "),
format_timestamp(app_info[4])
),
span(
class = "app-r-version",
strong("R version: "),
app_info[2]
)
)
)
}
39 changes: 39 additions & 0 deletions app/logic/general_utils.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#' Function to check if a string of log text has error keywords
#'
#' @param text Character. The log string
#' @param wordlist Character vector. List of keywords to scan. Default
#' list is `c("halt", "err", "terminat", "not found")`
#' @param ignore_case Logical. Whether to ignore the case for words
#' Default is TRUE
#' @export
check_text_error <- function(
text,
wordlist = c("halt", "err", "terminat", "not found"),
ignore_case = TRUE
) {
grepl(
paste(wordlist, collapse = "|"),
text,
ignore.case = ignore_case
)
}

#' Function to convert timestamp between formats
#'
#' @param timestamp Character. The timestamp string
#' @param from Character. Original format. Default is "%Y-%m-%dT%H:%M:%OSZ"
#' @param to Character. New format. Default is "%Y-%m-%d %H:%M:%S"
#' @export
format_timestamp <- function(
timestamp,
from = "%Y-%m-%dT%H:%M:%OSZ",
to = "%Y-%m-%d %H:%M:%S"
) {
format(
as.POSIXct(
timestamp,
format = from
),
format = to
)
}
47 changes: 47 additions & 0 deletions app/logic/job_list_utils.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
box::use(
shiny[
div,
span,
icon,
a,
strong
],
glue[glue]
)

box::use(
app/logic/general_utils[format_timestamp]
)


#' Function to process each row for the job table
#' This creates the HTML for the row
#'
#' @export
process_job_data <- function(
job_data
) {
job_info <- strsplit(job_data, "_-_")[[1]]
div(
class = "job-entry",
div(
class = "job-id",
job_info[1]
),
div(
class = "job-key",
strong("Key: "),
job_info[2]
),
div(
class = "job-start-time",
strong("Start: "),
format_timestamp(job_info[3], "%Y-%m-%dT%H:%M:%S")
),
div(
class = "job-end-time",
strong("End: "),
format_timestamp(job_info[4], "%Y-%m-%dT%H:%M:%S")
)
)
}
55 changes: 55 additions & 0 deletions app/logic/logs_utils.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
box::use(
glue[glue],
shiny[
icon,
div
]
)

box::use(
app/logic/general_utils[check_text_error, format_timestamp]
)

#' Function to process each row for the log table
#' This creates the HTML for the row
#'
#' @export
process_log_data <- function(
log_data
) {
log_info <- strsplit(log_data, "_-_")[[1]]
status <- get_status_info(log_info[1], log_info[3])
div(
class = glue("log-entry {status[1]}-highlight"),
icon(
name = status[2],
class = glue(
"log-status {status[1]}-text fa-solid"
),
),
div(
class = "log-info-block",
div(
class = glue("log-info {status[1]}-text"),
log_info[3]
),
div(
class = "log-time",
format_timestamp(log_info[2])
)
)
)
}

get_status_info <- function(
output_type,
log_data
) {
if (output_type == "stdout") {
c("green", "circle-info")
} else if (output_type == "stderr" && check_text_error(log_data)) {
c("red", "circle-xmark")
} else {
c("yellow", "circle-info")
}
}
Loading

0 comments on commit de7ce9b

Please sign in to comment.