mirror of
https://github.com/TwiN/gatus.git
synced 2024-11-21 23:43:27 +01:00
#77: Make page title customizable
This commit is contained in:
parent
effad21c64
commit
7a68920889
@ -41,6 +41,10 @@ var (
|
|||||||
|
|
||||||
// ErrInvalidSecurityConfig is an error returned when the security configuration is invalid
|
// ErrInvalidSecurityConfig is an error returned when the security configuration is invalid
|
||||||
ErrInvalidSecurityConfig = errors.New("invalid security configuration")
|
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
|
// Config is the main configuration structure
|
||||||
@ -75,6 +79,9 @@ type Config struct {
|
|||||||
// Web is the configuration for the web listener
|
// Web is the configuration for the web listener
|
||||||
Web *WebConfig `yaml:"web"`
|
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
|
filePath string // path to the file from which config was loaded from
|
||||||
lastFileModTime time.Time // last modification time
|
lastFileModTime time.Time // last modification time
|
||||||
}
|
}
|
||||||
@ -162,6 +169,9 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
|
|||||||
if err := validateWebConfig(config); err != nil {
|
if err := validateWebConfig(config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := validateUIConfig(config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if err := validateStorageConfig(config); err != nil {
|
if err := validateStorageConfig(config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -191,9 +201,20 @@ func validateStorageConfig(config *Config) error {
|
|||||||
return nil
|
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 {
|
func validateWebConfig(config *Config) error {
|
||||||
if config.Web == nil {
|
if config.Web == nil {
|
||||||
config.Web = &WebConfig{Address: DefaultAddress, Port: DefaultPort}
|
config.Web = GetDefaultWebConfig()
|
||||||
} else {
|
} else {
|
||||||
return config.Web.validateAndSetDefaults()
|
return config.Web.validateAndSetDefaults()
|
||||||
}
|
}
|
||||||
|
@ -36,10 +36,15 @@ func TestLoadDefaultConfigurationFile(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseAndValidateConfigBytes(t *testing.T) {
|
func TestParseAndValidateConfigBytes(t *testing.T) {
|
||||||
file := t.TempDir() + "/test.db"
|
file := t.TempDir() + "/test.db"
|
||||||
|
StaticFolder = "../web/static"
|
||||||
|
defer func() {
|
||||||
|
StaticFolder = "./web/static"
|
||||||
|
}()
|
||||||
config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(`
|
config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(`
|
||||||
storage:
|
storage:
|
||||||
file: %s
|
file: %s
|
||||||
|
ui:
|
||||||
|
title: Test
|
||||||
services:
|
services:
|
||||||
- name: twinnation
|
- name: twinnation
|
||||||
url: https://twinnation.org/health
|
url: https://twinnation.org/health
|
||||||
@ -71,6 +76,9 @@ services:
|
|||||||
if config == nil {
|
if config == nil {
|
||||||
t.Fatal("Config shouldn't have been 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 {
|
if len(config.Services) != 3 {
|
||||||
t.Error("Should have returned two services")
|
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"`
|
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
|
// validateAndSetDefaults checks and sets the default values for fields that are not set
|
||||||
func (web *WebConfig) validateAndSetDefaults() error {
|
func (web *WebConfig) validateAndSetDefaults() error {
|
||||||
// Validate the Address
|
// Validate the Address
|
||||||
|
@ -30,18 +30,14 @@ const (
|
|||||||
var (
|
var (
|
||||||
cache = gocache.NewCache().WithMaxSize(100).WithEvictionPolicy(gocache.FirstInFirstOut)
|
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.
|
// server is the http.Server created by Handle.
|
||||||
// The only reason it exists is for testing purposes.
|
// The only reason it exists is for testing purposes.
|
||||||
server *http.Server
|
server *http.Server
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handle creates the router and starts the server
|
// Handle creates the router and starts the server
|
||||||
func Handle(securityConfig *security.Config, webConfig *config.WebConfig, enableMetrics bool) {
|
func Handle(securityConfig *security.Config, webConfig *config.WebConfig, uiConfig *config.UIConfig, enableMetrics bool) {
|
||||||
var router http.Handler = CreateRouter(securityConfig, enableMetrics)
|
var router http.Handler = CreateRouter(config.StaticFolder, securityConfig, uiConfig, enableMetrics)
|
||||||
if os.Getenv("ENVIRONMENT") == "dev" {
|
if os.Getenv("ENVIRONMENT") == "dev" {
|
||||||
router = developmentCorsHandler(router)
|
router = developmentCorsHandler(router)
|
||||||
}
|
}
|
||||||
@ -68,14 +64,14 @@ func Shutdown() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateRouter creates the router for the http server
|
// 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()
|
router := mux.NewRouter()
|
||||||
if enabledMetrics {
|
if enabledMetrics {
|
||||||
router.Handle("/metrics", promhttp.Handler()).Methods("GET")
|
router.Handle("/metrics", promhttp.Handler()).Methods("GET")
|
||||||
}
|
}
|
||||||
router.Handle("/health", health.Handler().WithJSON(true)).Methods("GET")
|
router.Handle("/health", health.Handler().WithJSON(true)).Methods("GET")
|
||||||
router.HandleFunc("/favicon.ico", favIconHandler).Methods("GET")
|
router.HandleFunc("/favicon.ico", favIconHandler(staticFolder)).Methods("GET")
|
||||||
// New endpoints
|
// 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/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")
|
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")
|
// 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}/badge.svg", responseTimeBadgeHandler).Methods("GET")
|
||||||
router.HandleFunc("/api/v1/services/{key}/response-times/{duration}/chart.svg", responseTimeChartHandler).Methods("GET")
|
router.HandleFunc("/api/v1/services/{key}/response-times/{duration}/chart.svg", responseTimeChartHandler).Methods("GET")
|
||||||
// SPA
|
// 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
|
// Everything else falls back on static content
|
||||||
router.PathPrefix("/").Handler(GzipHandler(http.FileServer(http.Dir(staticFolder))))
|
router.PathPrefix("/").Handler(GzipHandler(http.FileServer(http.Dir(staticFolder))))
|
||||||
return router
|
return router
|
||||||
|
@ -88,7 +88,6 @@ var (
|
|||||||
func TestCreateRouter(t *testing.T) {
|
func TestCreateRouter(t *testing.T) {
|
||||||
defer storage.Get().Clear()
|
defer storage.Get().Clear()
|
||||||
defer cache.Clear()
|
defer cache.Clear()
|
||||||
staticFolder = "../web/static"
|
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Metrics: true,
|
Metrics: true,
|
||||||
Services: []*core.Service{
|
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[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()})
|
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 {
|
type Scenario struct {
|
||||||
Name string
|
Name string
|
||||||
Path string
|
Path string
|
||||||
@ -287,7 +286,7 @@ func TestHandle(t *testing.T) {
|
|||||||
_ = os.Setenv("ROUTER_TEST", "true")
|
_ = os.Setenv("ROUTER_TEST", "true")
|
||||||
_ = os.Setenv("ENVIRONMENT", "dev")
|
_ = os.Setenv("ENVIRONMENT", "dev")
|
||||||
defer os.Clearenv()
|
defer os.Clearenv()
|
||||||
Handle(cfg.Security, cfg.Web, cfg.Metrics)
|
Handle(cfg.Security, cfg.Web, cfg.UI, cfg.Metrics)
|
||||||
defer Shutdown()
|
defer Shutdown()
|
||||||
request, _ := http.NewRequest("GET", "/health", nil)
|
request, _ := http.NewRequest("GET", "/health", nil)
|
||||||
responseRecorder := httptest.NewRecorder()
|
responseRecorder := httptest.NewRecorder()
|
||||||
@ -312,7 +311,6 @@ func TestShutdown(t *testing.T) {
|
|||||||
func TestServiceStatusesHandler(t *testing.T) {
|
func TestServiceStatusesHandler(t *testing.T) {
|
||||||
defer storage.Get().Clear()
|
defer storage.Get().Clear()
|
||||||
defer cache.Clear()
|
defer cache.Clear()
|
||||||
staticFolder = "../web/static"
|
|
||||||
firstResult := &testSuccessfulResult
|
firstResult := &testSuccessfulResult
|
||||||
secondResult := &testUnsuccessfulResult
|
secondResult := &testUnsuccessfulResult
|
||||||
storage.Get().Insert(&testService, firstResult)
|
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
|
// Can't be bothered dealing with timezone issues on the worker that runs the automated tests
|
||||||
firstResult.Timestamp = time.Time{}
|
firstResult.Timestamp = time.Time{}
|
||||||
secondResult.Timestamp = time.Time{}
|
secondResult.Timestamp = time.Time{}
|
||||||
router := CreateRouter(nil, false)
|
router := CreateRouter("../web/static", nil, nil, false)
|
||||||
|
|
||||||
type Scenario struct {
|
type Scenario struct {
|
||||||
Name string
|
Name string
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
// favIconHandler handles requests for /favicon.ico
|
// favIconHandler handles requests for /favicon.ico
|
||||||
func favIconHandler(writer http.ResponseWriter, request *http.Request) {
|
func favIconHandler(staticFolder string) http.HandlerFunc {
|
||||||
http.ServeFile(writer, request, staticFolder+"/favicon.ico")
|
return func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
http.ServeFile(writer, request, staticFolder+"/favicon.ico")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,27 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
// spaHandler handles requests for /
|
"github.com/TwinProduction/gatus/config"
|
||||||
func spaHandler(writer http.ResponseWriter, request *http.Request) {
|
)
|
||||||
http.ServeFile(writer, request, staticFolder+"/index.html")
|
|
||||||
|
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) {
|
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)
|
watchdog.Monitor(cfg)
|
||||||
go listenToConfigurationFileChanges(cfg)
|
go listenToConfigurationFileChanges(cfg)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Health Dashboard | Gatus</title>
|
<title>{{ .Title }}</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<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 New Issue
Block a user