This is the 2nd post of a series of 3 posts that discuss how to use Open Source Shiny Server a bit more wisely. The topic of this post is multiple page rendering and the motivation of each of the topic is listed below. If you’ve missed the previous one, see here.
- Load balancing (and auto scaling)
- Each application (or folder in
/srv/shiny-server) is binded by a single process so that multiple users or sessions are served by the same process. Let say multiple cores exist in the server machine. Then this can be one of the main causes of performance bottleneck as only a single process is reserved for an application.
- Each application (or folder in
- Rendering multiple pages, including authentication
- An application is served as a single-page web application and thus it is not built to render multiple pages. Application code could be easier to manage if code is split by different pages. Moreover it is highly desirable to implement authentication.
- Running with a Proxy and SSL configuration for HTTPS
- By default, an application is served by HTTP with port 3838. A useful use case to serve a Shiny application via HTTPS is it can be integrated with a Tableau dashboard.
As indicated above, Shiny is not designed to render multiple pages and, in general, the UI is rendered on the fly as defined in ui.R or app.R. However this is not the only way as the UI can be rendered as a html output using
htmlOutput() in ui.R and
renderUI() in server.R. In this post, rendering multiple pages will be illustrated using an example application.
Example application structure
A total of 6 pages exist in the application as shown below.
At the beginning, the login page is rendered. A user can enter credentials for authentication or move to the register page. User credentials are kept in a SQLite db and the following user information is initialized at each start-up - passwords are encrypted using the bcrypt package.
Note that the authentication plan of this application is for demonstration only. In practice, for instance, LDAP or Active Directory authentication may be considered if it is possible to contact to a directory server - for Active Directory authentication, the radhelper package might be useful.
It is assumed that an application key (application-key) should be specified for registration together with user name and password. The register page is shown below.
Once logged on, two extra buttons appear: Profile and App. The screen shots of before and after login are shown below.
The main purpose of the profile page is to change the password.
The application page keeps the main contents of the application. The default Shiny application is used.
Each UI elements are constructed in a function and it is set to be rendered using
htmlOutput() in ui.R. Actual rendering is made by
renderUI() in server.R.
Each UI function has unspecified argument (
...) so that some values can be passed from server.R. For example, the logout and application pages include message and username from the server.
At the end, UI is set to be rendered as a html output.
A page is rendered using
render_page() in server.R. This function accepts a UI element function in ui.R and renders a fluid page with some extra values. I didn’t have much luck with Shiny Dashboard that the flud page layout is chosen instead.
The authentication process shows a tricky part of implementing this setup. Depending on which page is currently rendered, only a part of inputs exist in the current page. In this circumstance, if an input is captured in reactive context such as
reactive() but it doesn’t exist in the current page, an error will be thrown. Therefore whether an input exists or not should be checked as seen in the observer below. On the other hand,
observeEvent() is free from this error as it works only if the input exists.
A try-catch block can also be useful to prevent this type of error due to a missing element. Below the plot of the application page is handled in
tryCatch so that the application doesn’t stop abruptly with an error although the plot element doesn’t exist in the current page.
I hope this post is useful.