Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions api/spa.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ package api

import (
_ "embed"
"html/template"

"github.com/TwiN/gatus/v5/config/ui"
static "github.com/TwiN/gatus/v5/web"
"github.com/TwiN/logr"
"github.com/gofiber/fiber/v2"
)
Expand All @@ -23,7 +21,7 @@ func SinglePageApplication(uiConfig *ui.Config) fiber.Handler {
vd.Theme = "dark"
}
}
t, err := template.ParseFS(static.FileSystem, static.IndexPath)
t, err := ui.GetTemplate()
if err != nil {
// This should never happen, because ui.ValidateAndSetDefaults validates that the template works.
logr.Errorf("[api.SinglePageApplication] Failed to parse template. This should never happen, because the template is validated on start. Error: %s", err.Error())
Expand Down
74 changes: 48 additions & 26 deletions config/ui/ui.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ui

import (
"bytes"
"encoding/json"
"errors"
"html/template"

Expand All @@ -10,16 +11,16 @@ import (
)

const (
defaultTitle = "Health Dashboard | Gatus"
defaultDescription = "Gatus is an advanced automated status page that lets you monitor your applications and configure alerts to notify you if there's an issue"
defaultHeader = "Gatus"
defaultDashboardHeading = "Health Dashboard"
defaultDashboardSubheading = "Monitor the health of your endpoints in real-time"
defaultLogo = ""
defaultLink = ""
defaultCustomCSS = ""
defaultSortBy = "name"
defaultFilterBy = "none"
defaultTitle = "Health Dashboard | Gatus"
defaultDescription = "Gatus is an advanced automated status page that lets you monitor your applications and configure alerts to notify you if there's an issue"
defaultHeader = "Gatus"
defaultDashboardHeading = "Health Dashboard"
defaultDashboardSubheading = "Monitor the health of your endpoints in real-time"
defaultLogo = ""
defaultLink = ""
defaultCustomCSS = ""
defaultSortBy = "name"
defaultFilterBy = "none"
)

var (
Expand All @@ -28,26 +29,28 @@ var (
ErrButtonValidationFailed = errors.New("invalid button configuration: missing required name or link")
ErrInvalidDefaultSortBy = errors.New("invalid default-sort-by value: must be 'name', 'group', or 'health'")
ErrInvalidDefaultFilterBy = errors.New("invalid default-filter-by value: must be 'none', 'failing', or 'unstable'")

uiTemplate *template.Template = nil
)

// Config is the configuration for the UI of Gatus
type Config struct {
Title string `yaml:"title,omitempty"` // Title of the page
Description string `yaml:"description,omitempty"` // Meta description of the page
DashboardHeading string `yaml:"dashboard-heading,omitempty"` // Dashboard Title between header and endpoints
DashboardSubheading string `yaml:"dashboard-subheading,omitempty"` // Dashboard Description between header and endpoints
Header string `yaml:"header,omitempty"` // Header is the text at the top of the page
Logo string `yaml:"logo,omitempty"` // Logo to display on the page
Link string `yaml:"link,omitempty"` // Link to open when clicking on the logo
Buttons []Button `yaml:"buttons,omitempty"` // Buttons to display below the header
CustomCSS string `yaml:"custom-css,omitempty"` // Custom CSS to include in the page
DarkMode *bool `yaml:"dark-mode,omitempty"` // DarkMode is a flag to enable dark mode by default
DefaultSortBy string `yaml:"default-sort-by,omitempty"` // DefaultSortBy is the default sort option ('name', 'group', 'health')
DefaultFilterBy string `yaml:"default-filter-by,omitempty"` // DefaultFilterBy is the default filter option ('none', 'failing', 'unstable')
Title string `yaml:"title,omitempty" json:"-"` // Title of the page
Description string `yaml:"description,omitempty" json:"-"` // Meta description of the page
DashboardHeading string `yaml:"dashboard-heading,omitempty" json:"dashboardHeading,omitempty"` // Dashboard Title between header and endpoints
DashboardSubheading string `yaml:"dashboard-subheading,omitempty" json:"dashboardSubheading,omitempty"` // Dashboard Description between header and endpoints
Header string `yaml:"header,omitempty" json:"header,omitempty"` // Header is the text at the top of the page
Logo string `yaml:"logo,omitempty" json:"logo,omitempty"` // Logo to display on the page
Link string `yaml:"link,omitempty" json:"link,omitempty"` // Link to open when clicking on the logo
Buttons []Button `yaml:"buttons,omitempty" json:"buttons,omitempty"` // Buttons to display below the header
CustomCSS string `yaml:"custom-css,omitempty" json:"-"` // Custom CSS to include in the page
DarkMode *bool `yaml:"dark-mode,omitempty" json:"-"` // DarkMode is a flag to enable dark mode by default
DefaultSortBy string `yaml:"default-sort-by,omitempty" json:"defaultSortBy,omitempty"` // DefaultSortBy is the default sort option ('name', 'group', 'health')
DefaultFilterBy string `yaml:"default-filter-by,omitempty" json:"defaultFilterBy,omitempty"` // DefaultFilterBy is the default filter option ('none', 'failing', 'unstable')
//////////////////////////////////////////////
// Non-configurable - used for UI rendering //
//////////////////////////////////////////////
MaximumNumberOfResults int `yaml:"-"` // MaximumNumberOfResults to display on the page, it's not configurable because we're passing it from the storage config
MaximumNumberOfResults int `yaml:"-" json:"maximumNumberOfResults,omitempty"` // MaximumNumberOfResults to display on the page, it's not configurable because we're passing it from the storage config
}

func (cfg *Config) IsDarkMode() bool {
Expand All @@ -59,8 +62,8 @@ func (cfg *Config) IsDarkMode() bool {

// Button is the configuration for a button on the UI
type Button struct {
Name string `yaml:"name,omitempty"` // Name is the text to display on the button
Link string `yaml:"link,omitempty"` // Link to open when the button is clicked.
Name string `yaml:"name,omitempty" json:"name,omitempty"` // Name is the text to display on the button
Link string `yaml:"link,omitempty" json:"link,omitempty"` // Link to open when the button is clicked.
}

// Validate validates the button configuration
Expand Down Expand Up @@ -134,7 +137,7 @@ func (cfg *Config) ValidateAndSetDefaults() error {
}
}
// Validate that the template works
t, err := template.ParseFS(static.FileSystem, static.IndexPath)
t, err := GetTemplate()
if err != nil {
return err
}
Expand All @@ -146,3 +149,22 @@ type ViewData struct {
UI *Config
Theme string
}

func toJSON(v any) template.JS {
b, err := json.Marshal(v)
if err != nil {
return template.JS("null")
}
return template.JS(b)
}

func GetTemplate() (*template.Template, error) {
if uiTemplate != nil {
return uiTemplate, nil
}
var err error
uiTemplate, err = template.New("index.html").Funcs(template.FuncMap{
"toJSON": toJSON,
}).ParseFS(static.FileSystem, static.IndexPath)
return uiTemplate, err
}
11 changes: 8 additions & 3 deletions web/app/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@
<html lang="en" class="{{ .Theme }}">
<head>
<meta charset="utf-8" />
<title>{{ .UI.Title }}</title>
<script type="text/javascript">
window.config = {logo: "{{ .UI.Logo }}", header: "{{ .UI.Header }}", dashboardHeading: "{{ .UI.DashboardHeading }}", dashboardSubheading: "{{ .UI.DashboardSubheading }}", link: "{{ .UI.Link }}", buttons: [], maximumNumberOfResults: "{{ .UI.MaximumNumberOfResults }}", defaultSortBy: "{{ .UI.DefaultSortBy }}", defaultFilterBy: "{{ .UI.DefaultFilterBy }}"};{{- range .UI.Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}
// Initialize theme immediately to prevent flash
if (document.title !== decodeURIComponent("%7B%7B%20.UI.Title%20%7D%7D")) {
Function(`window.config = {{ toJSON .UI }};`)(); // Wrapped in Function to prevent error in development mode
}
else {
document.title = "Gatus (Development)";
}
(function() {
// Initialize theme immediately to prevent flash
const themeFromCookie = document.cookie.match(/theme=(dark|light);?/)?.[1];
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (themeFromCookie === 'dark' || (!themeFromCookie && prefersDark)) {
Expand All @@ -15,7 +21,6 @@
}
})();
</script>
<title>{{ .UI.Title }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
Expand Down
11 changes: 8 additions & 3 deletions web/static/index.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
<!doctype html><html lang="en" class="{{ .Theme }}"><head><meta charset="utf-8"/><script>window.config = {logo: "{{ .UI.Logo }}", header: "{{ .UI.Header }}", dashboardHeading: "{{ .UI.DashboardHeading }}", dashboardSubheading: "{{ .UI.DashboardSubheading }}", link: "{{ .UI.Link }}", buttons: [], maximumNumberOfResults: "{{ .UI.MaximumNumberOfResults }}", defaultSortBy: "{{ .UI.DefaultSortBy }}", defaultFilterBy: "{{ .UI.DefaultFilterBy }}"};{{- range .UI.Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}
// Initialize theme immediately to prevent flash
<!doctype html><html lang="en" class="{{ .Theme }}"><head><meta charset="utf-8"/><title>{{ .UI.Title }}</title><script>if (document.title !== decodeURIComponent("%7B%7B%20.UI.Title%20%7D%7D")) {
Function(`window.config = {{ toJSON .UI }};`)(); // Wrapped in Function to prevent error in development mode
}
else {
document.title = "Gatus (Development)";
}
(function() {
// Initialize theme immediately to prevent flash
const themeFromCookie = document.cookie.match(/theme=(dark|light);?/)?.[1];
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (themeFromCookie === 'dark' || (!themeFromCookie && prefersDark)) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
})();</script><title>{{ .UI.Title }}</title><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="manifest" href="/manifest.json" crossorigin="use-credentials"/><link rel="shortcut icon" href="/favicon.ico"/><link rel="stylesheet" href="/css/custom.css"/><meta name="description" content="{{ .UI.Description }}"/><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/><meta name="apple-mobile-web-app-title" content="{{ .UI.Title }}"/><meta name="application-name" content="{{ .UI.Title }}"/><meta name="theme-color" content="#f7f9fb"/><script defer="defer" src="/js/chunk-vendors.js"></script><script defer="defer" src="/js/app.js"></script><link href="/css/app.css" rel="stylesheet"></head><body><noscript><strong>Enable JavaScript to view this page.</strong></noscript><div id="app"></div></body></html>
})();</script><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="manifest" href="/manifest.json" crossorigin="use-credentials"/><link rel="shortcut icon" href="/favicon.ico"/><link rel="stylesheet" href="/css/custom.css"/><meta name="description" content="{{ .UI.Description }}"/><meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/><meta name="apple-mobile-web-app-title" content="{{ .UI.Title }}"/><meta name="application-name" content="{{ .UI.Title }}"/><meta name="theme-color" content="#f7f9fb"/><script defer="defer" src="/js/chunk-vendors.js"></script><script defer="defer" src="/js/app.js"></script><link href="/css/app.css" rel="stylesheet"></head><body><noscript><strong>Enable JavaScript to view this page.</strong></noscript><div id="app"></div></body></html>