mirror of
https://github.com/TwiN/gatus.git
synced 2024-12-14 11:58:04 +00:00
#77: Make page title customizable
This commit is contained in:
parent
effad21c64
commit
7a68920889
12 changed files with 136 additions and 27 deletions
|
@ -41,6 +41,10 @@ var (
|
|||
|
||||
// ErrInvalidSecurityConfig is an error returned when the security configuration is invalid
|
||||
ErrInvalidSecurityConfig = errors.New("invalid security configuration")
|
||||
|
||||
// StaticFolder is the path to the location of the static folder from the root path of the project
|
||||
// The only reason this is exposed is to allow running tests from a different path than the root path of the project
|
||||
StaticFolder = "./web/static"
|
||||
)
|
||||
|
||||
// Config is the main configuration structure
|
||||
|
@ -75,6 +79,9 @@ type Config struct {
|
|||
// Web is the configuration for the web listener
|
||||
Web *WebConfig `yaml:"web"`
|
||||
|
||||
// UI is the configuration for the UI
|
||||
UI *UIConfig `yaml:"ui"`
|
||||
|
||||
filePath string // path to the file from which config was loaded from
|
||||
lastFileModTime time.Time // last modification time
|
||||
}
|
||||
|
@ -162,6 +169,9 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
|
|||
if err := validateWebConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateUIConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateStorageConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -191,9 +201,20 @@ func validateStorageConfig(config *Config) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func validateUIConfig(config *Config) error {
|
||||
if config.UI == nil {
|
||||
config.UI = GetDefaultUIConfig()
|
||||
} else {
|
||||
if err := config.UI.validateAndSetDefaults(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateWebConfig(config *Config) error {
|
||||
if config.Web == nil {
|
||||
config.Web = &WebConfig{Address: DefaultAddress, Port: DefaultPort}
|
||||
config.Web = GetDefaultWebConfig()
|
||||
} else {
|
||||
return config.Web.validateAndSetDefaults()
|
||||
}
|
||||
|
|
|
@ -36,10 +36,15 @@ func TestLoadDefaultConfigurationFile(t *testing.T) {
|
|||
|
||||
func TestParseAndValidateConfigBytes(t *testing.T) {
|
||||
file := t.TempDir() + "/test.db"
|
||||
StaticFolder = "../web/static"
|
||||
defer func() {
|
||||
StaticFolder = "./web/static"
|
||||
}()
|
||||
config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(`
|
||||
storage:
|
||||
file: %s
|
||||
|
||||
ui:
|
||||
title: Test
|
||||
services:
|
||||
- name: twinnation
|
||||
url: https://twinnation.org/health
|
||||
|
@ -71,6 +76,9 @@ services:
|
|||
if config == nil {
|
||||
t.Fatal("Config shouldn't have been nil")
|
||||
}
|
||||
if config.UI == nil || config.UI.Title != "Test" {
|
||||
t.Error("Expected Config.UI.Title to be Test")
|
||||
}
|
||||
if len(config.Services) != 3 {
|
||||
t.Error("Should have returned two services")
|
||||
}
|
||||
|
|
36
config/ui.go
Normal file
36
config/ui.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
const defaultTitle = "Health Dashboard | Gatus"
|
||||
|
||||
// UIConfig is the configuration for the UI of Gatus
|
||||
type UIConfig struct {
|
||||
Title string `yaml:"title"` // Title of the page
|
||||
}
|
||||
|
||||
// GetDefaultUIConfig returns a UIConfig struct with the default values
|
||||
func GetDefaultUIConfig() *UIConfig {
|
||||
return &UIConfig{
|
||||
Title: defaultTitle,
|
||||
}
|
||||
}
|
||||
|
||||
func (cfg *UIConfig) validateAndSetDefaults() error {
|
||||
if len(cfg.Title) == 0 {
|
||||
cfg.Title = defaultTitle
|
||||
}
|
||||
t, err := template.ParseFiles(StaticFolder + "/index.html")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
err = t.Execute(&buffer, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
21
config/ui_test.go
Normal file
21
config/ui_test.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestUIConfig_validateAndSetDefaults(t *testing.T) {
|
||||
StaticFolder = "../web/static"
|
||||
defer func() {
|
||||
StaticFolder = "./web/static"
|
||||
}()
|
||||
uiConfig := &UIConfig{Title: ""}
|
||||
if err := uiConfig.validateAndSetDefaults(); err != nil {
|
||||
t.Error("expected no error, got", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDefaultUIConfig(t *testing.T) {
|
||||
defaultUIConfig := GetDefaultUIConfig()
|
||||
if defaultUIConfig.Title != defaultTitle {
|
||||
t.Error("expected GetDefaultUIConfig() to return defaultTitle, got", defaultUIConfig.Title)
|
||||
}
|
||||
}
|
|
@ -15,6 +15,11 @@ type WebConfig struct {
|
|||
Port int `yaml:"port"`
|
||||
}
|
||||
|
||||
// GetDefaultWebConfig returns a WebConfig struct with the default values
|
||||
func GetDefaultWebConfig() *WebConfig {
|
||||
return &WebConfig{Address: DefaultAddress, Port: DefaultPort}
|
||||
}
|
||||
|
||||
// validateAndSetDefaults checks and sets the default values for fields that are not set
|
||||
func (web *WebConfig) validateAndSetDefaults() error {
|
||||
// Validate the Address
|
||||
|
|
|
@ -30,18 +30,14 @@ const (
|
|||
var (
|
||||
cache = gocache.NewCache().WithMaxSize(100).WithEvictionPolicy(gocache.FirstInFirstOut)
|
||||
|
||||
// staticFolder is the path to the location of the static folder from the root path of the project
|
||||
// The only reason this is exposed is to allow running tests from a different path than the root path of the project
|
||||
staticFolder = "./web/static"
|
||||
|
||||
// server is the http.Server created by Handle.
|
||||
// The only reason it exists is for testing purposes.
|
||||
server *http.Server
|
||||
)
|
||||
|
||||
// Handle creates the router and starts the server
|
||||
func Handle(securityConfig *security.Config, webConfig *config.WebConfig, enableMetrics bool) {
|
||||
var router http.Handler = CreateRouter(securityConfig, enableMetrics)
|
||||
func Handle(securityConfig *security.Config, webConfig *config.WebConfig, uiConfig *config.UIConfig, enableMetrics bool) {
|
||||
var router http.Handler = CreateRouter(config.StaticFolder, securityConfig, uiConfig, enableMetrics)
|
||||
if os.Getenv("ENVIRONMENT") == "dev" {
|
||||
router = developmentCorsHandler(router)
|
||||
}
|
||||
|
@ -68,14 +64,14 @@ func Shutdown() {
|
|||
}
|
||||
|
||||
// CreateRouter creates the router for the http server
|
||||
func CreateRouter(securityConfig *security.Config, enabledMetrics bool) *mux.Router {
|
||||
func CreateRouter(staticFolder string, securityConfig *security.Config, uiConfig *config.UIConfig, enabledMetrics bool) *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
if enabledMetrics {
|
||||
router.Handle("/metrics", promhttp.Handler()).Methods("GET")
|
||||
}
|
||||
router.Handle("/health", health.Handler().WithJSON(true)).Methods("GET")
|
||||
router.HandleFunc("/favicon.ico", favIconHandler).Methods("GET")
|
||||
// New endpoints
|
||||
router.HandleFunc("/favicon.ico", favIconHandler(staticFolder)).Methods("GET")
|
||||
// Endpoints
|
||||
router.HandleFunc("/api/v1/services/statuses", secureIfNecessary(securityConfig, serviceStatusesHandler)).Methods("GET") // No GzipHandler for this one, because we cache the content as Gzipped already
|
||||
router.HandleFunc("/api/v1/services/{key}/statuses", secureIfNecessary(securityConfig, GzipHandlerFunc(serviceStatusHandler))).Methods("GET")
|
||||
// TODO: router.HandleFunc("/api/v1/services/{key}/uptimes", secureIfNecessary(securityConfig, GzipHandlerFunc(serviceUptimesHandler))).Methods("GET")
|
||||
|
@ -84,7 +80,8 @@ func CreateRouter(securityConfig *security.Config, enabledMetrics bool) *mux.Rou
|
|||
router.HandleFunc("/api/v1/services/{key}/response-times/{duration}/badge.svg", responseTimeBadgeHandler).Methods("GET")
|
||||
router.HandleFunc("/api/v1/services/{key}/response-times/{duration}/chart.svg", responseTimeChartHandler).Methods("GET")
|
||||
// SPA
|
||||
router.HandleFunc("/services/{service}", spaHandler).Methods("GET")
|
||||
router.HandleFunc("/services/{service}", spaHandler(staticFolder, uiConfig)).Methods("GET")
|
||||
router.HandleFunc("/", spaHandler(staticFolder, uiConfig)).Methods("GET")
|
||||
// Everything else falls back on static content
|
||||
router.PathPrefix("/").Handler(GzipHandler(http.FileServer(http.Dir(staticFolder))))
|
||||
return router
|
||||
|
|
|
@ -88,7 +88,6 @@ var (
|
|||
func TestCreateRouter(t *testing.T) {
|
||||
defer storage.Get().Clear()
|
||||
defer cache.Clear()
|
||||
staticFolder = "../web/static"
|
||||
cfg := &config.Config{
|
||||
Metrics: true,
|
||||
Services: []*core.Service{
|
||||
|
@ -104,7 +103,7 @@ func TestCreateRouter(t *testing.T) {
|
|||
}
|
||||
watchdog.UpdateServiceStatuses(cfg.Services[0], &core.Result{Success: true, Duration: time.Millisecond, Timestamp: time.Now()})
|
||||
watchdog.UpdateServiceStatuses(cfg.Services[1], &core.Result{Success: false, Duration: time.Second, Timestamp: time.Now()})
|
||||
router := CreateRouter(cfg.Security, cfg.Metrics)
|
||||
router := CreateRouter("../web/static", cfg.Security, nil, cfg.Metrics)
|
||||
type Scenario struct {
|
||||
Name string
|
||||
Path string
|
||||
|
@ -287,7 +286,7 @@ func TestHandle(t *testing.T) {
|
|||
_ = os.Setenv("ROUTER_TEST", "true")
|
||||
_ = os.Setenv("ENVIRONMENT", "dev")
|
||||
defer os.Clearenv()
|
||||
Handle(cfg.Security, cfg.Web, cfg.Metrics)
|
||||
Handle(cfg.Security, cfg.Web, cfg.UI, cfg.Metrics)
|
||||
defer Shutdown()
|
||||
request, _ := http.NewRequest("GET", "/health", nil)
|
||||
responseRecorder := httptest.NewRecorder()
|
||||
|
@ -312,7 +311,6 @@ func TestShutdown(t *testing.T) {
|
|||
func TestServiceStatusesHandler(t *testing.T) {
|
||||
defer storage.Get().Clear()
|
||||
defer cache.Clear()
|
||||
staticFolder = "../web/static"
|
||||
firstResult := &testSuccessfulResult
|
||||
secondResult := &testUnsuccessfulResult
|
||||
storage.Get().Insert(&testService, firstResult)
|
||||
|
@ -320,7 +318,7 @@ func TestServiceStatusesHandler(t *testing.T) {
|
|||
// Can't be bothered dealing with timezone issues on the worker that runs the automated tests
|
||||
firstResult.Timestamp = time.Time{}
|
||||
secondResult.Timestamp = time.Time{}
|
||||
router := CreateRouter(nil, false)
|
||||
router := CreateRouter("../web/static", nil, nil, false)
|
||||
|
||||
type Scenario struct {
|
||||
Name string
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
package controller
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// favIconHandler handles requests for /favicon.ico
|
||||
func favIconHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
http.ServeFile(writer, request, staticFolder+"/favicon.ico")
|
||||
func favIconHandler(staticFolder string) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
http.ServeFile(writer, request, staticFolder+"/favicon.ico")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,27 @@
|
|||
package controller
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
// spaHandler handles requests for /
|
||||
func spaHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
http.ServeFile(writer, request, staticFolder+"/index.html")
|
||||
"github.com/TwinProduction/gatus/config"
|
||||
)
|
||||
|
||||
func spaHandler(staticFolder string, ui *config.UIConfig) http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
t, err := template.ParseFiles(staticFolder + "/index.html")
|
||||
if err != nil {
|
||||
log.Println("[controller][spaHandler] Failed to parse template:", err.Error())
|
||||
http.ServeFile(writer, request, staticFolder+"/index.html")
|
||||
return
|
||||
}
|
||||
writer.Header().Set("Content-Type", "text/html")
|
||||
err = t.Execute(writer, ui)
|
||||
if err != nil {
|
||||
log.Println("[controller][spaHandler] Failed to parse template:", err.Error())
|
||||
http.ServeFile(writer, request, staticFolder+"/index.html")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
2
main.go
2
main.go
|
@ -35,7 +35,7 @@ func main() {
|
|||
}
|
||||
|
||||
func start(cfg *config.Config) {
|
||||
go controller.Handle(cfg.Security, cfg.Web, cfg.Metrics)
|
||||
go controller.Handle(cfg.Security, cfg.Web, cfg.UI, cfg.Metrics)
|
||||
watchdog.Monitor(cfg)
|
||||
go listenToConfigurationFileChanges(cfg)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Health Dashboard | Gatus</title>
|
||||
<title>{{ .Title }}</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
|
|
|
@ -1 +1 @@
|
|||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>Health Dashboard | Gatus</title><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><script defer="defer" src="/js/chunk-vendors.js" type="module"></script><script defer="defer" src="/js/app.js" type="module"></script><link href="/css/app.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors-legacy.js" nomodule></script><script defer="defer" src="/js/app-legacy.js" nomodule></script></head><body class="dark:bg-gray-900"><noscript><strong>Enable JavaScript to view this page.</strong></noscript><div id="app"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><title>{{ .Title }}</title><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><script defer="defer" src="/js/chunk-vendors.js" type="module"></script><script defer="defer" src="/js/app.js" type="module"></script><link href="/css/app.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors-legacy.js" nomodule></script><script defer="defer" src="/js/app-legacy.js" nomodule></script></head><body class="dark:bg-gray-900"><noscript><strong>Enable JavaScript to view this page.</strong></noscript><div id="app"></div></body></html>
|
Loading…
Reference in a new issue