Required classes for our style of building/running shiny applications (no documentation)
This commit is contained in:
@@ -9,3 +9,10 @@ License: GPL (>= 3)
|
|||||||
Encoding: UTF-8
|
Encoding: UTF-8
|
||||||
Roxygen: list(markdown = TRUE)
|
Roxygen: list(markdown = TRUE)
|
||||||
RoxygenNote: 7.3.3
|
RoxygenNote: 7.3.3
|
||||||
|
Imports:
|
||||||
|
R6,
|
||||||
|
rlang,
|
||||||
|
shiny
|
||||||
|
Suggests:
|
||||||
|
AVSDevR.DBClient,
|
||||||
|
AVSDevR.ShinyDisconnectHandler
|
||||||
|
|||||||
12
NAMESPACE
12
NAMESPACE
@@ -1,2 +1,14 @@
|
|||||||
# Generated by roxygen2: do not edit by hand
|
# Generated by roxygen2: do not edit by hand
|
||||||
|
|
||||||
|
export(AdminLTEApplication)
|
||||||
|
export(FrameApplication)
|
||||||
|
export(SessionLogger)
|
||||||
|
export(SessionLoggerConsole)
|
||||||
|
export(SessionLoggerDB)
|
||||||
|
export(UFApplication)
|
||||||
|
export(UFDashboardApplication)
|
||||||
|
export(UFFrameApplication)
|
||||||
|
import(R6)
|
||||||
|
import(rlang)
|
||||||
|
import(shiny)
|
||||||
|
importFrom(R6,R6Class)
|
||||||
|
|||||||
5
R/aaa.R
Normal file
5
R/aaa.R
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#' @importFrom R6 R6Class
|
||||||
|
#' @import R6
|
||||||
|
#' @import rlang
|
||||||
|
#' @import shiny
|
||||||
|
NULL
|
||||||
31
R/adminLTEApplication.R
Normal file
31
R/adminLTEApplication.R
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
#' @export
|
||||||
|
AdminLTEApplication <- R6::R6Class(
|
||||||
|
"AdminLTEApplication",
|
||||||
|
inherit = FrameApplication,
|
||||||
|
public = list(
|
||||||
|
withSkin = function(skin) {
|
||||||
|
private$skin <- skin
|
||||||
|
invisible(self)
|
||||||
|
},
|
||||||
|
frameUI = function() {
|
||||||
|
super$ui()
|
||||||
|
},
|
||||||
|
ui = function() {
|
||||||
|
ui <- shiny::tags$body(
|
||||||
|
class = paste0("hold-transition skin-", private$skin),
|
||||||
|
`data-skin` = private$skin,
|
||||||
|
style = "min-height: 611px; padding-top: 20px",
|
||||||
|
shinydashboardPlus:::addDeps(
|
||||||
|
super$ui(),
|
||||||
|
md = FALSE,
|
||||||
|
options = NULL
|
||||||
|
)
|
||||||
|
)
|
||||||
|
shiny:::setLang(ui, private$language)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
private = list(
|
||||||
|
skin = "blue"
|
||||||
|
)
|
||||||
|
)
|
||||||
143
R/frameApplication.R
Normal file
143
R/frameApplication.R
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
|
||||||
|
#' @export
|
||||||
|
FrameApplication <- R6::R6Class(
|
||||||
|
"FrameApplication",
|
||||||
|
public = list(
|
||||||
|
initialize = function(appPage = NULL, language = NULL) {
|
||||||
|
if (!is.null(appPage)) {
|
||||||
|
private$appPage <- appPage
|
||||||
|
} else {
|
||||||
|
private$appPage <- modules::use("pages/application.R")
|
||||||
|
}
|
||||||
|
if (!is.null(language)) {
|
||||||
|
private$language <- language
|
||||||
|
}
|
||||||
|
invisible(self)
|
||||||
|
},
|
||||||
|
withSessionLogger = function(sessionLogger) {
|
||||||
|
if (!inherits(sessionLogger, "SessionLogger")) {
|
||||||
|
rlang::abort("sessionLogger is not an instance of SessionLogger class")
|
||||||
|
}
|
||||||
|
private$sessionLogger <- sessionLogger
|
||||||
|
invisible(self)
|
||||||
|
},
|
||||||
|
withBootstrap4Shim = function() {
|
||||||
|
private$withBS4 <- TRUE
|
||||||
|
invisible(self)
|
||||||
|
},
|
||||||
|
getSessionDBClient = function(
|
||||||
|
session, db_config = NULL, allowFailure = FALSE
|
||||||
|
) {
|
||||||
|
# Check DBClient package is installed
|
||||||
|
if (!rlang::is_installed("AVSDevR.DBClient")) {
|
||||||
|
return(NULL)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check connection configuration is available
|
||||||
|
db_conn <- AVSDevR.DBClient::DBConnection$new()
|
||||||
|
if (is.null(db_config)) {
|
||||||
|
if (!db_conn$loadConfiguration(TRUE)) {
|
||||||
|
return(NULL)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
db_conn$configure(db_config)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try and connect
|
||||||
|
if (!db_conn$connect()) {
|
||||||
|
if (allowFailure) {
|
||||||
|
rlang::abort(
|
||||||
|
"Failed to connect to database - database not available or not \
|
||||||
|
created"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return(NULL)
|
||||||
|
}
|
||||||
|
db_conn$disconnectOnSessionEnd(session)
|
||||||
|
|
||||||
|
# Create the client instance
|
||||||
|
AVSDevR.DBClient::DBClient$new(db_conn)
|
||||||
|
},
|
||||||
|
ui = function() {
|
||||||
|
if (file.exists("widgets/menu.R")) {
|
||||||
|
# Multi-frame application
|
||||||
|
frame <- do.call(tabItems, private$appPage$tabs("application"))
|
||||||
|
frame_tabs <- sapply(frame$children, function(tab) {
|
||||||
|
tab$attr$`data-tabName`
|
||||||
|
})
|
||||||
|
|
||||||
|
menu <- modules::use("widgets/menu.R")
|
||||||
|
menu_tabs <- unname(sapply(
|
||||||
|
menu$ui, function(ti) {
|
||||||
|
ti$children[[1]]$attr$`data-value`
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
if (!identical(sort(menu_tabs), sort(frame_tabs))) {
|
||||||
|
rlang::abort(
|
||||||
|
"Tab names in widgets/menu.R do not match tab names in \
|
||||||
|
pages/application.R!"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
# Single frame application
|
||||||
|
frame <- shiny::fluidPage(
|
||||||
|
title = private$appPage$title(),
|
||||||
|
private$appPage$ui("application")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ui <- shiny::tagList(
|
||||||
|
shiny::tags$head(
|
||||||
|
shiny::singleton(
|
||||||
|
shinyjs::useShinyjs(debug = getOption("debug.shinyjs", FALSE))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
frame,
|
||||||
|
if (rlang::is_installed("AVSDevR.ShinyDisconnectHandler")) {
|
||||||
|
AVSDevR.ShinyDisconnectHandler::useBannerDisconnectHandler()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (private$withBS4) {
|
||||||
|
# Attach boostrap 4 css shim
|
||||||
|
if (getOption("shiny.minified", TRUE)) {
|
||||||
|
css <- "bootstrap-4-shim.min.css"
|
||||||
|
} else {
|
||||||
|
css <- "bootstrap-4-shim.css"
|
||||||
|
}
|
||||||
|
uiDeps <- htmltools::htmlDependency(
|
||||||
|
"AVSDevR.ShinyApplication",
|
||||||
|
as.character(utils::packageVersion("AVSDevR.ShinyApplication")),
|
||||||
|
c(file = system.file(package = "AVSDevR.ShinyApplication")),
|
||||||
|
stylesheet = css
|
||||||
|
)
|
||||||
|
ui <- htmltools::attachDependencies(ui, uiDeps)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set the language
|
||||||
|
shiny:::setLang(ui, private$language)
|
||||||
|
},
|
||||||
|
server = function(input, output, session, ...) {
|
||||||
|
if (!is.null(private$sessionLogger)) {
|
||||||
|
private$sessionLogger$attachSession(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
shiny::callModule(
|
||||||
|
private$appPage$server,
|
||||||
|
"application",
|
||||||
|
db_client = self$getSessionDBClient(session),
|
||||||
|
...
|
||||||
|
)
|
||||||
|
},
|
||||||
|
app = function() {
|
||||||
|
shiny::shinyApp(self$ui(), self$server)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
private = list(
|
||||||
|
appPage = NULL,
|
||||||
|
language = "en",
|
||||||
|
sessionLogger = NULL,
|
||||||
|
withBS4 = FALSE
|
||||||
|
)
|
||||||
|
)
|
||||||
128
R/sessionLogger.R
Normal file
128
R/sessionLogger.R
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
#' @export
|
||||||
|
SessionLogger <- R6::R6Class(
|
||||||
|
"SessionLogger",
|
||||||
|
public = list(
|
||||||
|
wrap = function(app) {
|
||||||
|
wrapped_server <- function(input, output, session, ...) {
|
||||||
|
self$attachSession(session)
|
||||||
|
app$serverFuncSource()(input, output, session, ...)
|
||||||
|
}
|
||||||
|
|
||||||
|
app$serverFuncSource <- function() {
|
||||||
|
wrapped_server
|
||||||
|
}
|
||||||
|
|
||||||
|
app
|
||||||
|
},
|
||||||
|
attachSession = function(session) {
|
||||||
|
private$onSessionStarted(session)
|
||||||
|
session$onSessionEnded(function() {
|
||||||
|
private$onSessionEnded(session)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
),
|
||||||
|
private = list(
|
||||||
|
getClientIp = function(session) {
|
||||||
|
if ("HTTP_X_REAL_IP" %in% names(session$request)) {
|
||||||
|
if (getOption("debug", FALSE)) {
|
||||||
|
cat("Using client IP from HTTP_X_REAL_IP\n")
|
||||||
|
}
|
||||||
|
client_ip <- session$request$HTTP_X_REAL_IP
|
||||||
|
} else if ("HTTP_X_FORWARDED_FOR" %in% names(session$request)) {
|
||||||
|
if (getOption("debug", FALSE)) {
|
||||||
|
cat("Using client IP from HTTP_X_FORWARDED_FOR\n")
|
||||||
|
}
|
||||||
|
client_ip <- strsplit(session$request$HTTP_X_FORWARDED_FOR, ",")[[1]][[1]]
|
||||||
|
} else if ("REMOTE_ADDR" %in% names(session$request)) {
|
||||||
|
if (getOption("debug", FALSE)) {
|
||||||
|
cat("Using client IP from REMOTE_ADDR\n")
|
||||||
|
}
|
||||||
|
client_ip <- session$request$REMOTE_ADDR
|
||||||
|
} else {
|
||||||
|
if (getOption("debug", FALSE)) {
|
||||||
|
cat("Warning: No client IP found\n")
|
||||||
|
}
|
||||||
|
client_ip <- "x.x.x.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onSessionStarted = function(session) {
|
||||||
|
private$logSessionEvent("session_started", session)
|
||||||
|
},
|
||||||
|
|
||||||
|
onSessionEnded = function(session) {
|
||||||
|
private$logSessionEvent("session_ended", session)
|
||||||
|
},
|
||||||
|
|
||||||
|
logSessionEvent = function(
|
||||||
|
event, session = shiny::getDefaultReactiveDomain()
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
#' @export
|
||||||
|
SessionLoggerConsole <- R6::R6Class(
|
||||||
|
"SessionLoggerConsole",
|
||||||
|
inherit = SessionLogger,
|
||||||
|
public = list(),
|
||||||
|
private = list(
|
||||||
|
logSessionEvent = function(
|
||||||
|
event, session = shiny::getDefaultReactiveDomain()
|
||||||
|
) {
|
||||||
|
event_row <- tibble::tibble(
|
||||||
|
app_name = basename(getwd()),
|
||||||
|
app_pid = Sys.getpid(),
|
||||||
|
client_ip = private$getClientIp(session),
|
||||||
|
user_id = session$userData$user_id,
|
||||||
|
session_token = session$token,
|
||||||
|
!!event := lubridate::now()
|
||||||
|
)
|
||||||
|
|
||||||
|
# nolint start: line_length_linter
|
||||||
|
cat("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n")
|
||||||
|
dplyr::glimpse(event_row)
|
||||||
|
cat("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n")
|
||||||
|
# nolint end
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
#' @export
|
||||||
|
SessionLoggerDB <- R6::R6Class(
|
||||||
|
"SessionLoggerDB",
|
||||||
|
inherit = SessionLogger,
|
||||||
|
public = list(
|
||||||
|
initialize = function(db_client) {
|
||||||
|
if (is.null(conn) || !R6::is.R6(conn) || !inherits(conn, "DBClient")) {
|
||||||
|
stop("DBClient instance required!")
|
||||||
|
}
|
||||||
|
private$db_client <- db_client
|
||||||
|
}
|
||||||
|
),
|
||||||
|
private = list(
|
||||||
|
db_client = NULL,
|
||||||
|
|
||||||
|
logSessionEvent = function(
|
||||||
|
event, session = shiny::getDefaultReactiveDomain()
|
||||||
|
) {
|
||||||
|
db_client$connect()
|
||||||
|
on.exit({
|
||||||
|
db_client$disconnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
event_row <- tibble::tibble(
|
||||||
|
app_name = basename(getwd()),
|
||||||
|
app_pid = Sys.getpid(),
|
||||||
|
client_ip = getClientIp(session),
|
||||||
|
user_id = session$userData$user_id,
|
||||||
|
session_token = session$token,
|
||||||
|
!!event := lubridate::now()
|
||||||
|
)
|
||||||
|
|
||||||
|
private$db_client$upsertRows(
|
||||||
|
"shiny_sessions", event_row, key_name = "session_token"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
34
R/ufApplication.R
Normal file
34
R/ufApplication.R
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#' @export
|
||||||
|
UFApplication <- R6::R6Class(
|
||||||
|
"UFApplication",
|
||||||
|
inherit = AdminLTEApplication,
|
||||||
|
public = list(
|
||||||
|
configureUF = function(
|
||||||
|
uri, with_users = TRUE, user_optional = FALSE, app_name = NULL
|
||||||
|
) {
|
||||||
|
private$ufURI <- uri
|
||||||
|
if (is.logical(with_users)) {
|
||||||
|
private$with_users <- with_users
|
||||||
|
}
|
||||||
|
if (is.logical(user_optional)) {
|
||||||
|
private$user_optional <- user_optional
|
||||||
|
}
|
||||||
|
if (!is.null(app_name)) {
|
||||||
|
private$app_name <- app_name
|
||||||
|
}
|
||||||
|
invisible(self)
|
||||||
|
},
|
||||||
|
app = function() {
|
||||||
|
AVSDevR.UserFrosting::ufWrapApplication(
|
||||||
|
super$app(), private$ufURI, private$with_users,
|
||||||
|
private$user_optional, private$app_name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
private = list(
|
||||||
|
ufURI = NULL,
|
||||||
|
with_users = TRUE,
|
||||||
|
user_optional = FALSE,
|
||||||
|
app_name = NULL
|
||||||
|
)
|
||||||
|
)
|
||||||
95
R/ufDashboardApplication.R
Normal file
95
R/ufDashboardApplication.R
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
#' @export
|
||||||
|
UFDashboardApplication <- R6::R6Class(
|
||||||
|
"UFDashboardApplication",
|
||||||
|
inherit = UFApplication,
|
||||||
|
public = list(
|
||||||
|
withSidebarCollapsed = function() {
|
||||||
|
private$sidebar_collapsed <- TRUE
|
||||||
|
invisible(self)
|
||||||
|
},
|
||||||
|
ui = function() {
|
||||||
|
if (file.exists("widgets/help.R")) {
|
||||||
|
private$mHelp <- modules::use("widgets/help.R")
|
||||||
|
}
|
||||||
|
if (file.exists("widgets/title.R")) {
|
||||||
|
private$mDashboardTitle <- modules::use("widgets/title.R")
|
||||||
|
}
|
||||||
|
if (file.exists("widgets/menu.R")) {
|
||||||
|
private$mDashboardMenu <- modules::use("widgets/menu.R")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create a UI
|
||||||
|
ui <- shinydashboard::dashboardPage(
|
||||||
|
header = shinydashboard::dashboardHeader(
|
||||||
|
title = if (!is.null(private$mDashboardTitle)) {
|
||||||
|
private$mDashboardTitle$ui
|
||||||
|
} else {
|
||||||
|
private$appPage$title()
|
||||||
|
},
|
||||||
|
.list = c(
|
||||||
|
# Help links
|
||||||
|
if (!is.null(private$mHelp)) list(private$mHelp$ui("help")),
|
||||||
|
|
||||||
|
# UF stuff
|
||||||
|
if (length(private$with_users) == 0 || private$with_users) {
|
||||||
|
list(AVSDevR.UserFrosting::ufUiTopbar())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
sidebar = shinydashboard::dashboardSidebar(
|
||||||
|
collapsed = !(
|
||||||
|
length(private$sidebar_collapsed) == 0
|
||||||
|
|| private$sidebar_collapsed == FALSE
|
||||||
|
),
|
||||||
|
shiny::div(
|
||||||
|
id = "mainSidebar",
|
||||||
|
|
||||||
|
# UF stuff
|
||||||
|
if (length(private$with_users) == 0 || private$with_users) {
|
||||||
|
AVSDevR.UserFrosting::ufUiSidebar()
|
||||||
|
},
|
||||||
|
|
||||||
|
# Normal sidebar
|
||||||
|
shinydashboard::sidebarMenu(
|
||||||
|
id = "mainMenu",
|
||||||
|
shiny::tags$li(class = "header", "NAVIGATION"),
|
||||||
|
|
||||||
|
if (!is.null(private$mDashboardMenu)) private$mDashboardMenu$ui
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
body = shinydashboard::dashboardBody(self$frameUI()),
|
||||||
|
title = private$appPage$title(),
|
||||||
|
skin = private$skin
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set the language
|
||||||
|
shiny:::setLang(ui, private$language)
|
||||||
|
},
|
||||||
|
server = function(
|
||||||
|
input, output, session, ufApi = NULL, ufUser = NULL, ...
|
||||||
|
) {
|
||||||
|
# Load help menu server (if supplied)
|
||||||
|
if (!is.null(private$mHelp)) {
|
||||||
|
shiny::callModule(private$mHelp$server, "help")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load UF server (if user support required)
|
||||||
|
if (length(private$with_users) == 0 || private$with_users) {
|
||||||
|
shiny::callModule(AVSDevR.UserFrosting::ufServer, "UF", ufApi)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Launch the frame
|
||||||
|
super$server(
|
||||||
|
input, output, session, ufApi = ufApi, ufUser = ufUser, ...
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
private = list(
|
||||||
|
sidebar_collapsed = FALSE,
|
||||||
|
|
||||||
|
mHelp = NULL,
|
||||||
|
mDashboardTitle = NULL,
|
||||||
|
mDashboardMenu = NULL
|
||||||
|
)
|
||||||
|
)
|
||||||
7
R/ufFrameApplication.R
Normal file
7
R/ufFrameApplication.R
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#' @export
|
||||||
|
UFFrameApplication <- R6::R6Class(
|
||||||
|
"UFFrameApplication",
|
||||||
|
inherit = UFApplication,
|
||||||
|
public = list(),
|
||||||
|
private = list()
|
||||||
|
)
|
||||||
2951
inst/css/bootstrap-4-shim.css
vendored
Normal file
2951
inst/css/bootstrap-4-shim.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
inst/css/bootstrap-4-shim.min.css
vendored
Normal file
1
inst/css/bootstrap-4-shim.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user