feat(ui): Add support for buttons below header (#106)

This commit is contained in:
TwiN 2022-04-25 20:20:32 -04:00
parent dcb997f501
commit 9ede992e4e
11 changed files with 97 additions and 15 deletions

View File

@ -186,6 +186,9 @@ If you want to test it locally, see [Docker](#docker).
| `ui.header` | Header at the top of the dashboard. | `Health Status` | | `ui.header` | Header at the top of the dashboard. | `Health Status` |
| `ui.logo` | URL to the logo to display. | `""` | | `ui.logo` | URL to the logo to display. | `""` |
| `ui.link` | Link to open when the logo is clicked. | `""` | | `ui.link` | Link to open when the logo is clicked. | `""` |
| `ui.buttons` | List of buttons to display below the header. | `[]` |
| `ui.buttons[].name` | Text to display on the button. | Required `""` |
| `ui.buttons[].link` | Link to open when the button is clicked. | Required `""` |
| `maintenance` | [Maintenance configuration](#maintenance). | `{}` | | `maintenance` | [Maintenance configuration](#maintenance). | `{}` |
@ -1328,7 +1331,7 @@ web:
``` ```
### Badges ### Badges
### Uptime #### Uptime
![Uptime 1h](https://status.twin.sh/api/v1/endpoints/core_blog-external/uptimes/1h/badge.svg) ![Uptime 1h](https://status.twin.sh/api/v1/endpoints/core_blog-external/uptimes/1h/badge.svg)
![Uptime 24h](https://status.twin.sh/api/v1/endpoints/core_blog-external/uptimes/24h/badge.svg) ![Uptime 24h](https://status.twin.sh/api/v1/endpoints/core_blog-external/uptimes/24h/badge.svg)
![Uptime 7d](https://status.twin.sh/api/v1/endpoints/core_blog-external/uptimes/7d/badge.svg) ![Uptime 7d](https://status.twin.sh/api/v1/endpoints/core_blog-external/uptimes/7d/badge.svg)
@ -1361,7 +1364,7 @@ Example:
If you'd like to see a visual example of each badge available, you can simply navigate to the endpoint's detail page. If you'd like to see a visual example of each badge available, you can simply navigate to the endpoint's detail page.
### Response time #### Response time
![Response time 1h](https://status.twin.sh/api/v1/endpoints/core_blog-external/response-times/1h/badge.svg) ![Response time 1h](https://status.twin.sh/api/v1/endpoints/core_blog-external/response-times/1h/badge.svg)
![Response time 24h](https://status.twin.sh/api/v1/endpoints/core_blog-external/response-times/24h/badge.svg) ![Response time 24h](https://status.twin.sh/api/v1/endpoints/core_blog-external/response-times/24h/badge.svg)
![Response time 7d](https://status.twin.sh/api/v1/endpoints/core_blog-external/response-times/7d/badge.svg) ![Response time 7d](https://status.twin.sh/api/v1/endpoints/core_blog-external/response-times/7d/badge.svg)

View File

@ -53,7 +53,14 @@ maintenance:
duration: 4h duration: 4h
every: [Monday, Thursday] every: [Monday, Thursday]
ui: ui:
title: Test title: T
header: H
link: https://example.org
buttons:
- name: "Home"
link: "https://example.org"
- name: "Status page"
link: "https://status.example.org"
endpoints: endpoints:
- name: website - name: website
url: https://twin.sh/health url: https://twin.sh/health
@ -88,8 +95,8 @@ endpoints:
if config.Storage == nil || config.Storage.Path != file || config.Storage.Type != storage.TypeSQLite { if config.Storage == nil || config.Storage.Path != file || config.Storage.Type != storage.TypeSQLite {
t.Error("expected storage to be set to sqlite, got", config.Storage) t.Error("expected storage to be set to sqlite, got", config.Storage)
} }
if config.UI == nil || config.UI.Title != "Test" { if config.UI == nil || config.UI.Title != "T" || config.UI.Header != "H" || config.UI.Link != "https://example.org" || len(config.UI.Buttons) != 2 || config.UI.Buttons[0].Name != "Home" || config.UI.Buttons[0].Link != "https://example.org" || config.UI.Buttons[1].Name != "Status page" || config.UI.Buttons[1].Link != "https://status.example.org" {
t.Error("Expected Config.UI.Title to be Test") t.Error("expected ui to be set to T, H, https://example.org, 2 buttons, Home and Status page, got", config.UI)
} }
if mc := config.Maintenance; mc == nil || mc.Start != "00:00" || !mc.IsEnabled() || mc.Duration != 4*time.Hour || len(mc.Every) != 2 { if mc := config.Maintenance; mc == nil || mc.Start != "00:00" || !mc.IsEnabled() || mc.Duration != 4*time.Hour || len(mc.Every) != 2 {
t.Error("Expected Config.Maintenance to be configured properly") t.Error("Expected Config.Maintenance to be configured properly")

View File

@ -2,6 +2,7 @@ package ui
import ( import (
"bytes" "bytes"
"errors"
"html/template" "html/template"
) )
@ -16,6 +17,8 @@ var (
// StaticFolder is the path to the location of the static folder from the root path of the project // 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 // 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" StaticFolder = "./web/static"
ErrButtonValidationFailed = errors.New("invalid button configuration: missing required name or link")
) )
// Config is the configuration for the UI of Gatus // Config is the configuration for the UI of Gatus
@ -24,6 +27,21 @@ type Config struct {
Header string `yaml:"header,omitempty"` // Header is the text at the top of the page 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 Logo string `yaml:"logo,omitempty"` // Logo to display on the page
Link string `yaml:"link,omitempty"` // Link to open when clicking on the logo Link string `yaml:"link,omitempty"` // Link to open when clicking on the logo
Buttons []Button `yaml:"buttons,omitempty"` // Buttons to display below the header
}
// 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.
}
// Validate validates the button configuration
func (btn *Button) Validate() error {
if len(btn.Name) == 0 || len(btn.Link) == 0 {
return ErrButtonValidationFailed
}
return nil
} }
// GetDefaultConfig returns a Config struct with the default values // GetDefaultConfig returns a Config struct with the default values
@ -47,6 +65,12 @@ func (cfg *Config) ValidateAndSetDefaults() error {
if len(cfg.Header) == 0 { if len(cfg.Header) == 0 {
cfg.Header = defaultLink cfg.Header = defaultLink
} }
for _, btn := range cfg.Buttons {
if err := btn.Validate(); err != nil {
return err
}
}
// Validate that the template works
t, err := template.ParseFiles(StaticFolder + "/index.html") t, err := template.ParseFiles(StaticFolder + "/index.html")
if err != nil { if err != nil {
return err return err

View File

@ -1,6 +1,7 @@
package ui package ui
import ( import (
"strconv"
"testing" "testing"
) )
@ -26,6 +27,45 @@ func TestConfig_ValidateAndSetDefaults(t *testing.T) {
} }
} }
func TestButton_Validate(t *testing.T) {
scenarios := []struct {
Name, Link string
ExpectedError error
}{
{
Name: "",
Link: "",
ExpectedError: ErrButtonValidationFailed,
},
{
Name: "",
Link: "link",
ExpectedError: ErrButtonValidationFailed,
},
{
Name: "name",
Link: "",
ExpectedError: ErrButtonValidationFailed,
},
{
Name: "name",
Link: "link",
ExpectedError: nil,
},
}
for i, scenario := range scenarios {
t.Run(strconv.Itoa(i)+"_"+scenario.Name+"_"+scenario.Link, func(t *testing.T) {
button := &Button{
Name: scenario.Name,
Link: scenario.Link,
}
if err := button.Validate(); err != scenario.ExpectedError {
t.Errorf("expected error %v, got %v", scenario.ExpectedError, err)
}
})
}
}
func TestGetDefaultConfig(t *testing.T) { func TestGetDefaultConfig(t *testing.T) {
defaultConfig := GetDefaultConfig() defaultConfig := GetDefaultConfig()
if defaultConfig.Title != defaultTitle { if defaultConfig.Title != defaultTitle {

View File

@ -1,6 +1,6 @@
{ {
"name": "gatus", "name": "gatus",
"version": "3.6.0", "version": "3.7.0",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve --mode development", "serve": "vue-cli-service serve --mode development",

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<script type="text/javascript"> <script type="text/javascript">
window.config = {logo: "{{ .Logo }}", header: "{{ .Header }}", link: "{{ .Link }}"}; window.config = {logo: "{{ .Logo }}", header: "{{ .Header }}", link: "{{ .Link }}", buttons: []};{{- range .Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}
</script> </script>
<title>{{ .Title }}</title> <title>{{ .Title }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">

View File

@ -13,6 +13,11 @@
</a> </a>
</div> </div>
</div> </div>
<div v-if="buttons" class="flex flex-wrap">
<a v-for="button in buttons" :key="button.name" :href="button.link" target="_blank" class="px-2 py-0.5 font-medium select-none text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-400 hover:underline">
{{ button.name }}
</a>
</div>
</div> </div>
<router-view @showTooltip="showTooltip" /> <router-view @showTooltip="showTooltip" />
</div> </div>
@ -85,6 +90,9 @@ export default {
}, },
link() { link() {
return window.config && window.config.link && window.config.link !== '{{ .Link }}' ? window.config.link : null; return window.config && window.config.link && window.config.link !== '{{ .Link }}' ? window.config.link : null;
},
buttons() {
return window.config && window.config.buttons ? window.config.buttons : [];
} }
}, },
data() { data() {

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><script>window.config = {logo: "{{ .Logo }}", header: "{{ .Header }}", link: "{{ .Link }}"};</script><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" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="shortcut icon" href="/favicon.ico"/><meta property="description" content="Gatus is an advanced automated status page that lets you monitor your applications and configure alerts to notify you if there's an issue"/><script defer="defer" type="module" src="/js/chunk-vendors.js"></script><script defer="defer" type="module" src="/js/app.js"></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"/><script>window.config = {logo: "{{ .Logo }}", header: "{{ .Header }}", link: "{{ .Link }}", buttons: []};{{- range .Buttons}}window.config.buttons.push({name:"{{ .Name }}",link:"{{ .Link }}"});{{end}}</script><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" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="shortcut icon" href="/favicon.ico"/><meta property="description" content="Gatus is an advanced automated status page that lets you monitor your applications and configure alerts to notify you if there's an issue"/><script defer="defer" type="module" src="/js/chunk-vendors.js"></script><script defer="defer" type="module" src="/js/app.js"></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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long