mirror of
https://github.com/TwiN/gatus.git
synced 2024-11-25 09:24:04 +01:00
Add page for individual service details
This commit is contained in:
parent
2ccd656386
commit
dcbbec7931
@ -3,6 +3,7 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -53,12 +54,9 @@ func Handle() {
|
|||||||
// CreateRouter creates the router for the http server
|
// CreateRouter creates the router for the http server
|
||||||
func CreateRouter(cfg *config.Config) *mux.Router {
|
func CreateRouter(cfg *config.Config) *mux.Router {
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
statusesHandler := serviceStatusesHandler
|
|
||||||
if cfg.Security != nil && cfg.Security.IsValid() {
|
|
||||||
statusesHandler = security.Handler(serviceStatusesHandler, cfg.Security)
|
|
||||||
}
|
|
||||||
router.HandleFunc("/favicon.ico", favIconHandler).Methods("GET") // favicon needs to be always served from the root
|
router.HandleFunc("/favicon.ico", favIconHandler).Methods("GET") // favicon needs to be always served from the root
|
||||||
router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/statuses"), statusesHandler).Methods("GET")
|
router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/statuses"), secureIfNecessary(cfg, serviceStatusesHandler)).Methods("GET")
|
||||||
|
router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/statuses/{key}"), secureIfNecessary(cfg, GzipHandlerFunc(serviceStatusHandler))).Methods("GET")
|
||||||
router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/badges/uptime/{duration}/{identifier}"), badgeHandler).Methods("GET")
|
router.HandleFunc(cfg.Web.PrependWithContextRoot("/api/v1/badges/uptime/{duration}/{identifier}"), badgeHandler).Methods("GET")
|
||||||
router.HandleFunc(cfg.Web.PrependWithContextRoot("/health"), healthHandler).Methods("GET")
|
router.HandleFunc(cfg.Web.PrependWithContextRoot("/health"), healthHandler).Methods("GET")
|
||||||
router.PathPrefix(cfg.Web.ContextRoot).Handler(GzipHandler(http.StripPrefix(cfg.Web.ContextRoot, http.FileServer(http.Dir("./static")))))
|
router.PathPrefix(cfg.Web.ContextRoot).Handler(GzipHandler(http.StripPrefix(cfg.Web.ContextRoot, http.FileServer(http.Dir("./static")))))
|
||||||
@ -68,6 +66,16 @@ func CreateRouter(cfg *config.Config) *mux.Router {
|
|||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func secureIfNecessary(cfg *config.Config, handler http.HandlerFunc) http.HandlerFunc {
|
||||||
|
if cfg.Security != nil && cfg.Security.IsValid() {
|
||||||
|
return security.Handler(serviceStatusesHandler, cfg.Security)
|
||||||
|
}
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceStatusesHandler handles requests to retrieve all service statuses
|
||||||
|
// Due to the size of the response, this function leverages a cache.
|
||||||
|
// Must not be wrapped by GzipHandler
|
||||||
func serviceStatusesHandler(writer http.ResponseWriter, r *http.Request) {
|
func serviceStatusesHandler(writer http.ResponseWriter, r *http.Request) {
|
||||||
gzipped := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
|
gzipped := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
|
||||||
var exists bool
|
var exists bool
|
||||||
@ -85,7 +93,7 @@ func serviceStatusesHandler(writer http.ResponseWriter, r *http.Request) {
|
|||||||
gzipWriter := gzip.NewWriter(buffer)
|
gzipWriter := gzip.NewWriter(buffer)
|
||||||
data, err = watchdog.GetServiceStatusesAsJSON()
|
data, err = watchdog.GetServiceStatusesAsJSON()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[main][serviceStatusesHandler] Unable to marshal object to JSON: %s", err.Error())
|
log.Printf("[controller][serviceStatusesHandler] Unable to marshal object to JSON: %s", err.Error())
|
||||||
writer.WriteHeader(http.StatusInternalServerError)
|
writer.WriteHeader(http.StatusInternalServerError)
|
||||||
_, _ = writer.Write([]byte("Unable to marshal object to JSON"))
|
_, _ = writer.Write([]byte("Unable to marshal object to JSON"))
|
||||||
return
|
return
|
||||||
@ -106,6 +114,28 @@ func serviceStatusesHandler(writer http.ResponseWriter, r *http.Request) {
|
|||||||
_, _ = writer.Write(data)
|
_, _ = writer.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serviceStatusHandler retrieves a single ServiceStatus by group name and service name
|
||||||
|
func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
serviceStatus := watchdog.GetServiceStatusByKey(vars["key"])
|
||||||
|
if serviceStatus == nil {
|
||||||
|
log.Printf("[controller][serviceStatusHandler] Service with key=%s not found", vars["key"])
|
||||||
|
writer.WriteHeader(http.StatusNotFound)
|
||||||
|
_, _ = writer.Write([]byte("not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(serviceStatus)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[controller][serviceStatusHandler] Unable to marshal object to JSON: %s", err.Error())
|
||||||
|
writer.WriteHeader(http.StatusInternalServerError)
|
||||||
|
_, _ = writer.Write([]byte("Unable to marshal object to JSON"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writer.Header().Add("Content-Type", "application/json")
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = writer.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
func healthHandler(writer http.ResponseWriter, _ *http.Request) {
|
func healthHandler(writer http.ResponseWriter, _ *http.Request) {
|
||||||
writer.Header().Add("Content-Type", "application/json")
|
writer.Header().Add("Content-Type", "application/json")
|
||||||
writer.WriteHeader(http.StatusOK)
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
@ -32,10 +32,18 @@ func (w *gzipResponseWriter) Write(b []byte) (int, error) {
|
|||||||
return w.Writer.Write(b)
|
return w.Writer.Write(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GzipHandler compresses the response of a given handler if the request's headers specify that the client
|
// GzipHandler compresses the response of a given http.Handler if the request's headers specify that the client
|
||||||
// supports gzip encoding
|
// supports gzip encoding
|
||||||
func GzipHandler(next http.Handler) http.Handler {
|
func GzipHandler(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(writer http.ResponseWriter, r *http.Request) {
|
return GzipHandlerFunc(func(writer http.ResponseWriter, r *http.Request) {
|
||||||
|
next.ServeHTTP(writer, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GzipHandlerFunc compresses the response of a given http.HandlerFunc if the request's headers specify that the client
|
||||||
|
// supports gzip encoding
|
||||||
|
func GzipHandlerFunc(next http.HandlerFunc) http.HandlerFunc {
|
||||||
|
return func(writer http.ResponseWriter, r *http.Request) {
|
||||||
// If the request doesn't specify that it supports gzip, then don't compress it
|
// If the request doesn't specify that it supports gzip, then don't compress it
|
||||||
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
next.ServeHTTP(writer, r)
|
next.ServeHTTP(writer, r)
|
||||||
@ -47,5 +55,5 @@ func GzipHandler(next http.Handler) http.Handler {
|
|||||||
gz.Reset(writer)
|
gz.Reset(writer)
|
||||||
defer gz.Close()
|
defer gz.Close()
|
||||||
next.ServeHTTP(&gzipResponseWriter{ResponseWriter: writer, Writer: gz}, r)
|
next.ServeHTTP(&gzipResponseWriter{ResponseWriter: writer, Writer: gz}, r)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
|
import "github.com/TwinProduction/gatus/util"
|
||||||
|
|
||||||
// ServiceStatus contains the evaluation Results of a Service
|
// ServiceStatus contains the evaluation Results of a Service
|
||||||
type ServiceStatus struct {
|
type ServiceStatus struct {
|
||||||
// Name of the service
|
// Name of the service
|
||||||
@ -8,6 +10,9 @@ type ServiceStatus struct {
|
|||||||
// Group the service is a part of. Used for grouping multiple services together on the front end.
|
// Group the service is a part of. Used for grouping multiple services together on the front end.
|
||||||
Group string `json:"group,omitempty"`
|
Group string `json:"group,omitempty"`
|
||||||
|
|
||||||
|
// Key is the key representing the ServiceStatus
|
||||||
|
Key string `json:"key"`
|
||||||
|
|
||||||
// Results is the list of service evaluation results
|
// Results is the list of service evaluation results
|
||||||
Results []*Result `json:"results"`
|
Results []*Result `json:"results"`
|
||||||
|
|
||||||
@ -20,6 +25,7 @@ func NewServiceStatus(service *Service) *ServiceStatus {
|
|||||||
return &ServiceStatus{
|
return &ServiceStatus{
|
||||||
Name: service.Name,
|
Name: service.Name,
|
||||||
Group: service.Group,
|
Group: service.Group,
|
||||||
|
Key: util.ConvertGroupAndServiceToKey(service.Group, service.Name),
|
||||||
Results: make([]*Result, 0),
|
Results: make([]*Result, 0),
|
||||||
Uptime: NewUptime(),
|
Uptime: NewUptime(),
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,9 @@ func TestNewServiceStatus(t *testing.T) {
|
|||||||
if serviceStatus.Group != service.Group {
|
if serviceStatus.Group != service.Group {
|
||||||
t.Errorf("expected %s, got %s", service.Group, serviceStatus.Group)
|
t.Errorf("expected %s, got %s", service.Group, serviceStatus.Group)
|
||||||
}
|
}
|
||||||
|
if serviceStatus.Key != "group_name" {
|
||||||
|
t.Errorf("expected %s, got %s", "group_name", serviceStatus.Key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServiceStatus_AddResult(t *testing.T) {
|
func TestServiceStatus_AddResult(t *testing.T) {
|
||||||
|
@ -2,10 +2,10 @@ package storage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
|
"github.com/TwinProduction/gatus/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InMemoryStore implements an in-memory store
|
// InMemoryStore implements an in-memory store
|
||||||
@ -32,8 +32,16 @@ func (ims *InMemoryStore) GetAllAsJSON() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetServiceStatus returns the service status for a given service name in the given group
|
// GetServiceStatus returns the service status for a given service name in the given group
|
||||||
func (ims *InMemoryStore) GetServiceStatus(group, name string) *core.ServiceStatus {
|
func (ims *InMemoryStore) GetServiceStatus(groupName, serviceName string) *core.ServiceStatus {
|
||||||
key := fmt.Sprintf("%s_%s", group, name)
|
key := util.ConvertGroupAndServiceToKey(groupName, serviceName)
|
||||||
|
ims.serviceResultsMutex.RLock()
|
||||||
|
serviceStatus := ims.serviceStatuses[key]
|
||||||
|
ims.serviceResultsMutex.RUnlock()
|
||||||
|
return serviceStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetServiceStatusByKey returns the service status for a given key
|
||||||
|
func (ims *InMemoryStore) GetServiceStatusByKey(key string) *core.ServiceStatus {
|
||||||
ims.serviceResultsMutex.RLock()
|
ims.serviceResultsMutex.RLock()
|
||||||
serviceStatus := ims.serviceStatuses[key]
|
serviceStatus := ims.serviceStatuses[key]
|
||||||
ims.serviceResultsMutex.RUnlock()
|
ims.serviceResultsMutex.RUnlock()
|
||||||
@ -42,7 +50,7 @@ func (ims *InMemoryStore) GetServiceStatus(group, name string) *core.ServiceStat
|
|||||||
|
|
||||||
// Insert inserts the observed result for the specified service into the in memory store
|
// Insert inserts the observed result for the specified service into the in memory store
|
||||||
func (ims *InMemoryStore) Insert(service *core.Service, result *core.Result) {
|
func (ims *InMemoryStore) Insert(service *core.Service, result *core.Result) {
|
||||||
key := fmt.Sprintf("%s_%s", service.Group, service.Name)
|
key := util.ConvertGroupAndServiceToKey(service.Group, service.Name)
|
||||||
ims.serviceResultsMutex.Lock()
|
ims.serviceResultsMutex.Lock()
|
||||||
serviceStatus, exists := ims.serviceStatuses[key]
|
serviceStatus, exists := ims.serviceStatuses[key]
|
||||||
if !exists {
|
if !exists {
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
|
"github.com/TwinProduction/gatus/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -160,7 +161,6 @@ func TestInMemoryStore_GetServiceStatus(t *testing.T) {
|
|||||||
if serviceStatus.Uptime.LastSevenDays != 0.5 {
|
if serviceStatus.Uptime.LastSevenDays != 0.5 {
|
||||||
t.Errorf("serviceStatus.Uptime.LastSevenDays should've been 0.5")
|
t.Errorf("serviceStatus.Uptime.LastSevenDays should've been 0.5")
|
||||||
}
|
}
|
||||||
fmt.Println(serviceStatus.Results[0].Timestamp.Format(time.RFC3339))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInMemoryStore_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) {
|
func TestInMemoryStore_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) {
|
||||||
@ -181,6 +181,29 @@ func TestInMemoryStore_GetServiceStatusForMissingStatusReturnsNil(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInMemoryStore_GetServiceStatusByKey(t *testing.T) {
|
||||||
|
store := NewInMemoryStore()
|
||||||
|
store.Insert(&testService, &testSuccessfulResult)
|
||||||
|
store.Insert(&testService, &testUnsuccessfulResult)
|
||||||
|
|
||||||
|
serviceStatus := store.GetServiceStatusByKey(util.ConvertGroupAndServiceToKey(testService.Group, testService.Name))
|
||||||
|
if serviceStatus == nil {
|
||||||
|
t.Fatalf("serviceStatus shouldn't have been nil")
|
||||||
|
}
|
||||||
|
if serviceStatus.Uptime == nil {
|
||||||
|
t.Fatalf("serviceStatus.Uptime shouldn't have been nil")
|
||||||
|
}
|
||||||
|
if serviceStatus.Uptime.LastHour != 0.5 {
|
||||||
|
t.Errorf("serviceStatus.Uptime.LastHour should've been 0.5")
|
||||||
|
}
|
||||||
|
if serviceStatus.Uptime.LastTwentyFourHours != 0.5 {
|
||||||
|
t.Errorf("serviceStatus.Uptime.LastTwentyFourHours should've been 0.5")
|
||||||
|
}
|
||||||
|
if serviceStatus.Uptime.LastSevenDays != 0.5 {
|
||||||
|
t.Errorf("serviceStatus.Uptime.LastSevenDays should've been 0.5")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestInMemoryStore_GetAllAsJSON(t *testing.T) {
|
func TestInMemoryStore_GetAllAsJSON(t *testing.T) {
|
||||||
store := NewInMemoryStore()
|
store := NewInMemoryStore()
|
||||||
firstResult := &testSuccessfulResult
|
firstResult := &testSuccessfulResult
|
||||||
@ -194,7 +217,7 @@ func TestInMemoryStore_GetAllAsJSON(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("shouldn't have returned an error, got", err.Error())
|
t.Fatal("shouldn't have returned an error, got", err.Error())
|
||||||
}
|
}
|
||||||
expectedOutput := `{"group_name":{"name":"name","group":"group","results":[{"status":200,"hostname":"example.org","duration":150000000,"errors":null,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"condition-results":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}],"uptime":{"7d":0.5,"24h":0.5,"1h":0.5}}}`
|
expectedOutput := `{"group_name":{"name":"name","group":"group","results":[{"status":200,"hostname":"example.org","duration":150000000,"errors":null,"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":true},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":true}],"success":true,"timestamp":"0001-01-01T00:00:00Z"},{"status":200,"hostname":"example.org","duration":750000000,"errors":["error-1","error-2"],"conditionResults":[{"condition":"[STATUS] == 200","success":true},{"condition":"[RESPONSE_TIME] \u003c 500","success":false},{"condition":"[CERTIFICATE_EXPIRATION] \u003c 72h","success":false}],"success":false,"timestamp":"0001-01-01T00:00:00Z"}],"uptime":{"7d":0.5,"24h":0.5,"1h":0.5}}}`
|
||||||
if string(output) != expectedOutput {
|
if string(output) != expectedOutput {
|
||||||
t.Errorf("expected:\n %s\n\ngot:\n %s", expectedOutput, string(output))
|
t.Errorf("expected:\n %s\n\ngot:\n %s", expectedOutput, string(output))
|
||||||
}
|
}
|
||||||
|
18
util/key.go
Normal file
18
util/key.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// ConvertGroupAndServiceToKey converts a group and a service to a key
|
||||||
|
func ConvertGroupAndServiceToKey(group, service string) string {
|
||||||
|
return sanitize(group) + "_" + sanitize(service)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitize(s string) string {
|
||||||
|
s = strings.TrimSpace(strings.ToLower(s))
|
||||||
|
s = strings.ReplaceAll(s, "/", "-")
|
||||||
|
s = strings.ReplaceAll(s, "_", "-")
|
||||||
|
s = strings.ReplaceAll(s, ".", "-")
|
||||||
|
s = strings.ReplaceAll(s, ",", "-")
|
||||||
|
s = strings.ReplaceAll(s, " ", "-")
|
||||||
|
return s
|
||||||
|
}
|
36
util/key_test.go
Normal file
36
util/key_test.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestConvertGroupAndServiceToKey(t *testing.T) {
|
||||||
|
type Scenario struct {
|
||||||
|
GroupName string
|
||||||
|
ServiceName string
|
||||||
|
ExpectedOutput string
|
||||||
|
}
|
||||||
|
scenarios := []Scenario{
|
||||||
|
{
|
||||||
|
GroupName: "Core",
|
||||||
|
ServiceName: "Front End",
|
||||||
|
ExpectedOutput: "core_front-end",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
GroupName: "Load balancers",
|
||||||
|
ServiceName: "us-west-2",
|
||||||
|
ExpectedOutput: "load-balancers_us-west-2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
GroupName: "a/b test",
|
||||||
|
ServiceName: "a",
|
||||||
|
ExpectedOutput: "a-b-test_a",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.ExpectedOutput, func(t *testing.T) {
|
||||||
|
output := ConvertGroupAndServiceToKey(scenario.GroupName, scenario.ServiceName)
|
||||||
|
if output != scenario.ExpectedOutput {
|
||||||
|
t.Errorf("Expected '%s', got '%s'", scenario.ExpectedOutput, output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -25,15 +25,20 @@ func GetServiceStatusesAsJSON() ([]byte, error) {
|
|||||||
return store.GetAllAsJSON()
|
return store.GetAllAsJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUptimeByServiceGroupAndName returns the uptime of a service based on its group and name
|
// GetUptimeByKey returns the uptime of a service based on the ServiceStatus key
|
||||||
func GetUptimeByServiceGroupAndName(group, name string) *core.Uptime {
|
func GetUptimeByKey(key string) *core.Uptime {
|
||||||
serviceStatus := store.GetServiceStatus(group, name)
|
serviceStatus := store.GetServiceStatusByKey(key)
|
||||||
if serviceStatus == nil {
|
if serviceStatus == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return serviceStatus.Uptime
|
return serviceStatus.Uptime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetServiceStatusByKey returns the uptime of a service based on its ServiceStatus key
|
||||||
|
func GetServiceStatusByKey(key string) *core.ServiceStatus {
|
||||||
|
return store.GetServiceStatusByKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
// Monitor loops over each services and starts a goroutine to monitor each services separately
|
// Monitor loops over each services and starts a goroutine to monitor each services separately
|
||||||
func Monitor(cfg *config.Config) {
|
func Monitor(cfg *config.Config) {
|
||||||
for _, service := range cfg.Services {
|
for _, service := range cfg.Services {
|
||||||
|
32
web/app/package-lock.json
generated
32
web/app/package-lock.json
generated
@ -12,11 +12,13 @@
|
|||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"postcss": "^7.0.35",
|
"postcss": "^7.0.35",
|
||||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.2",
|
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.2",
|
||||||
"vue": "^3.0.0"
|
"vue": "^3.0.0",
|
||||||
|
"vue-router": "^4.0.0-0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "~4.5.0",
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-router": "~4.5.0",
|
||||||
"@vue/cli-service": "~4.5.0",
|
"@vue/cli-service": "~4.5.0",
|
||||||
"@vue/compiler-sfc": "^3.0.0",
|
"@vue/compiler-sfc": "^3.0.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
@ -14086,6 +14088,14 @@
|
|||||||
"integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=",
|
"integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-router": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-AD1OjtVPyQHTSpoRsEGfPpxRQwhAhxcacOYO3zJ3KNkYP/r09mileSp6kdMQKhZWP2cFsPR3E2M3PZguSN5/ww==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-style-loader": {
|
"node_modules/vue-style-loader": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz",
|
||||||
@ -17085,8 +17095,7 @@
|
|||||||
"version": "4.5.11",
|
"version": "4.5.11",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.11.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.11.tgz",
|
||||||
"integrity": "sha512-JBPeZLubiSHbRkEKDj0tnLiU43AJ3vt6JULn4IKWH1XWZ6MFC8vElaP5/AA4O3Zko5caamDDBq3TRyxdA2ncUQ==",
|
"integrity": "sha512-JBPeZLubiSHbRkEKDj0tnLiU43AJ3vt6JULn4IKWH1XWZ6MFC8vElaP5/AA4O3Zko5caamDDBq3TRyxdA2ncUQ==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"requires": {}
|
|
||||||
},
|
},
|
||||||
"@vue/cli-service": {
|
"@vue/cli-service": {
|
||||||
"version": "4.5.11",
|
"version": "4.5.11",
|
||||||
@ -17325,8 +17334,7 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz",
|
||||||
"integrity": "sha512-LIZMuJk38pk9U9Ur4YzHjlIyMuxPlACdBIHH9/nGYVTsaGKOSnSuELiE8vS9wa+dJpIYspYUOqk+L1Q4pgHQHQ==",
|
"integrity": "sha512-LIZMuJk38pk9U9Ur4YzHjlIyMuxPlACdBIHH9/nGYVTsaGKOSnSuELiE8vS9wa+dJpIYspYUOqk+L1Q4pgHQHQ==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"requires": {}
|
|
||||||
},
|
},
|
||||||
"@vue/reactivity": {
|
"@vue/reactivity": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
@ -17572,8 +17580,7 @@
|
|||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
|
||||||
"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
|
"integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"requires": {}
|
|
||||||
},
|
},
|
||||||
"acorn-node": {
|
"acorn-node": {
|
||||||
"version": "1.8.2",
|
"version": "1.8.2",
|
||||||
@ -17622,15 +17629,13 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
|
||||||
"integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
|
"integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"requires": {}
|
|
||||||
},
|
},
|
||||||
"ajv-keywords": {
|
"ajv-keywords": {
|
||||||
"version": "3.5.2",
|
"version": "3.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"requires": {}
|
|
||||||
},
|
},
|
||||||
"alphanum-sort": {
|
"alphanum-sort": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
@ -26881,6 +26886,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"vue-router": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-AD1OjtVPyQHTSpoRsEGfPpxRQwhAhxcacOYO3zJ3KNkYP/r09mileSp6kdMQKhZWP2cFsPR3E2M3PZguSN5/ww=="
|
||||||
|
},
|
||||||
"vue-style-loader": {
|
"vue-style-loader": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz",
|
||||||
|
@ -13,11 +13,13 @@
|
|||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"postcss": "^7.0.35",
|
"postcss": "^7.0.35",
|
||||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.2",
|
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.2",
|
||||||
"vue": "^3.0.0"
|
"vue": "^3.0.0",
|
||||||
|
"vue-router": "^4.0.0-0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "~4.5.0",
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
"@vue/cli-plugin-eslint": "~4.5.0",
|
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-router": "~4.5.0",
|
||||||
"@vue/cli-service": "~4.5.0",
|
"@vue/cli-service": "~4.5.0",
|
||||||
"@vue/compiler-sfc": "^3.0.0",
|
"@vue/compiler-sfc": "^3.0.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
|
@ -1,51 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<Services :serviceStatuses="serviceStatuses" :showStatusOnHover="true" @showTooltip="showTooltip"/>
|
<div class="container container-xs relative mx-auto rounded shadow-xl border my-3 p-5 text-left" id="global">
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
<div class="w-2/3 text-left my-auto">
|
||||||
|
<div class="title text-5xl font-light">Health Status</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-1/3 flex justify-end">
|
||||||
|
<img src="./assets/logo.png" alt="Gatus" style="min-width: 50px; max-width: 200px; width: 20%;"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<router-view @showTooltip="showTooltip"/>
|
||||||
|
</div>
|
||||||
<Tooltip :result="tooltip.result" :event="tooltip.event"/>
|
<Tooltip :result="tooltip.result" :event="tooltip.event"/>
|
||||||
<Social/>
|
<Social/>
|
||||||
<Settings @refreshStatuses="fetchStatuses"/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Social from './components/Social.vue'
|
import Social from './components/Social.vue'
|
||||||
import Settings from './components/Settings.vue'
|
|
||||||
import Services from './components/Services.vue';
|
|
||||||
import Tooltip from './components/Tooltip.vue';
|
import Tooltip from './components/Tooltip.vue';
|
||||||
import {SERVER_URL} from "./main.js";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
Services,
|
|
||||||
Social,
|
Social,
|
||||||
Settings,
|
|
||||||
Tooltip
|
Tooltip
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchStatuses() {
|
|
||||||
console.log("[App][fetchStatuses] Fetching statuses");
|
|
||||||
fetch(`${SERVER_URL}/api/v1/statuses`)
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (JSON.stringify(this.serviceStatuses) !== JSON.stringify(data)) {
|
|
||||||
console.log(data);
|
|
||||||
this.serviceStatuses = data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
showTooltip(result, event) {
|
showTooltip(result, event) {
|
||||||
this.tooltip = {result: result, event: event};
|
this.tooltip = {result: result, event: event};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
serviceStatuses: {},
|
|
||||||
tooltip: {}
|
tooltip: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
|
||||||
this.fetchStatuses();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -53,9 +44,10 @@ export default {
|
|||||||
<style>
|
<style>
|
||||||
html, body {
|
html, body {
|
||||||
background-color: #f7f9fb;
|
background-color: #f7f9fb;
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#global, #results {
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class='container px-3 py-3 border-l border-r border-t rounded-none'>
|
<div class='service container px-3 py-3 border-l border-r border-t rounded-none' v-if="data && data.results && data.results.length">
|
||||||
<div class='flex flex-wrap mb-2'>
|
<div class='flex flex-wrap mb-2'>
|
||||||
<div class='w-3/4'>
|
<div class='w-3/4'>
|
||||||
<span class='font-bold'>{{ data.name }}</span> <span class='text-gray-500 font-light'>- {{ data.results[data.results.length - 1].hostname }}</span>
|
<router-link :to="generatePath()" class="font-bold transition duration-200 ease-in-out hover:text-blue-900">{{ data.name }}</router-link> <span class='text-gray-500 font-light'>- {{ data.results[data.results.length - 1].hostname }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class='w-1/4 text-right'>
|
<div class='w-1/4 text-right'>
|
||||||
<span class='font-light status-min-max-ms'>
|
<span class='font-light status-min-max-ms'>
|
||||||
@ -41,6 +41,7 @@ export default {
|
|||||||
maximumNumberOfResults: Number,
|
maximumNumberOfResults: Number,
|
||||||
data: Object,
|
data: Object,
|
||||||
},
|
},
|
||||||
|
emits: ['showTooltip'],
|
||||||
methods: {
|
methods: {
|
||||||
updateMinAndMaxResponseTimes() {
|
updateMinAndMaxResponseTimes() {
|
||||||
let minResponseTime = null;
|
let minResponseTime = null;
|
||||||
@ -73,6 +74,12 @@ export default {
|
|||||||
}
|
}
|
||||||
return (differenceInMs/1000).toFixed(0) + " seconds ago";
|
return (differenceInMs/1000).toFixed(0) + " seconds ago";
|
||||||
},
|
},
|
||||||
|
generatePath() {
|
||||||
|
if (!this.data) {
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
return "/services/" + this.data.key;
|
||||||
|
},
|
||||||
showTooltip(result, event) {
|
showTooltip(result, event) {
|
||||||
this.$emit('showTooltip', result, event);
|
this.$emit('showTooltip', result, event);
|
||||||
}
|
}
|
||||||
@ -96,6 +103,19 @@ export default {
|
|||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.service:first-child {
|
||||||
|
border-top-left-radius: 3px;
|
||||||
|
border-top-right-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service:last-child {
|
||||||
|
border-bottom-left-radius: 3px;
|
||||||
|
border-bottom-right-radius: 3px;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-color: #dee2e6;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 500ms ease-in-out;
|
transition: all 500ms ease-in-out;
|
||||||
|
@ -33,6 +33,7 @@ export default {
|
|||||||
name: String,
|
name: String,
|
||||||
services: Array
|
services: Array
|
||||||
},
|
},
|
||||||
|
emits: ['showTooltip'],
|
||||||
methods: {
|
methods: {
|
||||||
healthCheck() {
|
healthCheck() {
|
||||||
if (this.services) {
|
if (this.services) {
|
||||||
|
@ -1,21 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container mx-auto rounded shadow-xl border my-3 p-5 text-left" id="global">
|
|
||||||
<div class="mb-2">
|
|
||||||
<div class="flex flex-wrap">
|
|
||||||
<div class="w-2/3 text-left my-auto">
|
|
||||||
<div class="title font-light">Health Status</div>
|
|
||||||
</div>
|
|
||||||
<div class="w-1/3 flex justify-end">
|
|
||||||
<img src="../assets/logo.png" alt="Gatus" style="min-width: 50px; max-width: 200px; width: 20%;"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="results">
|
<div id="results">
|
||||||
<slot v-for="serviceGroup in serviceGroups" :key="serviceGroup">
|
<slot v-for="serviceGroup in serviceGroups" :key="serviceGroup">
|
||||||
<ServiceGroup :services="serviceGroup.services" :name="serviceGroup.name" @showTooltip="showTooltip"/>
|
<ServiceGroup :services="serviceGroup.services" :name="serviceGroup.name" @showTooltip="showTooltip"/>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
@ -31,6 +19,7 @@ export default {
|
|||||||
showStatusOnHover: Boolean,
|
showStatusOnHover: Boolean,
|
||||||
serviceStatuses: Object
|
serviceStatuses: Object
|
||||||
},
|
},
|
||||||
|
emits: ['showTooltip'],
|
||||||
methods: {
|
methods: {
|
||||||
process() {
|
process() {
|
||||||
let outputByGroup = {};
|
let outputByGroup = {};
|
||||||
@ -74,29 +63,8 @@ export default {
|
|||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#global {
|
.service-group-content > div:nth-child(1) {
|
||||||
max-width: 1140px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#results div.container:first-child {
|
|
||||||
border-top-left-radius: 3px;
|
|
||||||
border-top-right-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#results div.container:last-child {
|
|
||||||
border-bottom-left-radius: 3px;
|
|
||||||
border-bottom-right-radius: 3px;
|
|
||||||
border-bottom-width: 1px;
|
|
||||||
border-color: #dee2e6;
|
|
||||||
border-style: solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
#results .service-group-content > div:nth-child(1) {
|
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -25,14 +25,14 @@ export default {
|
|||||||
setRefreshInterval(seconds) {
|
setRefreshInterval(seconds) {
|
||||||
let that = this;
|
let that = this;
|
||||||
this.refreshIntervalHandler = setInterval(function() {
|
this.refreshIntervalHandler = setInterval(function() {
|
||||||
that.refreshStatuses();
|
that.refreshData();
|
||||||
}, seconds * 1000);
|
}, seconds * 1000);
|
||||||
},
|
},
|
||||||
refreshStatuses() {
|
refreshData() {
|
||||||
this.$emit('refreshStatuses');
|
this.$emit('refreshData');
|
||||||
},
|
},
|
||||||
handleChangeRefreshInterval() {
|
handleChangeRefreshInterval() {
|
||||||
this.refreshStatuses();
|
this.refreshData();
|
||||||
clearInterval(this.refreshIntervalHandler);
|
clearInterval(this.refreshIntervalHandler);
|
||||||
this.setRefreshInterval(this.$refs.refreshInterval.value);
|
this.setRefreshInterval(this.$refs.refreshInterval.value);
|
||||||
}
|
}
|
||||||
@ -40,6 +40,9 @@ export default {
|
|||||||
created() {
|
created() {
|
||||||
this.setRefreshInterval(this.refreshInterval);
|
this.setRefreshInterval(this.refreshInterval);
|
||||||
},
|
},
|
||||||
|
unmounted() {
|
||||||
|
clearInterval(this.refreshIntervalHandler);
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
refreshInterval: 30,
|
refreshInterval: 30,
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
export const SERVER_URL = process.env.NODE_ENV === 'production' ? '.' : 'http://localhost:8080'
|
export const SERVER_URL = process.env.NODE_ENV === 'production' ? '.' : 'http://localhost:8080'
|
||||||
|
|
||||||
createApp(App).mount('#app')
|
createApp(App).use(router).mount('#app')
|
||||||
|
23
web/app/src/router/index.js
Normal file
23
web/app/src/router/index.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import Home from '../views/Home.vue'
|
||||||
|
import Details from "@/views/Details";
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: Home
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/services/:key',
|
||||||
|
name: 'Details',
|
||||||
|
component: Details
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(process.env.BASE_URL),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
90
web/app/src/views/Details.vue
Normal file
90
web/app/src/views/Details.vue
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<router-link to="/" class="absolute top-2 left-2 inline-block px-2 py-0 text-lg font-medium leading-6 text-center text-black transition bg-gray-100 rounded shadow ripple hover:shadow-lg hover:bg-gray-200 focus:outline-none">
|
||||||
|
←
|
||||||
|
</router-link>
|
||||||
|
<div class="container mx-auto">
|
||||||
|
<slot v-if="serviceStatus">
|
||||||
|
<h1 class="text-3xl text-monospace text-gray-400">RECENT CHECKS</h1>
|
||||||
|
<hr class="mb-4" />
|
||||||
|
<Service :data="serviceStatus" :maximumNumberOfResults="20" @showTooltip="showTooltip" />
|
||||||
|
</slot>
|
||||||
|
<!-- print table of each results in table? that'd be sick as fuck -->
|
||||||
|
|
||||||
|
<div v-if="serviceStatus.uptime" class="mt-5">
|
||||||
|
<h1 class="text-3xl text-monospace text-gray-400">UPTIME</h1>
|
||||||
|
<hr />
|
||||||
|
<div class="flex space-x-4 text-center text-2xl mt-5">
|
||||||
|
<div class="flex-1">
|
||||||
|
{{ prettifyUptime(serviceStatus.uptime['7d']) }}
|
||||||
|
<h2 class="text-sm text-gray-400">Last 7 days</h2>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
{{ prettifyUptime(serviceStatus.uptime['24h']) }}
|
||||||
|
<h2 class="text-sm text-gray-400">Last 24 hours</h2>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
{{ prettifyUptime(serviceStatus.uptime['1h']) }}
|
||||||
|
<h2 class="text-sm text-gray-400">Last hour</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<Settings @refreshData="fetchData"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Settings from '@/components/Settings.vue'
|
||||||
|
import Service from '@/components/Service.vue';
|
||||||
|
import {SERVER_URL} from "@/main.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Details',
|
||||||
|
components: {
|
||||||
|
Service,
|
||||||
|
Settings,
|
||||||
|
},
|
||||||
|
emits: ['showTooltip'],
|
||||||
|
methods: {
|
||||||
|
fetchData() {
|
||||||
|
console.log("[Details][fetchData] Fetching data");
|
||||||
|
fetch(`${SERVER_URL}/api/v1/statuses/${this.$route.params.key}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (JSON.stringify(this.serviceStatus) !== JSON.stringify(data)) {
|
||||||
|
console.log(data);
|
||||||
|
this.serviceStatus = data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
prettifyUptime(uptime) {
|
||||||
|
if (!uptime) {
|
||||||
|
return "0%";
|
||||||
|
}
|
||||||
|
return (uptime * 100).toFixed(2) + "%"
|
||||||
|
},
|
||||||
|
showTooltip(result, event) {
|
||||||
|
this.$emit('showTooltip', result, event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
serviceStatus: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.service {
|
||||||
|
border-bottom-left-radius: 3px;
|
||||||
|
border-bottom-right-radius: 3px;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-color: #dee2e6;
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
</style>
|
43
web/app/src/views/Home.vue
Normal file
43
web/app/src/views/Home.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<Services :serviceStatuses="serviceStatuses" :showStatusOnHover="true" @showTooltip="showTooltip"/>
|
||||||
|
<Settings @refreshData="fetchData"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Settings from '@/components/Settings.vue'
|
||||||
|
import Services from '@/components/Services.vue';
|
||||||
|
import {SERVER_URL} from "@/main.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Home',
|
||||||
|
components: {
|
||||||
|
Services,
|
||||||
|
Settings,
|
||||||
|
},
|
||||||
|
emits: ['showTooltip'],
|
||||||
|
methods: {
|
||||||
|
fetchData() {
|
||||||
|
console.log("[Home][fetchData] Fetching data");
|
||||||
|
fetch(`${SERVER_URL}/api/v1/statuses`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (JSON.stringify(this.serviceStatuses) !== JSON.stringify(data)) {
|
||||||
|
console.log(data);
|
||||||
|
this.serviceStatuses = data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
showTooltip(result, event) {
|
||||||
|
this.$emit('showTooltip', result, event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
serviceStatuses: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
Loading…
Reference in New Issue
Block a user