mirror of
https://github.com/TwiN/gatus.git
synced 2024-12-22 14:41:01 +01:00
#89: First implementation of longer result history
This commit is contained in:
parent
42825b62fb
commit
dc929dac70
@ -7,7 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
"github.com/TwinProduction/gatus/watchdog"
|
"github.com/TwinProduction/gatus/storage"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -25,18 +25,23 @@ func badgeHandler(writer http.ResponseWriter, request *http.Request) {
|
|||||||
}
|
}
|
||||||
identifier := variables["identifier"]
|
identifier := variables["identifier"]
|
||||||
key := strings.TrimSuffix(identifier, ".svg")
|
key := strings.TrimSuffix(identifier, ".svg")
|
||||||
uptime := watchdog.GetUptimeByKey(key)
|
serviceStatus := storage.Get().GetServiceStatusByKey(key)
|
||||||
if uptime == nil {
|
if serviceStatus == nil {
|
||||||
writer.WriteHeader(http.StatusNotFound)
|
writer.WriteHeader(http.StatusNotFound)
|
||||||
_, _ = writer.Write([]byte("Requested service not found"))
|
_, _ = writer.Write([]byte("Requested service not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if serviceStatus.Uptime == nil {
|
||||||
|
writer.WriteHeader(http.StatusInternalServerError)
|
||||||
|
_, _ = writer.Write([]byte("Failed to compute uptime"))
|
||||||
|
return
|
||||||
|
}
|
||||||
formattedDate := time.Now().Format(http.TimeFormat)
|
formattedDate := time.Now().Format(http.TimeFormat)
|
||||||
writer.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
writer.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
writer.Header().Set("Date", formattedDate)
|
writer.Header().Set("Date", formattedDate)
|
||||||
writer.Header().Set("Expires", formattedDate)
|
writer.Header().Set("Expires", formattedDate)
|
||||||
writer.Header().Set("Content-Type", "image/svg+xml")
|
writer.Header().Set("Content-Type", "image/svg+xml")
|
||||||
_, _ = writer.Write(generateSVG(duration, uptime))
|
_, _ = writer.Write(generateSVG(duration, serviceStatus.Uptime))
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateSVG(duration string, uptime *core.Uptime) []byte {
|
func generateSVG(duration string, uptime *core.Uptime) []byte {
|
||||||
|
@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwinProduction/gatus/config"
|
"github.com/TwinProduction/gatus/config"
|
||||||
"github.com/TwinProduction/gatus/security"
|
"github.com/TwinProduction/gatus/security"
|
||||||
"github.com/TwinProduction/gatus/watchdog"
|
"github.com/TwinProduction/gatus/storage"
|
||||||
"github.com/TwinProduction/gocache"
|
"github.com/TwinProduction/gocache"
|
||||||
"github.com/TwinProduction/health"
|
"github.com/TwinProduction/health"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
@ -101,21 +101,22 @@ func secureIfNecessary(cfg *config.Config, handler http.HandlerFunc) http.Handle
|
|||||||
// Due to the size of the response, this function leverages a cache.
|
// Due to the size of the response, this function leverages a cache.
|
||||||
// Must not be wrapped by GzipHandler
|
// Must not be wrapped by GzipHandler
|
||||||
func serviceStatusesHandler(writer http.ResponseWriter, r *http.Request) {
|
func serviceStatusesHandler(writer http.ResponseWriter, r *http.Request) {
|
||||||
|
page, pageSize := extractPageAndPageSizeFromRequest(r)
|
||||||
gzipped := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
|
gzipped := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
|
||||||
var exists bool
|
var exists bool
|
||||||
var value interface{}
|
var value interface{}
|
||||||
if gzipped {
|
if gzipped {
|
||||||
writer.Header().Set("Content-Encoding", "gzip")
|
writer.Header().Set("Content-Encoding", "gzip")
|
||||||
value, exists = cache.Get("service-status-gzipped")
|
value, exists = cache.Get(fmt.Sprintf("service-status-%d-%d-gzipped", page, pageSize))
|
||||||
} else {
|
} else {
|
||||||
value, exists = cache.Get("service-status")
|
value, exists = cache.Get(fmt.Sprintf("service-status-%d-%d", page, pageSize))
|
||||||
}
|
}
|
||||||
var data []byte
|
var data []byte
|
||||||
if !exists {
|
if !exists {
|
||||||
var err error
|
var err error
|
||||||
buffer := &bytes.Buffer{}
|
buffer := &bytes.Buffer{}
|
||||||
gzipWriter := gzip.NewWriter(buffer)
|
gzipWriter := gzip.NewWriter(buffer)
|
||||||
data, err = watchdog.GetServiceStatusesAsJSON()
|
data, err = json.Marshal(storage.Get().GetAllServiceStatusesWithResultPagination(page, pageSize))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[controller][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)
|
||||||
@ -125,8 +126,8 @@ func serviceStatusesHandler(writer http.ResponseWriter, r *http.Request) {
|
|||||||
_, _ = gzipWriter.Write(data)
|
_, _ = gzipWriter.Write(data)
|
||||||
_ = gzipWriter.Close()
|
_ = gzipWriter.Close()
|
||||||
gzippedData := buffer.Bytes()
|
gzippedData := buffer.Bytes()
|
||||||
cache.SetWithTTL("service-status", data, cacheTTL)
|
cache.SetWithTTL(fmt.Sprintf("service-status-%d-%d", page, pageSize), data, cacheTTL)
|
||||||
cache.SetWithTTL("service-status-gzipped", gzippedData, cacheTTL)
|
cache.SetWithTTL(fmt.Sprintf("service-status-%d-%d-gzipped", page, pageSize), gzippedData, cacheTTL)
|
||||||
if gzipped {
|
if gzipped {
|
||||||
data = gzippedData
|
data = gzippedData
|
||||||
}
|
}
|
||||||
@ -140,8 +141,9 @@ func serviceStatusesHandler(writer http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// serviceStatusHandler retrieves a single ServiceStatus by group name and service name
|
// serviceStatusHandler retrieves a single ServiceStatus by group name and service name
|
||||||
func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) {
|
func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) {
|
||||||
|
page, pageSize := extractPageAndPageSizeFromRequest(r)
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
serviceStatus := watchdog.GetServiceStatusByKey(vars["key"])
|
serviceStatus := storage.Get().GetServiceStatusByKey(vars["key"])
|
||||||
if serviceStatus == nil {
|
if serviceStatus == nil {
|
||||||
log.Printf("[controller][serviceStatusHandler] Service with key=%s not found", vars["key"])
|
log.Printf("[controller][serviceStatusHandler] Service with key=%s not found", vars["key"])
|
||||||
writer.WriteHeader(http.StatusNotFound)
|
writer.WriteHeader(http.StatusNotFound)
|
||||||
@ -149,7 +151,7 @@ func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"serviceStatus": serviceStatus,
|
"serviceStatus": serviceStatus.WithResultPagination(page, pageSize),
|
||||||
// The following fields, while present on core.ServiceStatus, are annotated to remain hidden so that we can
|
// The following fields, while present on core.ServiceStatus, are annotated to remain hidden so that we can
|
||||||
// expose only the necessary data on /api/v1/statuses.
|
// expose only the necessary data on /api/v1/statuses.
|
||||||
// Since the /api/v1/statuses/{key} endpoint does need this data, however, we explicitly expose it here
|
// Since the /api/v1/statuses/{key} endpoint does need this data, however, we explicitly expose it here
|
||||||
@ -160,20 +162,10 @@ func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[controller][serviceStatusHandler] Unable to marshal object to JSON: %s", err.Error())
|
log.Printf("[controller][serviceStatusHandler] 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
|
||||||
}
|
}
|
||||||
writer.Header().Add("Content-Type", "application/json")
|
writer.Header().Add("Content-Type", "application/json")
|
||||||
writer.WriteHeader(http.StatusOK)
|
writer.WriteHeader(http.StatusOK)
|
||||||
_, _ = writer.Write(output)
|
_, _ = writer.Write(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
// favIconHandler handles requests for /favicon.ico
|
|
||||||
func favIconHandler(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
http.ServeFile(writer, request, staticFolder+"/favicon.ico")
|
|
||||||
}
|
|
||||||
|
|
||||||
// spaHandler handles requests for /favicon.ico
|
|
||||||
func spaHandler(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
http.ServeFile(writer, request, staticFolder+"/index.html")
|
|
||||||
}
|
|
||||||
|
@ -10,10 +10,87 @@ import (
|
|||||||
|
|
||||||
"github.com/TwinProduction/gatus/config"
|
"github.com/TwinProduction/gatus/config"
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
|
"github.com/TwinProduction/gatus/storage"
|
||||||
"github.com/TwinProduction/gatus/watchdog"
|
"github.com/TwinProduction/gatus/watchdog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
firstCondition = core.Condition("[STATUS] == 200")
|
||||||
|
secondCondition = core.Condition("[RESPONSE_TIME] < 500")
|
||||||
|
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
||||||
|
|
||||||
|
timestamp = time.Now()
|
||||||
|
|
||||||
|
testService = core.Service{
|
||||||
|
Name: "name",
|
||||||
|
Group: "group",
|
||||||
|
URL: "https://example.org/what/ever",
|
||||||
|
Method: "GET",
|
||||||
|
Body: "body",
|
||||||
|
Interval: 30 * time.Second,
|
||||||
|
Conditions: []*core.Condition{&firstCondition, &secondCondition, &thirdCondition},
|
||||||
|
Alerts: nil,
|
||||||
|
Insecure: false,
|
||||||
|
NumberOfFailuresInARow: 0,
|
||||||
|
NumberOfSuccessesInARow: 0,
|
||||||
|
}
|
||||||
|
testSuccessfulResult = core.Result{
|
||||||
|
Hostname: "example.org",
|
||||||
|
IP: "127.0.0.1",
|
||||||
|
HTTPStatus: 200,
|
||||||
|
Body: []byte("body"),
|
||||||
|
Errors: nil,
|
||||||
|
Connected: true,
|
||||||
|
Success: true,
|
||||||
|
Timestamp: timestamp,
|
||||||
|
Duration: 150 * time.Millisecond,
|
||||||
|
CertificateExpiration: 10 * time.Hour,
|
||||||
|
ConditionResults: []*core.ConditionResult{
|
||||||
|
{
|
||||||
|
Condition: "[STATUS] == 200",
|
||||||
|
Success: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Condition: "[RESPONSE_TIME] < 500",
|
||||||
|
Success: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Condition: "[CERTIFICATE_EXPIRATION] < 72h",
|
||||||
|
Success: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testUnsuccessfulResult = core.Result{
|
||||||
|
Hostname: "example.org",
|
||||||
|
IP: "127.0.0.1",
|
||||||
|
HTTPStatus: 200,
|
||||||
|
Body: []byte("body"),
|
||||||
|
Errors: []string{"error-1", "error-2"},
|
||||||
|
Connected: true,
|
||||||
|
Success: false,
|
||||||
|
Timestamp: timestamp,
|
||||||
|
Duration: 750 * time.Millisecond,
|
||||||
|
CertificateExpiration: 10 * time.Hour,
|
||||||
|
ConditionResults: []*core.ConditionResult{
|
||||||
|
{
|
||||||
|
Condition: "[STATUS] == 200",
|
||||||
|
Success: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Condition: "[RESPONSE_TIME] < 500",
|
||||||
|
Success: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Condition: "[CERTIFICATE_EXPIRATION] < 72h",
|
||||||
|
Success: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func TestCreateRouter(t *testing.T) {
|
func TestCreateRouter(t *testing.T) {
|
||||||
|
defer storage.Get().Clear()
|
||||||
|
defer cache.Clear()
|
||||||
staticFolder = "../web/static"
|
staticFolder = "../web/static"
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Metrics: true,
|
Metrics: true,
|
||||||
@ -137,6 +214,8 @@ func TestCreateRouter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandle(t *testing.T) {
|
func TestHandle(t *testing.T) {
|
||||||
|
defer storage.Get().Clear()
|
||||||
|
defer cache.Clear()
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Web: &config.WebConfig{
|
Web: &config.WebConfig{
|
||||||
Address: "0.0.0.0",
|
Address: "0.0.0.0",
|
||||||
@ -154,10 +233,12 @@ func TestHandle(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
config.Set(cfg)
|
config.Set(cfg)
|
||||||
|
defer config.Set(nil)
|
||||||
_ = 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()
|
Handle()
|
||||||
|
defer Shutdown()
|
||||||
request, _ := http.NewRequest("GET", "/health", nil)
|
request, _ := http.NewRequest("GET", "/health", nil)
|
||||||
responseRecorder := httptest.NewRecorder()
|
responseRecorder := httptest.NewRecorder()
|
||||||
server.Handler.ServeHTTP(responseRecorder, request)
|
server.Handler.ServeHTTP(responseRecorder, request)
|
||||||
@ -177,3 +258,30 @@ func TestShutdown(t *testing.T) {
|
|||||||
t.Error("server should've been shut down")
|
t.Error("server should've been shut down")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServiceStatusesHandler(t *testing.T) {
|
||||||
|
defer storage.Get().Clear()
|
||||||
|
defer cache.Clear()
|
||||||
|
staticFolder = "../web/static"
|
||||||
|
firstResult := &testSuccessfulResult
|
||||||
|
secondResult := &testUnsuccessfulResult
|
||||||
|
storage.Get().Insert(&testService, firstResult)
|
||||||
|
storage.Get().Insert(&testService, secondResult)
|
||||||
|
// 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(&config.Config{})
|
||||||
|
|
||||||
|
request, _ := http.NewRequest("GET", "/api/v1/statuses", nil)
|
||||||
|
responseRecorder := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(responseRecorder, request)
|
||||||
|
if responseRecorder.Code != http.StatusOK {
|
||||||
|
t.Errorf("%s %s should have returned %d, but returned %d instead", request.Method, request.URL, http.StatusOK, responseRecorder.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
output := responseRecorder.Body.String()
|
||||||
|
expectedOutput := `{"group_name":{"name":"name","group":"group","key":"group_name","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"}]}}`
|
||||||
|
if output != expectedOutput {
|
||||||
|
t.Errorf("expected:\n %s\n\ngot:\n %s", expectedOutput, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
8
controller/favicon.go
Normal file
8
controller/favicon.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// favIconHandler handles requests for /favicon.ico
|
||||||
|
func favIconHandler(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
http.ServeFile(writer, request, staticFolder+"/favicon.ico")
|
||||||
|
}
|
8
controller/spa.go
Normal file
8
controller/spa.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// spaHandler handles requests for /
|
||||||
|
func spaHandler(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
http.ServeFile(writer, request, staticFolder+"/index.html")
|
||||||
|
}
|
30
controller/util.go
Normal file
30
controller/util.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func extractPageAndPageSizeFromRequest(r *http.Request) (page int, pageSize int) {
|
||||||
|
var err error
|
||||||
|
if pageParameter := r.URL.Query().Get("page"); len(pageParameter) == 0 {
|
||||||
|
page = 1
|
||||||
|
} else {
|
||||||
|
page, err = strconv.Atoi(pageParameter)
|
||||||
|
if err != nil {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pageSizeParameter := r.URL.Query().Get("pageSize"); len(pageSizeParameter) == 0 {
|
||||||
|
pageSize = 20
|
||||||
|
} else {
|
||||||
|
pageSize, err = strconv.Atoi(pageSizeParameter)
|
||||||
|
if err != nil {
|
||||||
|
pageSize = 20
|
||||||
|
}
|
||||||
|
if pageSize > 100 {
|
||||||
|
pageSize = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// MaximumNumberOfResults is the maximum number of results that ServiceStatus.Results can have
|
// MaximumNumberOfResults is the maximum number of results that ServiceStatus.Results can have
|
||||||
MaximumNumberOfResults = 20
|
MaximumNumberOfResults = 100
|
||||||
|
|
||||||
// MaximumNumberOfEvents is the maximum number of events that ServiceStatus.Events can have
|
// MaximumNumberOfEvents is the maximum number of events that ServiceStatus.Events can have
|
||||||
MaximumNumberOfEvents = 50
|
MaximumNumberOfEvents = 50
|
||||||
@ -58,6 +58,41 @@ func NewServiceStatus(service *Service) *ServiceStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShallowCopy creates a shallow copy of ServiceStatus
|
||||||
|
func (ss *ServiceStatus) ShallowCopy() *ServiceStatus {
|
||||||
|
return &ServiceStatus{
|
||||||
|
Name: ss.Name,
|
||||||
|
Group: ss.Group,
|
||||||
|
Key: ss.Key,
|
||||||
|
Results: ss.Results,
|
||||||
|
Events: ss.Events,
|
||||||
|
Uptime: ss.Uptime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithResultPagination makes a shallow copy of the ServiceStatus with only the results
|
||||||
|
// within the range defined by the page and pageSize parameters
|
||||||
|
func (ss *ServiceStatus) WithResultPagination(page, pageSize int) *ServiceStatus {
|
||||||
|
shallowCopy := ss.ShallowCopy()
|
||||||
|
numberOfResults := len(shallowCopy.Results)
|
||||||
|
start := numberOfResults - (page * pageSize)
|
||||||
|
end := numberOfResults - ((page - 1) * pageSize)
|
||||||
|
if start > numberOfResults {
|
||||||
|
start = -1
|
||||||
|
} else if start < 0 {
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
if end > numberOfResults {
|
||||||
|
end = numberOfResults
|
||||||
|
}
|
||||||
|
if start < 0 || end < 0 {
|
||||||
|
shallowCopy.Results = []*Result{}
|
||||||
|
} else {
|
||||||
|
shallowCopy.Results = shallowCopy.Results[start:end]
|
||||||
|
}
|
||||||
|
return shallowCopy
|
||||||
|
}
|
||||||
|
|
||||||
// AddResult adds a Result to ServiceStatus.Results and makes sure that there are
|
// AddResult adds a Result to ServiceStatus.Results and makes sure that there are
|
||||||
// no more than 20 results in the Results slice
|
// no more than 20 results in the Results slice
|
||||||
func (ss *ServiceStatus) AddResult(result *Result) {
|
func (ss *ServiceStatus) AddResult(result *Result) {
|
||||||
|
@ -22,10 +22,45 @@ func TestNewServiceStatus(t *testing.T) {
|
|||||||
func TestServiceStatus_AddResult(t *testing.T) {
|
func TestServiceStatus_AddResult(t *testing.T) {
|
||||||
service := &Service{Name: "name", Group: "group"}
|
service := &Service{Name: "name", Group: "group"}
|
||||||
serviceStatus := NewServiceStatus(service)
|
serviceStatus := NewServiceStatus(service)
|
||||||
for i := 0; i < 50; i++ {
|
for i := 0; i < MaximumNumberOfResults+10; i++ {
|
||||||
serviceStatus.AddResult(&Result{Timestamp: time.Now()})
|
serviceStatus.AddResult(&Result{Timestamp: time.Now()})
|
||||||
}
|
}
|
||||||
if len(serviceStatus.Results) != 20 {
|
if len(serviceStatus.Results) != MaximumNumberOfResults {
|
||||||
t.Errorf("expected serviceStatus.Results to not exceed a length of 20")
|
t.Errorf("expected serviceStatus.Results to not exceed a length of 20")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServiceStatus_WithResultPagination(t *testing.T) {
|
||||||
|
service := &Service{Name: "name", Group: "group"}
|
||||||
|
serviceStatus := NewServiceStatus(service)
|
||||||
|
for i := 0; i < 25; i++ {
|
||||||
|
serviceStatus.AddResult(&Result{Timestamp: time.Now()})
|
||||||
|
}
|
||||||
|
if len(serviceStatus.WithResultPagination(1, 1).Results) != 1 {
|
||||||
|
t.Errorf("expected to have 1 result")
|
||||||
|
}
|
||||||
|
if len(serviceStatus.WithResultPagination(5, 0).Results) != 0 {
|
||||||
|
t.Errorf("expected to have 0 results")
|
||||||
|
}
|
||||||
|
if len(serviceStatus.WithResultPagination(-1, 20).Results) != 0 {
|
||||||
|
t.Errorf("expected to have 0 result, because the page was invalid")
|
||||||
|
}
|
||||||
|
if len(serviceStatus.WithResultPagination(1, -1).Results) != 0 {
|
||||||
|
t.Errorf("expected to have 0 result, because the page size was invalid")
|
||||||
|
}
|
||||||
|
if len(serviceStatus.WithResultPagination(1, 10).Results) != 10 {
|
||||||
|
t.Errorf("expected to have 10 results, because given a page size of 10, page 1 should have 10 elements")
|
||||||
|
}
|
||||||
|
if len(serviceStatus.WithResultPagination(2, 10).Results) != 10 {
|
||||||
|
t.Errorf("expected to have 10 results, because given a page size of 10, page 2 should have 10 elements")
|
||||||
|
}
|
||||||
|
if len(serviceStatus.WithResultPagination(3, 10).Results) != 5 {
|
||||||
|
t.Errorf("expected to have 5 results, because given a page size of 10, page 3 should have 5 elements")
|
||||||
|
}
|
||||||
|
if len(serviceStatus.WithResultPagination(4, 10).Results) != 0 {
|
||||||
|
t.Errorf("expected to have 0 results, because given a page size of 10, page 4 should have 0 elements")
|
||||||
|
}
|
||||||
|
if len(serviceStatus.WithResultPagination(1, 50).Results) != 25 {
|
||||||
|
t.Errorf("expected to have 25 results, because there's only 25 results")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,7 +2,6 @@ package memory
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
"github.com/TwinProduction/gatus/util"
|
"github.com/TwinProduction/gatus/util"
|
||||||
@ -37,9 +36,15 @@ func NewStore(file string) (*Store, error) {
|
|||||||
return store, nil
|
return store, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllAsJSON returns the JSON encoding of all monitored core.ServiceStatus
|
// GetAllServiceStatusesWithResultPagination returns all monitored core.ServiceStatus
|
||||||
func (s *Store) GetAllAsJSON() ([]byte, error) {
|
// with a subset of core.Result defined by the page and pageSize parameters
|
||||||
return json.Marshal(s.cache.GetAll())
|
func (s *Store) GetAllServiceStatusesWithResultPagination(page, pageSize int) map[string]*core.ServiceStatus {
|
||||||
|
serviceStatuses := s.cache.GetAll()
|
||||||
|
pagedServiceStatuses := make(map[string]*core.ServiceStatus, len(serviceStatuses))
|
||||||
|
for k, v := range serviceStatuses {
|
||||||
|
pagedServiceStatuses[k] = v.(*core.ServiceStatus).WithResultPagination(page, pageSize)
|
||||||
|
}
|
||||||
|
return pagedServiceStatuses
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
@ -53,7 +58,7 @@ func (s *Store) GetServiceStatusByKey(key string) *core.ServiceStatus {
|
|||||||
if serviceStatus == nil {
|
if serviceStatus == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return serviceStatus.(*core.ServiceStatus)
|
return serviceStatus.(*core.ServiceStatus).ShallowCopy()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert adds the observed result for the specified service into the store
|
// Insert adds the observed result for the specified service into the store
|
||||||
|
@ -204,7 +204,7 @@ func TestStore_GetServiceStatusByKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStore_GetAllAsJSON(t *testing.T) {
|
func TestStore_GetAllServiceStatusesWithResultPagination(t *testing.T) {
|
||||||
store, _ := NewStore("")
|
store, _ := NewStore("")
|
||||||
firstResult := &testSuccessfulResult
|
firstResult := &testSuccessfulResult
|
||||||
secondResult := &testUnsuccessfulResult
|
secondResult := &testUnsuccessfulResult
|
||||||
@ -213,13 +213,19 @@ func TestStore_GetAllAsJSON(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{}
|
||||||
output, err := store.GetAllAsJSON()
|
serviceStatuses := store.GetAllServiceStatusesWithResultPagination(1, 20)
|
||||||
if err != nil {
|
if len(serviceStatuses) != 1 {
|
||||||
t.Fatal("shouldn't have returned an error, got", err.Error())
|
t.Fatal("expected 1 service status")
|
||||||
}
|
}
|
||||||
expectedOutput := `{"group_name":{"name":"name","group":"group","key":"group_name","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"}]}}`
|
actual, exists := serviceStatuses[util.ConvertGroupAndServiceToKey(testService.Group, testService.Name)]
|
||||||
if string(output) != expectedOutput {
|
if !exists {
|
||||||
t.Errorf("expected:\n %s\n\ngot:\n %s", expectedOutput, string(output))
|
t.Fatal("expected service status to exist")
|
||||||
|
}
|
||||||
|
if len(actual.Results) != 2 {
|
||||||
|
t.Error("expected 2 results, got", len(actual.Results))
|
||||||
|
}
|
||||||
|
if len(actual.Events) != 2 {
|
||||||
|
t.Error("expected 2 events, got", len(actual.Events))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,8 @@ import (
|
|||||||
// Store is the interface that each stores should implement
|
// Store is the interface that each stores should implement
|
||||||
type Store interface {
|
type Store interface {
|
||||||
// GetAllAsJSON returns the JSON encoding of all monitored core.ServiceStatus
|
// GetAllAsJSON returns the JSON encoding of all monitored core.ServiceStatus
|
||||||
GetAllAsJSON() ([]byte, error)
|
// with a subset of core.Result defined by the page and pageSize parameters
|
||||||
|
GetAllServiceStatusesWithResultPagination(page, pageSize int) map[string]*core.ServiceStatus
|
||||||
|
|
||||||
// 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
|
||||||
GetServiceStatus(groupName, serviceName string) *core.ServiceStatus
|
GetServiceStatus(groupName, serviceName string) *core.ServiceStatus
|
||||||
|
@ -102,7 +102,7 @@ func BenchmarkStore_GetAllAsJSON(b *testing.B) {
|
|||||||
scenario.Store.Insert(&testService, &testUnsuccessfulResult)
|
scenario.Store.Insert(&testService, &testUnsuccessfulResult)
|
||||||
b.Run(scenario.Name, func(b *testing.B) {
|
b.Run(scenario.Name, func(b *testing.B) {
|
||||||
for n := 0; n < b.N; n++ {
|
for n := 0; n < b.N; n++ {
|
||||||
scenario.Store.GetAllAsJSON()
|
scenario.Store.GetAllServiceStatusesWithResultPagination(1, 20)
|
||||||
}
|
}
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
})
|
})
|
||||||
|
@ -18,31 +18,12 @@ var (
|
|||||||
monitoringMutex sync.Mutex
|
monitoringMutex sync.Mutex
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetServiceStatusesAsJSON the JSON encoding of all core.ServiceStatus recorded
|
|
||||||
func GetServiceStatusesAsJSON() ([]byte, error) {
|
|
||||||
return storage.Get().GetAllAsJSON()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUptimeByKey returns the uptime of a service based on the ServiceStatus key
|
|
||||||
func GetUptimeByKey(key string) *core.Uptime {
|
|
||||||
serviceStatus := storage.Get().GetServiceStatusByKey(key)
|
|
||||||
if serviceStatus == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return serviceStatus.Uptime
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetServiceStatusByKey returns the uptime of a service based on its ServiceStatus key
|
|
||||||
func GetServiceStatusByKey(key string) *core.ServiceStatus {
|
|
||||||
return storage.Get().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 {
|
||||||
go monitor(service)
|
// To prevent multiple requests from running at the same time, we'll wait for a little bit before each iteration
|
||||||
// To prevent multiple requests from running at the same time
|
|
||||||
time.Sleep(1111 * time.Millisecond)
|
time.Sleep(1111 * time.Millisecond)
|
||||||
|
go monitor(service)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
34
web/app/src/components/Pagination.vue
Normal file
34
web/app/src/components/Pagination.vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mt-2 flex">
|
||||||
|
<div class="flex-1">
|
||||||
|
<button v-if="currentPage < 5" @click="nextPage" class="bg-gray-200 hover:bg-gray-300 px-2 rounded border-gray-300 border text-monospace"><</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 text-right">
|
||||||
|
<button v-if="currentPage > 1" @click="previousPage" class="bg-gray-200 hover:bg-gray-300 px-2 rounded border-gray-300 border text-monospace">></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Pagination',
|
||||||
|
components: {},
|
||||||
|
emits: ['page'],
|
||||||
|
methods: {
|
||||||
|
nextPage() {
|
||||||
|
this.currentPage++;
|
||||||
|
this.$emit('page', this.currentPage);
|
||||||
|
},
|
||||||
|
previousPage() {
|
||||||
|
this.currentPage--;
|
||||||
|
this.$emit('page', this.currentPage);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentPage: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,37 +1,48 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class='service px-3 py-3 border-l border-r border-t rounded-none hover:bg-gray-100' v-if="data && data.results && data.results.length">
|
<div class='service px-3 py-3 border-l border-r border-t rounded-none hover:bg-gray-100' v-if="data">
|
||||||
<div class='flex flex-wrap mb-2'>
|
<div class='flex flex-wrap mb-2'>
|
||||||
<div class='w-3/4'>
|
<div class='w-3/4'>
|
||||||
<router-link :to="generatePath()" class="font-bold hover:text-blue-800 hover:underline" title="View detailed service health">
|
<router-link :to="generatePath()" class="font-bold hover:text-blue-800 hover:underline" title="View detailed service health">
|
||||||
{{ data.name }}
|
{{ data.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<span class='text-gray-500 font-light'> | {{ data.results[data.results.length - 1].hostname }}</span>
|
<span v-if="data.results && data.results.length" 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' v-if="data.results && data.results.length">
|
||||||
{{ (minResponseTime === maxResponseTime ? minResponseTime : (minResponseTime + '-' + maxResponseTime)) }}ms
|
{{ (minResponseTime === maxResponseTime ? minResponseTime : (minResponseTime + '-' + maxResponseTime)) }}ms
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class='status-over-time flex flex-row'>
|
<div class='status-over-time flex flex-row'>
|
||||||
|
<slot v-if="data.results && data.results.length">
|
||||||
<slot v-if="data.results.length < maximumNumberOfResults">
|
<slot v-if="data.results.length < maximumNumberOfResults">
|
||||||
<span v-for="filler in maximumNumberOfResults - data.results.length" :key="filler" class="status rounded border border-dashed"> </span>
|
<span v-for="filler in maximumNumberOfResults - data.results.length" :key="filler" class="status rounded border border-dashed"> </span>
|
||||||
</slot>
|
</slot>
|
||||||
<slot v-for="result in data.results" :key="result">
|
<slot v-for="result in data.results" :key="result">
|
||||||
<span v-if="result.success" class="status status-success rounded bg-success" @mouseenter="showTooltip(result, $event)" @mouseleave="showTooltip(null, $event)"></span>
|
<span v-if="result.success" class="status status-success rounded bg-success" @mouseenter="showTooltip(result, $event)" @mouseleave="showTooltip(null, $event)"></span>
|
||||||
<span v-else class="status status-failure rounded bg-red-600" @mouseenter="showTooltip(result, $event)" @mouseleave="showTooltip(null, $event)"></span>
|
<span v-else class="status status-failure rounded bg-red-600" @mouseenter="showTooltip(result, $event)" @mouseleave="showTooltip(null, $event)"></span>
|
||||||
</slot>
|
</slot>
|
||||||
|
</slot>
|
||||||
|
<slot v-else>
|
||||||
|
<span v-for="filler in maximumNumberOfResults" :key="filler" class="status rounded border border-dashed"> </span>
|
||||||
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class='flex flex-wrap status-time-ago'>
|
<div class='flex flex-wrap status-time-ago'>
|
||||||
<!-- Show "Last update at" instead? -->
|
<slot v-if="data.results && data.results.length">
|
||||||
<div class='w-1/2'>
|
<div class='w-1/2'>
|
||||||
{{ generatePrettyTimeAgo(data.results[0].timestamp) }}
|
{{ generatePrettyTimeAgo(data.results[0].timestamp) }}
|
||||||
</div>
|
</div>
|
||||||
<div class='w-1/2 text-right'>
|
<div class='w-1/2 text-right'>
|
||||||
{{ generatePrettyTimeAgo(data.results[data.results.length - 1].timestamp) }}
|
{{ generatePrettyTimeAgo(data.results[data.results.length - 1].timestamp) }}
|
||||||
</div>
|
</div>
|
||||||
|
</slot>
|
||||||
|
<slot v-else>
|
||||||
|
<div class='w-1/2'>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -153,7 +164,7 @@ export default {
|
|||||||
content: "X";
|
content: "X";
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 450px) {
|
@media screen and (max-width: 600px) {
|
||||||
.status.status-success::after,
|
.status.status-success::after,
|
||||||
.status.status-failure::after {
|
.status.status-failure::after {
|
||||||
content: " ";
|
content: " ";
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
<h1 class="text-xl xl:text-3xl text-monospace text-gray-400">RECENT CHECKS</h1>
|
<h1 class="text-xl xl:text-3xl text-monospace text-gray-400">RECENT CHECKS</h1>
|
||||||
<hr class="mb-4" />
|
<hr class="mb-4" />
|
||||||
<Service :data="serviceStatus" :maximumNumberOfResults="20" @showTooltip="showTooltip" />
|
<Service :data="serviceStatus" :maximumNumberOfResults="20" @showTooltip="showTooltip" />
|
||||||
|
<Pagination @page="changePage"/>
|
||||||
</slot>
|
</slot>
|
||||||
<div v-if="uptime" class="mt-12">
|
<div v-if="uptime" class="mt-12">
|
||||||
<h1 class="text-xl xl:text-3xl text-monospace text-gray-400">UPTIME</h1>
|
<h1 class="text-xl xl:text-3xl text-monospace text-gray-400">UPTIME</h1>
|
||||||
@ -73,10 +74,12 @@ import Settings from '@/components/Settings.vue'
|
|||||||
import Service from '@/components/Service.vue';
|
import Service from '@/components/Service.vue';
|
||||||
import {SERVER_URL} from "@/main.js";
|
import {SERVER_URL} from "@/main.js";
|
||||||
import {helper} from "@/mixins/helper.js";
|
import {helper} from "@/mixins/helper.js";
|
||||||
|
import Pagination from "@/components/Pagination";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Details',
|
name: 'Details',
|
||||||
components: {
|
components: {
|
||||||
|
Pagination,
|
||||||
Service,
|
Service,
|
||||||
Settings,
|
Settings,
|
||||||
},
|
},
|
||||||
@ -85,7 +88,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
fetchData() {
|
fetchData() {
|
||||||
//console.log("[Details][fetchData] Fetching data");
|
//console.log("[Details][fetchData] Fetching data");
|
||||||
fetch(`${this.serverUrl}/api/v1/statuses/${this.$route.params.key}`)
|
fetch(`${this.serverUrl}/api/v1/statuses/${this.$route.params.key}?page=${this.currentPage}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (JSON.stringify(this.serviceStatus) !== JSON.stringify(data)) {
|
if (JSON.stringify(this.serviceStatus) !== JSON.stringify(data)) {
|
||||||
@ -138,7 +141,11 @@ export default {
|
|||||||
},
|
},
|
||||||
showTooltip(result, event) {
|
showTooltip(result, event) {
|
||||||
this.$emit('showTooltip', result, event);
|
this.$emit('showTooltip', result, event);
|
||||||
}
|
},
|
||||||
|
changePage(page) {
|
||||||
|
this.currentPage = page;
|
||||||
|
this.fetchData();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -147,6 +154,7 @@ export default {
|
|||||||
uptime: {"7d": 0, "24h": 0, "1h": 0},
|
uptime: {"7d": 0, "24h": 0, "1h": 0},
|
||||||
// Since this page isn't at the root, we need to modify the server URL a bit
|
// Since this page isn't at the root, we need to modify the server URL a bit
|
||||||
serverUrl: SERVER_URL === '.' ? '..' : SERVER_URL,
|
serverUrl: SERVER_URL === '.' ? '..' : SERVER_URL,
|
||||||
|
currentPage: 1,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<Services :serviceStatuses="serviceStatuses" :showStatusOnHover="true" @showTooltip="showTooltip"/>
|
<Services :serviceStatuses="serviceStatuses" :showStatusOnHover="true" @showTooltip="showTooltip"/>
|
||||||
|
<Pagination @page="changePage"/>
|
||||||
<Settings @refreshData="fetchData"/>
|
<Settings @refreshData="fetchData"/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Settings from '@/components/Settings.vue'
|
import Settings from '@/components/Settings.vue'
|
||||||
import Services from '@/components/Services.vue';
|
import Services from '@/components/Services.vue';
|
||||||
|
import Pagination from "@/components/Pagination";
|
||||||
import {SERVER_URL} from "@/main.js";
|
import {SERVER_URL} from "@/main.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
components: {
|
components: {
|
||||||
|
Pagination,
|
||||||
Services,
|
Services,
|
||||||
Settings,
|
Settings,
|
||||||
},
|
},
|
||||||
@ -18,7 +21,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
fetchData() {
|
fetchData() {
|
||||||
//console.log("[Home][fetchData] Fetching data");
|
//console.log("[Home][fetchData] Fetching data");
|
||||||
fetch(`${SERVER_URL}/api/v1/statuses`)
|
fetch(`${SERVER_URL}/api/v1/statuses?page=${this.currentPage}`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (JSON.stringify(this.serviceStatuses) !== JSON.stringify(data)) {
|
if (JSON.stringify(this.serviceStatuses) !== JSON.stringify(data)) {
|
||||||
@ -28,11 +31,16 @@ export default {
|
|||||||
},
|
},
|
||||||
showTooltip(result, event) {
|
showTooltip(result, event) {
|
||||||
this.$emit('showTooltip', result, event);
|
this.$emit('showTooltip', result, event);
|
||||||
}
|
},
|
||||||
|
changePage(page) {
|
||||||
|
this.currentPage = page;
|
||||||
|
this.fetchData();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
serviceStatuses: {}
|
serviceStatuses: {},
|
||||||
|
currentPage: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user