diff --git a/controller/badge.go b/controller/badge.go index 68c4d63f..6ac1786a 100644 --- a/controller/badge.go +++ b/controller/badge.go @@ -7,7 +7,7 @@ import ( "time" "github.com/TwinProduction/gatus/core" - "github.com/TwinProduction/gatus/watchdog" + "github.com/TwinProduction/gatus/storage" "github.com/gorilla/mux" ) @@ -25,18 +25,23 @@ func badgeHandler(writer http.ResponseWriter, request *http.Request) { } identifier := variables["identifier"] key := strings.TrimSuffix(identifier, ".svg") - uptime := watchdog.GetUptimeByKey(key) - if uptime == nil { + serviceStatus := storage.Get().GetServiceStatusByKey(key) + if serviceStatus == nil { writer.WriteHeader(http.StatusNotFound) _, _ = writer.Write([]byte("Requested service not found")) return } + if serviceStatus.Uptime == nil { + writer.WriteHeader(http.StatusInternalServerError) + _, _ = writer.Write([]byte("Failed to compute uptime")) + return + } formattedDate := time.Now().Format(http.TimeFormat) writer.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") writer.Header().Set("Date", formattedDate) writer.Header().Set("Expires", formattedDate) 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 { diff --git a/controller/controller.go b/controller/controller.go index 039e5645..d2ebb3a6 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -14,7 +14,7 @@ import ( "github.com/TwinProduction/gatus/config" "github.com/TwinProduction/gatus/security" - "github.com/TwinProduction/gatus/watchdog" + "github.com/TwinProduction/gatus/storage" "github.com/TwinProduction/gocache" "github.com/TwinProduction/health" "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. // Must not be wrapped by GzipHandler func serviceStatusesHandler(writer http.ResponseWriter, r *http.Request) { + page, pageSize := extractPageAndPageSizeFromRequest(r) gzipped := strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") var exists bool var value interface{} if gzipped { 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 { - value, exists = cache.Get("service-status") + value, exists = cache.Get(fmt.Sprintf("service-status-%d-%d", page, pageSize)) } var data []byte if !exists { var err error buffer := &bytes.Buffer{} gzipWriter := gzip.NewWriter(buffer) - data, err = watchdog.GetServiceStatusesAsJSON() + data, err = json.Marshal(storage.Get().GetAllServiceStatusesWithResultPagination(page, pageSize)) if err != nil { log.Printf("[controller][serviceStatusesHandler] Unable to marshal object to JSON: %s", err.Error()) writer.WriteHeader(http.StatusInternalServerError) @@ -125,8 +126,8 @@ func serviceStatusesHandler(writer http.ResponseWriter, r *http.Request) { _, _ = gzipWriter.Write(data) _ = gzipWriter.Close() gzippedData := buffer.Bytes() - cache.SetWithTTL("service-status", data, cacheTTL) - cache.SetWithTTL("service-status-gzipped", gzippedData, cacheTTL) + cache.SetWithTTL(fmt.Sprintf("service-status-%d-%d", page, pageSize), data, cacheTTL) + cache.SetWithTTL(fmt.Sprintf("service-status-%d-%d-gzipped", page, pageSize), gzippedData, cacheTTL) if gzipped { 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 func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) { + page, pageSize := extractPageAndPageSizeFromRequest(r) vars := mux.Vars(r) - serviceStatus := watchdog.GetServiceStatusByKey(vars["key"]) + serviceStatus := storage.Get().GetServiceStatusByKey(vars["key"]) if serviceStatus == nil { log.Printf("[controller][serviceStatusHandler] Service with key=%s not found", vars["key"]) writer.WriteHeader(http.StatusNotFound) @@ -149,7 +151,7 @@ func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) { return } 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 // 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 @@ -160,20 +162,10 @@ func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) { 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")) + _, _ = writer.Write([]byte("unable to marshal object to JSON")) return } writer.Header().Add("Content-Type", "application/json") writer.WriteHeader(http.StatusOK) _, _ = 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") -} diff --git a/controller/controller_test.go b/controller/controller_test.go index bd828bdd..cb920c52 100644 --- a/controller/controller_test.go +++ b/controller/controller_test.go @@ -10,10 +10,87 @@ import ( "github.com/TwinProduction/gatus/config" "github.com/TwinProduction/gatus/core" + "github.com/TwinProduction/gatus/storage" "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) { + defer storage.Get().Clear() + defer cache.Clear() staticFolder = "../web/static" cfg := &config.Config{ Metrics: true, @@ -137,6 +214,8 @@ func TestCreateRouter(t *testing.T) { } func TestHandle(t *testing.T) { + defer storage.Get().Clear() + defer cache.Clear() cfg := &config.Config{ Web: &config.WebConfig{ Address: "0.0.0.0", @@ -154,10 +233,12 @@ func TestHandle(t *testing.T) { }, } config.Set(cfg) + defer config.Set(nil) _ = os.Setenv("ROUTER_TEST", "true") _ = os.Setenv("ENVIRONMENT", "dev") defer os.Clearenv() Handle() + defer Shutdown() request, _ := http.NewRequest("GET", "/health", nil) responseRecorder := httptest.NewRecorder() server.Handler.ServeHTTP(responseRecorder, request) @@ -177,3 +258,30 @@ func TestShutdown(t *testing.T) { 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) + } +} diff --git a/controller/favicon.go b/controller/favicon.go new file mode 100644 index 00000000..5d10a8d9 --- /dev/null +++ b/controller/favicon.go @@ -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") +} diff --git a/controller/spa.go b/controller/spa.go new file mode 100644 index 00000000..e44c1112 --- /dev/null +++ b/controller/spa.go @@ -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") +} diff --git a/controller/util.go b/controller/util.go new file mode 100644 index 00000000..ab1669fc --- /dev/null +++ b/controller/util.go @@ -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 +} diff --git a/core/service-status.go b/core/service-status.go index 114be72c..fdadbe35 100644 --- a/core/service-status.go +++ b/core/service-status.go @@ -8,7 +8,7 @@ import ( const ( // 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 = 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 // no more than 20 results in the Results slice func (ss *ServiceStatus) AddResult(result *Result) { diff --git a/core/service-status_test.go b/core/service-status_test.go index 6eee2e73..e1586149 100644 --- a/core/service-status_test.go +++ b/core/service-status_test.go @@ -22,10 +22,45 @@ func TestNewServiceStatus(t *testing.T) { func TestServiceStatus_AddResult(t *testing.T) { service := &Service{Name: "name", Group: "group"} serviceStatus := NewServiceStatus(service) - for i := 0; i < 50; i++ { + for i := 0; i < MaximumNumberOfResults+10; i++ { 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") } } + +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") + } +} diff --git a/storage/store/memory/memory.go b/storage/store/memory/memory.go index bb4f65a1..8baeb529 100644 --- a/storage/store/memory/memory.go +++ b/storage/store/memory/memory.go @@ -2,7 +2,6 @@ package memory import ( "encoding/gob" - "encoding/json" "github.com/TwinProduction/gatus/core" "github.com/TwinProduction/gatus/util" @@ -37,9 +36,15 @@ func NewStore(file string) (*Store, error) { return store, nil } -// GetAllAsJSON returns the JSON encoding of all monitored core.ServiceStatus -func (s *Store) GetAllAsJSON() ([]byte, error) { - return json.Marshal(s.cache.GetAll()) +// GetAllServiceStatusesWithResultPagination returns all monitored core.ServiceStatus +// with a subset of core.Result defined by the page and pageSize parameters +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 @@ -53,7 +58,7 @@ func (s *Store) GetServiceStatusByKey(key string) *core.ServiceStatus { if serviceStatus == nil { return nil } - return serviceStatus.(*core.ServiceStatus) + return serviceStatus.(*core.ServiceStatus).ShallowCopy() } // Insert adds the observed result for the specified service into the store diff --git a/storage/store/memory/memory_test.go b/storage/store/memory/memory_test.go index 20d1a851..d3162f9a 100644 --- a/storage/store/memory/memory_test.go +++ b/storage/store/memory/memory_test.go @@ -204,7 +204,7 @@ func TestStore_GetServiceStatusByKey(t *testing.T) { } } -func TestStore_GetAllAsJSON(t *testing.T) { +func TestStore_GetAllServiceStatusesWithResultPagination(t *testing.T) { store, _ := NewStore("") firstResult := &testSuccessfulResult 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 firstResult.Timestamp = time.Time{} secondResult.Timestamp = time.Time{} - output, err := store.GetAllAsJSON() - if err != nil { - t.Fatal("shouldn't have returned an error, got", err.Error()) + serviceStatuses := store.GetAllServiceStatusesWithResultPagination(1, 20) + if len(serviceStatuses) != 1 { + 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"}]}}` - if string(output) != expectedOutput { - t.Errorf("expected:\n %s\n\ngot:\n %s", expectedOutput, string(output)) + actual, exists := serviceStatuses[util.ConvertGroupAndServiceToKey(testService.Group, testService.Name)] + if !exists { + 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)) } } diff --git a/storage/store/store.go b/storage/store/store.go index 076ddfc2..33269a72 100644 --- a/storage/store/store.go +++ b/storage/store/store.go @@ -8,7 +8,8 @@ import ( // Store is the interface that each stores should implement type Store interface { // 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(groupName, serviceName string) *core.ServiceStatus diff --git a/storage/store/store_bench_test.go b/storage/store/store_bench_test.go index 10fba2b2..e3c34c20 100644 --- a/storage/store/store_bench_test.go +++ b/storage/store/store_bench_test.go @@ -102,7 +102,7 @@ func BenchmarkStore_GetAllAsJSON(b *testing.B) { scenario.Store.Insert(&testService, &testUnsuccessfulResult) b.Run(scenario.Name, func(b *testing.B) { for n := 0; n < b.N; n++ { - scenario.Store.GetAllAsJSON() + scenario.Store.GetAllServiceStatusesWithResultPagination(1, 20) } b.ReportAllocs() }) diff --git a/watchdog/watchdog.go b/watchdog/watchdog.go index 07317e2f..255615e6 100644 --- a/watchdog/watchdog.go +++ b/watchdog/watchdog.go @@ -18,31 +18,12 @@ var ( 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 func Monitor(cfg *config.Config) { for _, service := range cfg.Services { - go monitor(service) - // To prevent multiple requests from running at the same time + // To prevent multiple requests from running at the same time, we'll wait for a little bit before each iteration time.Sleep(1111 * time.Millisecond) + go monitor(service) } } diff --git a/web/app/src/components/Pagination.vue b/web/app/src/components/Pagination.vue new file mode 100644 index 00000000..6872c7ad --- /dev/null +++ b/web/app/src/components/Pagination.vue @@ -0,0 +1,34 @@ + + + + \ No newline at end of file diff --git a/web/app/src/components/Service.vue b/web/app/src/components/Service.vue index 9833367d..c3cba1c5 100644 --- a/web/app/src/components/Service.vue +++ b/web/app/src/components/Service.vue @@ -1,37 +1,48 @@ @@ -153,7 +164,7 @@ export default { content: "X"; } -@media screen and (max-width: 450px) { +@media screen and (max-width: 600px) { .status.status-success::after, .status.status-failure::after { content: " "; diff --git a/web/app/src/views/Details.vue b/web/app/src/views/Details.vue index c1d34f08..553e393e 100644 --- a/web/app/src/views/Details.vue +++ b/web/app/src/views/Details.vue @@ -7,6 +7,7 @@

RECENT CHECKS


+

UPTIME

@@ -73,10 +74,12 @@ import Settings from '@/components/Settings.vue' import Service from '@/components/Service.vue'; import {SERVER_URL} from "@/main.js"; import {helper} from "@/mixins/helper.js"; +import Pagination from "@/components/Pagination"; export default { name: 'Details', components: { + Pagination, Service, Settings, }, @@ -85,7 +88,7 @@ export default { methods: { fetchData() { //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(data => { if (JSON.stringify(this.serviceStatus) !== JSON.stringify(data)) { @@ -138,7 +141,11 @@ export default { }, showTooltip(result, event) { this.$emit('showTooltip', result, event); - } + }, + changePage(page) { + this.currentPage = page; + this.fetchData(); + }, }, data() { return { @@ -147,6 +154,7 @@ export default { uptime: {"7d": 0, "24h": 0, "1h": 0}, // Since this page isn't at the root, we need to modify the server URL a bit serverUrl: SERVER_URL === '.' ? '..' : SERVER_URL, + currentPage: 1, } }, created() { diff --git a/web/app/src/views/Home.vue b/web/app/src/views/Home.vue index 5a1baaf7..7b6f5d8e 100644 --- a/web/app/src/views/Home.vue +++ b/web/app/src/views/Home.vue @@ -1,16 +1,19 @@