mirror of
https://github.com/TwiN/gatus.git
synced 2024-11-25 01:13:40 +01:00
Add response time chart
This commit is contained in:
parent
733760dc06
commit
6942f0f8e0
@ -145,9 +145,10 @@ func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) {
|
|||||||
_, _ = writer.Write([]byte("not found"))
|
_, _ = writer.Write([]byte("not found"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
uptime7Days, _ := storage.Get().GetUptimeByKey(vars["key"], time.Now().Add(-time.Hour*24*7), time.Now())
|
uptime7Days, _ := storage.Get().GetUptimeByKey(vars["key"], time.Now().Add(-24*7*time.Hour), time.Now())
|
||||||
uptime24Hours, _ := storage.Get().GetUptimeByKey(vars["key"], time.Now().Add(-time.Hour*24), time.Now())
|
uptime24Hours, _ := storage.Get().GetUptimeByKey(vars["key"], time.Now().Add(-24*time.Hour), time.Now())
|
||||||
uptime1Hour, _ := storage.Get().GetUptimeByKey(vars["key"], time.Now().Add(-time.Hour), time.Now())
|
uptime1Hour, _ := storage.Get().GetUptimeByKey(vars["key"], time.Now().Add(-time.Hour), time.Now())
|
||||||
|
hourlyAverageResponseTime, _ := storage.Get().GetHourlyAverageResponseTimeByKey(vars["key"], time.Now().Add(-24*time.Hour), time.Now())
|
||||||
data := map[string]interface{}{
|
data := map[string]interface{}{
|
||||||
"serviceStatus": serviceStatus,
|
"serviceStatus": serviceStatus,
|
||||||
// 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
|
||||||
@ -160,6 +161,7 @@ func serviceStatusHandler(writer http.ResponseWriter, r *http.Request) {
|
|||||||
"24h": uptime24Hours,
|
"24h": uptime24Hours,
|
||||||
"1h": uptime1Hour,
|
"1h": uptime1Hour,
|
||||||
},
|
},
|
||||||
|
"hourlyAverageResponseTime": hourlyAverageResponseTime,
|
||||||
}
|
}
|
||||||
output, err := json.Marshal(data)
|
output, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -99,6 +99,30 @@ func (s *Store) GetUptimeByKey(key string, from, to time.Time) (float64, error)
|
|||||||
return float64(successfulExecutions) / float64(totalExecutions), nil
|
return float64(successfulExecutions) / float64(totalExecutions), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetHourlyAverageResponseTimeByKey returns a map of hourly (key) average response time in milliseconds (value) during a time range
|
||||||
|
func (s *Store) GetHourlyAverageResponseTimeByKey(key string, from, to time.Time) (map[int64]int, error) {
|
||||||
|
if from.After(to) {
|
||||||
|
return nil, common.ErrInvalidTimeRange
|
||||||
|
}
|
||||||
|
serviceStatus := s.cache.GetValue(key)
|
||||||
|
if serviceStatus == nil || serviceStatus.(*core.ServiceStatus).Uptime == nil {
|
||||||
|
return nil, common.ErrServiceNotFound
|
||||||
|
}
|
||||||
|
hourlyAverageResponseTimes := make(map[int64]int)
|
||||||
|
current := from
|
||||||
|
for to.Sub(current) >= 0 {
|
||||||
|
hourlyUnixTimestamp := current.Truncate(time.Hour).Unix()
|
||||||
|
hourlyStats := serviceStatus.(*core.ServiceStatus).Uptime.HourlyStatistics[hourlyUnixTimestamp]
|
||||||
|
if hourlyStats == nil || hourlyStats.TotalExecutions == 0 {
|
||||||
|
current = current.Add(time.Hour)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hourlyAverageResponseTimes[hourlyUnixTimestamp] = int(float64(hourlyStats.TotalExecutionsResponseTime) / float64(hourlyStats.TotalExecutions))
|
||||||
|
current = current.Add(time.Hour)
|
||||||
|
}
|
||||||
|
return hourlyAverageResponseTimes, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Insert adds the observed result for the specified service into the store
|
// Insert adds the observed result for the specified service into the store
|
||||||
func (s *Store) Insert(service *core.Service, result *core.Result) {
|
func (s *Store) Insert(service *core.Service, result *core.Result) {
|
||||||
key := service.Key()
|
key := service.Key()
|
||||||
|
@ -13,7 +13,7 @@ var (
|
|||||||
secondCondition = core.Condition("[RESPONSE_TIME] < 500")
|
secondCondition = core.Condition("[RESPONSE_TIME] < 500")
|
||||||
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
thirdCondition = core.Condition("[CERTIFICATE_EXPIRATION] < 72h")
|
||||||
|
|
||||||
timestamp = time.Now()
|
now = time.Now()
|
||||||
|
|
||||||
testService = core.Service{
|
testService = core.Service{
|
||||||
Name: "name",
|
Name: "name",
|
||||||
@ -35,7 +35,7 @@ var (
|
|||||||
Errors: nil,
|
Errors: nil,
|
||||||
Connected: true,
|
Connected: true,
|
||||||
Success: true,
|
Success: true,
|
||||||
Timestamp: timestamp,
|
Timestamp: now,
|
||||||
Duration: 150 * time.Millisecond,
|
Duration: 150 * time.Millisecond,
|
||||||
CertificateExpiration: 10 * time.Hour,
|
CertificateExpiration: 10 * time.Hour,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*core.ConditionResult{
|
||||||
@ -60,7 +60,7 @@ var (
|
|||||||
Errors: []string{"error-1", "error-2"},
|
Errors: []string{"error-1", "error-2"},
|
||||||
Connected: true,
|
Connected: true,
|
||||||
Success: false,
|
Success: false,
|
||||||
Timestamp: timestamp,
|
Timestamp: now,
|
||||||
Duration: 750 * time.Millisecond,
|
Duration: 750 * time.Millisecond,
|
||||||
CertificateExpiration: 10 * time.Hour,
|
CertificateExpiration: 10 * time.Hour,
|
||||||
ConditionResults: []*core.ConditionResult{
|
ConditionResults: []*core.ConditionResult{
|
||||||
@ -84,6 +84,7 @@ var (
|
|||||||
// This test is simply an extra sanity check
|
// This test is simply an extra sanity check
|
||||||
func TestStore_SanityCheck(t *testing.T) {
|
func TestStore_SanityCheck(t *testing.T) {
|
||||||
store, _ := NewStore("")
|
store, _ := NewStore("")
|
||||||
|
defer store.Close()
|
||||||
store.Insert(&testService, &testSuccessfulResult)
|
store.Insert(&testService, &testSuccessfulResult)
|
||||||
if numberOfServiceStatuses := len(store.GetAllServiceStatuses(paging.NewServiceStatusParams())); numberOfServiceStatuses != 1 {
|
if numberOfServiceStatuses := len(store.GetAllServiceStatuses(paging.NewServiceStatusParams())); numberOfServiceStatuses != 1 {
|
||||||
t.Fatalf("expected 1 ServiceStatus, got %d", numberOfServiceStatuses)
|
t.Fatalf("expected 1 ServiceStatus, got %d", numberOfServiceStatuses)
|
||||||
@ -93,6 +94,11 @@ func TestStore_SanityCheck(t *testing.T) {
|
|||||||
if numberOfServiceStatuses := len(store.GetAllServiceStatuses(paging.NewServiceStatusParams())); numberOfServiceStatuses != 1 {
|
if numberOfServiceStatuses := len(store.GetAllServiceStatuses(paging.NewServiceStatusParams())); numberOfServiceStatuses != 1 {
|
||||||
t.Fatalf("expected 1 ServiceStatus, got %d", numberOfServiceStatuses)
|
t.Fatalf("expected 1 ServiceStatus, got %d", numberOfServiceStatuses)
|
||||||
}
|
}
|
||||||
|
if hourlyAverageResponseTime, err := store.GetHourlyAverageResponseTimeByKey(testService.Key(), time.Now().Add(-24*time.Hour), time.Now()); err != nil {
|
||||||
|
t.Errorf("expected no error, got %v", err)
|
||||||
|
} else if len(hourlyAverageResponseTime) != 1 {
|
||||||
|
t.Errorf("expected 1 hour to have had a result in the past 24 hours, got %d", len(hourlyAverageResponseTime))
|
||||||
|
}
|
||||||
ss := store.GetServiceStatus(testService.Group, testService.Name, paging.NewServiceStatusParams().WithResults(1, 20).WithEvents(1, 20))
|
ss := store.GetServiceStatus(testService.Group, testService.Name, paging.NewServiceStatusParams().WithResults(1, 20).WithEvents(1, 20))
|
||||||
if ss == nil {
|
if ss == nil {
|
||||||
t.Fatalf("Store should've had key '%s', but didn't", testService.Key())
|
t.Fatalf("Store should've had key '%s', but didn't", testService.Key())
|
||||||
@ -123,6 +129,8 @@ func TestStore_Save(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("expected no error, got", err.Error())
|
t.Fatal("expected no error, got", err.Error())
|
||||||
}
|
}
|
||||||
|
store.Clear()
|
||||||
|
store.Close()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,6 +219,31 @@ func (s *Store) GetUptimeByKey(key string, from, to time.Time) (float64, error)
|
|||||||
return uptime, nil
|
return uptime, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetHourlyAverageResponseTimeByKey returns a map of hourly (key) average response time in milliseconds (value) during a time range
|
||||||
|
func (s *Store) GetHourlyAverageResponseTimeByKey(key string, from, to time.Time) (map[int64]int, error) {
|
||||||
|
if from.After(to) {
|
||||||
|
return nil, common.ErrInvalidTimeRange
|
||||||
|
}
|
||||||
|
tx, err := s.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
serviceID, _, _, err := s.getServiceIDGroupAndNameByKey(tx, key)
|
||||||
|
if err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
hourlyAverageResponseTimes, err := s.getServiceHourlyAverageResponseTimes(tx, serviceID, from, to)
|
||||||
|
if err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}
|
||||||
|
return hourlyAverageResponseTimes, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Insert adds the observed result for the specified service into the store
|
// Insert adds the observed result for the specified service into the store
|
||||||
func (s *Store) Insert(service *core.Service, result *core.Result) {
|
func (s *Store) Insert(service *core.Service, result *core.Result) {
|
||||||
tx, err := s.db.Begin()
|
tx, err := s.db.Begin()
|
||||||
@ -653,6 +678,34 @@ func (s *Store) getServiceUptime(tx *sql.Tx, serviceID int64, from, to time.Time
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) getServiceHourlyAverageResponseTimes(tx *sql.Tx, serviceID int64, from, to time.Time) (map[int64]int, error) {
|
||||||
|
rows, err := tx.Query(
|
||||||
|
`
|
||||||
|
SELECT hour_unix_timestamp, total_executions, total_response_time
|
||||||
|
FROM service_uptime
|
||||||
|
WHERE service_id = $1
|
||||||
|
AND total_executions > 0
|
||||||
|
AND hour_unix_timestamp >= $2
|
||||||
|
AND hour_unix_timestamp <= $3
|
||||||
|
`,
|
||||||
|
serviceID,
|
||||||
|
from.Unix(),
|
||||||
|
to.Unix(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var totalExecutions, totalResponseTime int
|
||||||
|
var unixTimestampFlooredAtHour int64
|
||||||
|
hourlyAverageResponseTimes := make(map[int64]int)
|
||||||
|
for rows.Next() {
|
||||||
|
_ = rows.Scan(&unixTimestampFlooredAtHour, &totalExecutions, &totalResponseTime)
|
||||||
|
hourlyAverageResponseTimes[unixTimestampFlooredAtHour] = int(float64(totalResponseTime) / float64(totalExecutions))
|
||||||
|
}
|
||||||
|
_ = rows.Close()
|
||||||
|
return hourlyAverageResponseTimes, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) getServiceID(tx *sql.Tx, service *core.Service) (int64, error) {
|
func (s *Store) getServiceID(tx *sql.Tx, service *core.Service) (int64, error) {
|
||||||
rows, err := tx.Query("SELECT service_id FROM service WHERE service_key = $1", service.Key())
|
rows, err := tx.Query("SELECT service_id FROM service WHERE service_key = $1", service.Key())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -274,6 +274,11 @@ func TestStore_SanityCheck(t *testing.T) {
|
|||||||
if numberOfServiceStatuses := len(store.GetAllServiceStatuses(paging.NewServiceStatusParams())); numberOfServiceStatuses != 1 {
|
if numberOfServiceStatuses := len(store.GetAllServiceStatuses(paging.NewServiceStatusParams())); numberOfServiceStatuses != 1 {
|
||||||
t.Fatalf("expected 1 ServiceStatus, got %d", numberOfServiceStatuses)
|
t.Fatalf("expected 1 ServiceStatus, got %d", numberOfServiceStatuses)
|
||||||
}
|
}
|
||||||
|
if hourlyAverageResponseTime, err := store.GetHourlyAverageResponseTimeByKey(testService.Key(), time.Now().Add(-24*time.Hour), time.Now()); err != nil {
|
||||||
|
t.Errorf("expected no error, got %v", err)
|
||||||
|
} else if len(hourlyAverageResponseTime) != 1 {
|
||||||
|
t.Errorf("expected 1 hour to have had a result in the past 24 hours, got %d", len(hourlyAverageResponseTime))
|
||||||
|
}
|
||||||
ss := store.GetServiceStatus(testService.Group, testService.Name, paging.NewServiceStatusParams().WithResults(1, 20).WithEvents(1, 20))
|
ss := store.GetServiceStatus(testService.Group, testService.Name, paging.NewServiceStatusParams().WithResults(1, 20).WithEvents(1, 20))
|
||||||
if ss == nil {
|
if ss == nil {
|
||||||
t.Fatalf("Store should've had key '%s', but didn't", testService.Key())
|
t.Fatalf("Store should've had key '%s', but didn't", testService.Key())
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
package store
|
package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/TwinProduction/gatus/core"
|
"github.com/TwinProduction/gatus/core"
|
||||||
"github.com/TwinProduction/gatus/storage/store/common/paging"
|
"github.com/TwinProduction/gatus/storage/store/common/paging"
|
||||||
"github.com/TwinProduction/gatus/storage/store/memory"
|
"github.com/TwinProduction/gatus/storage/store/memory"
|
||||||
"github.com/TwinProduction/gatus/storage/store/sqlite"
|
"github.com/TwinProduction/gatus/storage/store/sqlite"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Store is the interface that each stores should implement
|
// Store is the interface that each stores should implement
|
||||||
@ -23,6 +24,9 @@ type Store interface {
|
|||||||
// GetUptimeByKey returns the uptime percentage during a time range
|
// GetUptimeByKey returns the uptime percentage during a time range
|
||||||
GetUptimeByKey(key string, from, to time.Time) (float64, error)
|
GetUptimeByKey(key string, from, to time.Time) (float64, error)
|
||||||
|
|
||||||
|
// GetHourlyAverageResponseTimeByKey returns a map of hourly (key) average response time in milliseconds (value) during a time range
|
||||||
|
GetHourlyAverageResponseTimeByKey(key string, from, to time.Time) (map[int64]int, error)
|
||||||
|
|
||||||
// Insert adds the observed result for the specified service into the store
|
// Insert adds the observed result for the specified service into the store
|
||||||
Insert(service *core.Service, result *core.Result)
|
Insert(service *core.Service, result *core.Result)
|
||||||
|
|
||||||
|
@ -292,6 +292,45 @@ func TestStore_GetUptimeByKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStore_GetHourlyAverageResponseTimeByKey(t *testing.T) {
|
||||||
|
scenarios := initStoresAndBaseScenarios(t, "TestStore_GetHourlyAverageResponseTimeByKey")
|
||||||
|
defer cleanUp(scenarios)
|
||||||
|
firstResult := testSuccessfulResult
|
||||||
|
firstResult.Timestamp = now.Add(-(2 * time.Hour))
|
||||||
|
firstResult.Duration = 300 * time.Millisecond
|
||||||
|
secondResult := testSuccessfulResult
|
||||||
|
secondResult.Duration = 150 * time.Millisecond
|
||||||
|
secondResult.Timestamp = now.Add(-(1*time.Hour + 30*time.Minute))
|
||||||
|
thirdResult := testUnsuccessfulResult
|
||||||
|
thirdResult.Duration = 200 * time.Millisecond
|
||||||
|
thirdResult.Timestamp = now.Add(-(1 * time.Hour))
|
||||||
|
fourthResult := testSuccessfulResult
|
||||||
|
fourthResult.Duration = 500 * time.Millisecond
|
||||||
|
fourthResult.Timestamp = now
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
scenario.Store.Insert(&testService, &firstResult)
|
||||||
|
scenario.Store.Insert(&testService, &secondResult)
|
||||||
|
scenario.Store.Insert(&testService, &thirdResult)
|
||||||
|
scenario.Store.Insert(&testService, &fourthResult)
|
||||||
|
hourlyAverageResponseTime, err := scenario.Store.GetHourlyAverageResponseTimeByKey(testService.Key(), now.Add(-24*time.Hour), now)
|
||||||
|
if err != nil {
|
||||||
|
t.Error("shouldn't have returned an error, got", err)
|
||||||
|
}
|
||||||
|
if key := now.Truncate(time.Hour).Unix(); hourlyAverageResponseTime[key] != 500 {
|
||||||
|
t.Errorf("expected average response time to be 500ms at %d, got %v", key, hourlyAverageResponseTime[key])
|
||||||
|
}
|
||||||
|
if key := now.Truncate(time.Hour).Add(-time.Hour).Unix(); hourlyAverageResponseTime[key] != 175 {
|
||||||
|
t.Errorf("expected average response time to be 175ms at %d, got %v", key, hourlyAverageResponseTime[key])
|
||||||
|
}
|
||||||
|
if key := now.Truncate(time.Hour).Add(-2 * time.Hour).Unix(); hourlyAverageResponseTime[key] != 300 {
|
||||||
|
t.Errorf("expected average response time to be 300ms at %d, got %v", key, hourlyAverageResponseTime[key])
|
||||||
|
}
|
||||||
|
scenario.Store.Clear()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStore_Insert(t *testing.T) {
|
func TestStore_Insert(t *testing.T) {
|
||||||
scenarios := initStoresAndBaseScenarios(t, "TestStore_Insert")
|
scenarios := initStoresAndBaseScenarios(t, "TestStore_Insert")
|
||||||
defer cleanUp(scenarios)
|
defer cleanUp(scenarios)
|
||||||
|
222
web/app/package-lock.json
generated
222
web/app/package-lock.json
generated
@ -8,9 +8,12 @@
|
|||||||
"name": "gatus",
|
"name": "gatus",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"chart.js": "^3.5.0",
|
||||||
"core-js": "^3.16.1",
|
"core-js": "^3.16.1",
|
||||||
"vue": "^3.2.2",
|
"vue": "^3.2.2",
|
||||||
"vue-router": "^4.0.11"
|
"vue-chart-3": "^0.5.7",
|
||||||
|
"vue-router": "^4.0.11",
|
||||||
|
"vue3-chart-v2": "^0.8.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^5.0.0-beta.0",
|
"@vue/cli-plugin-babel": "^5.0.0-beta.0",
|
||||||
@ -1803,6 +1806,14 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/chart.js": {
|
||||||
|
"version": "2.9.34",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.34.tgz",
|
||||||
|
"integrity": "sha512-CtZVk+kh1IN67dv+fB0CWmCLCRrDJgqOj15qPic2B1VCMovNO6B7Vhf/TgPpNscjhAL1j+qUntDMWb9A4ZmPTg==",
|
||||||
|
"dependencies": {
|
||||||
|
"moment": "^2.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/connect": {
|
"node_modules/@types/connect": {
|
||||||
"version": "3.4.34",
|
"version": "3.4.34",
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
|
||||||
@ -3907,6 +3918,28 @@
|
|||||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/chart.js": {
|
||||||
|
"version": "3.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.0.tgz",
|
||||||
|
"integrity": "sha512-J1a4EAb1Gi/KbhwDRmoovHTRuqT8qdF0kZ4XgwxpGethJHUdDrkqyPYwke0a+BuvSeUxPf8Cos6AX2AB8H8GLA=="
|
||||||
|
},
|
||||||
|
"node_modules/chartjs-color": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==",
|
||||||
|
"dependencies": {
|
||||||
|
"chartjs-color-string": "^0.6.0",
|
||||||
|
"color-convert": "^1.9.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chartjs-color-string": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.5.2",
|
"version": "3.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
|
||||||
@ -4222,7 +4255,6 @@
|
|||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color-name": "1.1.3"
|
"color-name": "1.1.3"
|
||||||
}
|
}
|
||||||
@ -4230,8 +4262,7 @@
|
|||||||
"node_modules/color-name": {
|
"node_modules/color-name": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
|
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/color-string": {
|
"node_modules/color-string": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
@ -8158,8 +8189,7 @@
|
|||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.camelcase": {
|
"node_modules/lodash.camelcase": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
@ -8694,6 +8724,14 @@
|
|||||||
"integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==",
|
"integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/moment": {
|
||||||
|
"version": "2.29.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||||
|
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
@ -8747,7 +8785,6 @@
|
|||||||
"version": "3.1.23",
|
"version": "3.1.23",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
|
||||||
"integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==",
|
"integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==",
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"nanoid": "bin/nanoid.cjs"
|
"nanoid": "bin/nanoid.cjs"
|
||||||
},
|
},
|
||||||
@ -13057,6 +13094,51 @@
|
|||||||
"@vue/shared": "3.2.2"
|
"@vue/shared": "3.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-chart-3": {
|
||||||
|
"version": "0.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-chart-3/-/vue-chart-3-0.5.7.tgz",
|
||||||
|
"integrity": "sha512-BccfPv2rodY6IOppYHvMluVmIJE1CHfp5uW2DXrHrm1kIzaafLwpQ5SwdrxuCevn/QhKoi7azzcxwRcoWbX9hg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/runtime-core": "latest",
|
||||||
|
"@vue/runtime-dom": "latest",
|
||||||
|
"csstype": "latest",
|
||||||
|
"lodash": "latest",
|
||||||
|
"nanoid": "latest",
|
||||||
|
"vue-demi": "^0.10.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.4",
|
||||||
|
"chart.js": "^3.1.0",
|
||||||
|
"vue": "^2.0.0 || >=3.0.0-rc.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue-chart-3/node_modules/vue-demi": {
|
||||||
|
"version": "0.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.10.1.tgz",
|
||||||
|
"integrity": "sha512-L6Oi+BvmMv6YXvqv5rJNCFHEKSVu7llpWWJczqmAQYOdmPPw5PNYoz1KKS//Fxhi+4QP64dsPjtmvnYGo1jemA==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"bin": {
|
||||||
|
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||||
|
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^2.6.0 || >=3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-eslint-parser": {
|
"node_modules/vue-eslint-parser": {
|
||||||
"version": "7.10.0",
|
"version": "7.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.10.0.tgz",
|
||||||
@ -13250,6 +13332,38 @@
|
|||||||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/vue3-chart-v2": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue3-chart-v2/-/vue3-chart-v2-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-J+v3Q0ayYyWstPY1zOmdx6l/wkHT63Kzrp5X5PNNXrSUoJT8p608danWIJtncpWhuJB8qQ3t2/jWsg4WF8qJjg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/chart.js": "^2.9.29",
|
||||||
|
"chart.js": "^2.9.4",
|
||||||
|
"core-js": "^3.6.5",
|
||||||
|
"prettier": "^2.2.1",
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue3-chart-v2/node_modules/chart.js": {
|
||||||
|
"version": "2.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz",
|
||||||
|
"integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==",
|
||||||
|
"dependencies": {
|
||||||
|
"chartjs-color": "^2.1.0",
|
||||||
|
"moment": "^2.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue3-chart-v2/node_modules/prettier": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin-prettier.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/watchpack": {
|
"node_modules/watchpack": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.1.1.tgz",
|
||||||
@ -15883,6 +15997,14 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/chart.js": {
|
||||||
|
"version": "2.9.34",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.34.tgz",
|
||||||
|
"integrity": "sha512-CtZVk+kh1IN67dv+fB0CWmCLCRrDJgqOj15qPic2B1VCMovNO6B7Vhf/TgPpNscjhAL1j+qUntDMWb9A4ZmPTg==",
|
||||||
|
"requires": {
|
||||||
|
"moment": "^2.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/connect": {
|
"@types/connect": {
|
||||||
"version": "3.4.34",
|
"version": "3.4.34",
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
|
||||||
@ -17586,6 +17708,28 @@
|
|||||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"chart.js": {
|
||||||
|
"version": "3.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.0.tgz",
|
||||||
|
"integrity": "sha512-J1a4EAb1Gi/KbhwDRmoovHTRuqT8qdF0kZ4XgwxpGethJHUdDrkqyPYwke0a+BuvSeUxPf8Cos6AX2AB8H8GLA=="
|
||||||
|
},
|
||||||
|
"chartjs-color": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==",
|
||||||
|
"requires": {
|
||||||
|
"chartjs-color-string": "^0.6.0",
|
||||||
|
"color-convert": "^1.9.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chartjs-color-string": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==",
|
||||||
|
"requires": {
|
||||||
|
"color-name": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"chokidar": {
|
"chokidar": {
|
||||||
"version": "3.5.2",
|
"version": "3.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
|
||||||
@ -17826,7 +17970,6 @@
|
|||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"color-name": "1.1.3"
|
"color-name": "1.1.3"
|
||||||
}
|
}
|
||||||
@ -17834,8 +17977,7 @@
|
|||||||
"color-name": {
|
"color-name": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
|
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"color-string": {
|
"color-string": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
@ -20855,8 +20997,7 @@
|
|||||||
"lodash": {
|
"lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"lodash.camelcase": {
|
"lodash.camelcase": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
@ -21281,6 +21422,11 @@
|
|||||||
"integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==",
|
"integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"moment": {
|
||||||
|
"version": "2.29.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
|
||||||
|
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
|
||||||
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
@ -21330,8 +21476,7 @@
|
|||||||
"nanoid": {
|
"nanoid": {
|
||||||
"version": "3.1.23",
|
"version": "3.1.23",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
|
||||||
"integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==",
|
"integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"nanomatch": {
|
"nanomatch": {
|
||||||
"version": "1.2.13",
|
"version": "1.2.13",
|
||||||
@ -24604,6 +24749,27 @@
|
|||||||
"@vue/shared": "3.2.2"
|
"@vue/shared": "3.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"vue-chart-3": {
|
||||||
|
"version": "0.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-chart-3/-/vue-chart-3-0.5.7.tgz",
|
||||||
|
"integrity": "sha512-BccfPv2rodY6IOppYHvMluVmIJE1CHfp5uW2DXrHrm1kIzaafLwpQ5SwdrxuCevn/QhKoi7azzcxwRcoWbX9hg==",
|
||||||
|
"requires": {
|
||||||
|
"@vue/runtime-core": "latest",
|
||||||
|
"@vue/runtime-dom": "latest",
|
||||||
|
"csstype": "latest",
|
||||||
|
"lodash": "latest",
|
||||||
|
"nanoid": "latest",
|
||||||
|
"vue-demi": "^0.10.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue-demi": {
|
||||||
|
"version": "0.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.10.1.tgz",
|
||||||
|
"integrity": "sha512-L6Oi+BvmMv6YXvqv5rJNCFHEKSVu7llpWWJczqmAQYOdmPPw5PNYoz1KKS//Fxhi+4QP64dsPjtmvnYGo1jemA==",
|
||||||
|
"requires": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"vue-eslint-parser": {
|
"vue-eslint-parser": {
|
||||||
"version": "7.10.0",
|
"version": "7.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.10.0.tgz",
|
||||||
@ -24749,6 +24915,34 @@
|
|||||||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"vue3-chart-v2": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue3-chart-v2/-/vue3-chart-v2-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-J+v3Q0ayYyWstPY1zOmdx6l/wkHT63Kzrp5X5PNNXrSUoJT8p608danWIJtncpWhuJB8qQ3t2/jWsg4WF8qJjg==",
|
||||||
|
"requires": {
|
||||||
|
"@types/chart.js": "^2.9.29",
|
||||||
|
"chart.js": "^2.9.4",
|
||||||
|
"core-js": "^3.6.5",
|
||||||
|
"prettier": "^2.2.1",
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"chart.js": {
|
||||||
|
"version": "2.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz",
|
||||||
|
"integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==",
|
||||||
|
"requires": {
|
||||||
|
"chartjs-color": "^2.1.0",
|
||||||
|
"moment": "^2.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"watchpack": {
|
"watchpack": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.1.1.tgz",
|
||||||
|
@ -8,9 +8,11 @@
|
|||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"chart.js": "^3.5.0",
|
||||||
"core-js": "^3.16.1",
|
"core-js": "^3.16.1",
|
||||||
"vue": "^3.2.2",
|
"vue": "^3.2.2",
|
||||||
"vue-router": "^4.0.11"
|
"vue-router": "^4.0.11",
|
||||||
|
"vue3-chart-v2": "^0.8.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^5.0.0-beta.0",
|
"@vue/cli-plugin-babel": "^5.0.0-beta.0",
|
||||||
|
58
web/app/src/components/Chart.vue
Normal file
58
web/app/src/components/Chart.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<script>
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { Line } from 'vue3-chart-v2'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'Chart',
|
||||||
|
extends: Line,
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
data: function (value) {
|
||||||
|
this.renderChart(value, this.options)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [{
|
||||||
|
type: 'time',
|
||||||
|
time: {
|
||||||
|
stepSize: 60,
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
yAxes: [{
|
||||||
|
ticks: {
|
||||||
|
min: 0,
|
||||||
|
beginAtZero: true
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
padding: 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
canvas {
|
||||||
|
max-height: 300px !important;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,47 +1,72 @@
|
|||||||
<template>
|
<template>
|
||||||
<router-link to="../" class="absolute top-2 left-2 inline-block px-2 pb-0.5 text-lg text-black bg-gray-100 rounded hover:bg-gray-200 focus:outline-none border border-gray-200 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-600">
|
<router-link to="../"
|
||||||
|
class="absolute top-2 left-2 inline-block px-2 pb-0.5 text-lg text-black bg-gray-100 rounded hover:bg-gray-200 focus:outline-none border border-gray-200 dark:bg-gray-700 dark:text-gray-200 dark:border-gray-500 dark:hover:bg-gray-600">
|
||||||
←
|
←
|
||||||
</router-link>
|
</router-link>
|
||||||
<div>
|
<div>
|
||||||
<slot v-if="serviceStatus">
|
<slot v-if="serviceStatus">
|
||||||
<h1 class="text-xl xl:text-3xl font-mono text-gray-400">RECENT CHECKS</h1>
|
<h1 class="text-xl xl:text-3xl font-mono text-gray-400">RECENT CHECKS</h1>
|
||||||
<hr class="mb-4" />
|
<hr class="mb-4"/>
|
||||||
<Service
|
<Service
|
||||||
:data="serviceStatus"
|
:data="serviceStatus"
|
||||||
:maximumNumberOfResults="20"
|
:maximumNumberOfResults="20"
|
||||||
@showTooltip="showTooltip"
|
@showTooltip="showTooltip"
|
||||||
@toggleShowAverageResponseTime="toggleShowAverageResponseTime" :showAverageResponseTime="showAverageResponseTime"
|
@toggleShowAverageResponseTime="toggleShowAverageResponseTime"
|
||||||
|
:showAverageResponseTime="showAverageResponseTime"
|
||||||
/>
|
/>
|
||||||
<Pagination @page="changePage"/>
|
<Pagination @page="changePage"/>
|
||||||
</slot>
|
</slot>
|
||||||
<div class="mt-12">
|
<div class="mt-12">
|
||||||
<h1 class="text-xl xl:text-3xl font-mono text-gray-400">UPTIME</h1>
|
<h1 class="text-xl xl:text-3xl font-mono text-gray-400">UPTIME</h1>
|
||||||
<hr />
|
<hr/>
|
||||||
<div v-if="serviceStatus && serviceStatus.key" class="flex space-x-4 text-center text-2xl mt-6 relative bottom-2 mb-10">
|
<div v-if="serviceStatus && serviceStatus.key"
|
||||||
|
class="flex space-x-4 text-center text-2xl mt-6 relative bottom-2 mb-10">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h2 class="text-sm text-gray-400 mb-1">Last 7 days</h2>
|
<h2 class="text-sm text-gray-400 mb-1">Last 7 days</h2>
|
||||||
<img :src="generateBadgeImageURL('7d')" alt="7d uptime badge" class="mx-auto" />
|
<img :src="generateBadgeImageURL('7d')" alt="7d uptime badge" class="mx-auto"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h2 class="text-sm text-gray-400 mb-1">Last 24 hours</h2>
|
<h2 class="text-sm text-gray-400 mb-1">Last 24 hours</h2>
|
||||||
<img :src="generateBadgeImageURL('24h')" alt="24h uptime badge" class="mx-auto" />
|
<img :src="generateBadgeImageURL('24h')" alt="24h uptime badge" class="mx-auto"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h2 class="text-sm text-gray-400 mb-1">Last hour</h2>
|
<h2 class="text-sm text-gray-400 mb-1">Last hour</h2>
|
||||||
<img :src="generateBadgeImageURL('1h')" alt="1h uptime badge" class="mx-auto" />
|
<img :src="generateBadgeImageURL('1h')" alt="1h uptime badge" class="mx-auto"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-12">
|
||||||
|
<h1 class="text-xl xl:text-3xl font-mono text-gray-400">RESPONSE TIME</h1>
|
||||||
|
<hr/>
|
||||||
|
<Chart
|
||||||
|
:data="
|
||||||
|
{
|
||||||
|
labels: this.chartLabels,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Average response time (ms)',
|
||||||
|
borderColor: '#28a745',
|
||||||
|
fill: false,
|
||||||
|
tension: 0.05,
|
||||||
|
data: this.chartValues
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-xl xl:text-3xl font-mono text-gray-400 mt-4">EVENTS</h1>
|
<h1 class="text-xl xl:text-3xl font-mono text-gray-400 mt-4">EVENTS</h1>
|
||||||
<hr class="mb-4" />
|
<hr class="mb-4"/>
|
||||||
<div>
|
<div>
|
||||||
<slot v-for="event in events" :key="event">
|
<slot v-for="event in events" :key="event">
|
||||||
<div class="p-3 my-4">
|
<div class="p-3 my-4">
|
||||||
<h2 class="text-lg">
|
<h2 class="text-lg">
|
||||||
<img v-if="event.type === 'HEALTHY'" src="../assets/arrow-up-green.png" alt="Healthy" class="border border-green-600 rounded-full opacity-75 bg-green-100 mr-2 inline" width="26" />
|
<img v-if="event.type === 'HEALTHY'" src="../assets/arrow-up-green.png" alt="Healthy"
|
||||||
<img v-else-if="event.type === 'UNHEALTHY'" src="../assets/arrow-down-red.png" alt="Unhealthy" class="border border-red-500 rounded-full opacity-75 bg-red-100 mr-2 inline" width="26" />
|
class="border border-green-600 rounded-full opacity-75 bg-green-100 mr-2 inline" width="26"/>
|
||||||
<img v-else-if="event.type === 'START'" src="../assets/arrow-right-black.png" alt="Start" class="border border-gray-500 rounded-full opacity-75 bg-gray-100 mr-2 inline" width="26" />
|
<img v-else-if="event.type === 'UNHEALTHY'" src="../assets/arrow-down-red.png" alt="Unhealthy"
|
||||||
|
class="border border-red-500 rounded-full opacity-75 bg-red-100 mr-2 inline" width="26"/>
|
||||||
|
<img v-else-if="event.type === 'START'" src="../assets/arrow-right-black.png" alt="Start"
|
||||||
|
class="border border-gray-500 rounded-full opacity-75 bg-gray-100 mr-2 inline" width="26"/>
|
||||||
{{ event.fancyText }}
|
{{ event.fancyText }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex mt-1 text-sm text-gray-400">
|
<div class="flex mt-1 text-sm text-gray-400">
|
||||||
@ -67,10 +92,12 @@ 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";
|
import Pagination from "@/components/Pagination";
|
||||||
|
import Chart from "@/components/Chart";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Details',
|
name: 'Details',
|
||||||
components: {
|
components: {
|
||||||
|
Chart,
|
||||||
Pagination,
|
Pagination,
|
||||||
Service,
|
Service,
|
||||||
Settings,
|
Settings,
|
||||||
@ -86,10 +113,19 @@ export default {
|
|||||||
if (JSON.stringify(this.serviceStatus) !== JSON.stringify(data)) {
|
if (JSON.stringify(this.serviceStatus) !== JSON.stringify(data)) {
|
||||||
this.serviceStatus = data.serviceStatus;
|
this.serviceStatus = data.serviceStatus;
|
||||||
this.uptime = data.uptime;
|
this.uptime = data.uptime;
|
||||||
|
let labels = [];
|
||||||
|
let values = [];
|
||||||
|
for (let key in data.hourlyAverageResponseTime) {
|
||||||
|
labels.push(new Date(key*1000));
|
||||||
|
values.push(data.hourlyAverageResponseTime[key]);
|
||||||
|
}
|
||||||
|
this.chartLabels = labels;
|
||||||
|
this.chartValues = values;
|
||||||
|
|
||||||
let events = [];
|
let events = [];
|
||||||
for (let i = data.events.length-1; i >= 0; i--) {
|
for (let i = data.events.length - 1; i >= 0; i--) {
|
||||||
let event = data.events[i];
|
let event = data.events[i];
|
||||||
if (i === data.events.length-1) {
|
if (i === data.events.length - 1) {
|
||||||
if (event.type === 'UNHEALTHY') {
|
if (event.type === 'UNHEALTHY') {
|
||||||
event.fancyText = 'Service is unhealthy';
|
event.fancyText = 'Service is unhealthy';
|
||||||
} else if (event.type === 'HEALTHY') {
|
} else if (event.type === 'HEALTHY') {
|
||||||
@ -98,7 +134,7 @@ export default {
|
|||||||
event.fancyText = 'Monitoring started';
|
event.fancyText = 'Monitoring started';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let nextEvent = data.events[i+1];
|
let nextEvent = data.events[i + 1];
|
||||||
if (event.type === 'HEALTHY') {
|
if (event.type === 'HEALTHY') {
|
||||||
event.fancyText = 'Service became healthy';
|
event.fancyText = 'Service became healthy';
|
||||||
} else if (event.type === 'UNHEALTHY') {
|
} else if (event.type === 'UNHEALTHY') {
|
||||||
@ -128,7 +164,7 @@ export default {
|
|||||||
return (uptime * 100).toFixed(2) + '%'
|
return (uptime * 100).toFixed(2) + '%'
|
||||||
},
|
},
|
||||||
prettifyTimeDifference(start, end) {
|
prettifyTimeDifference(start, end) {
|
||||||
let minutes = Math.ceil((new Date(start) - new Date(end))/1000/60);
|
let minutes = Math.ceil((new Date(start) - new Date(end)) / 1000 / 60);
|
||||||
return minutes + (minutes === 1 ? ' minute' : ' minutes');
|
return minutes + (minutes === 1 ? ' minute' : ' minutes');
|
||||||
},
|
},
|
||||||
changePage(page) {
|
changePage(page) {
|
||||||
@ -145,11 +181,14 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
serviceStatus: {},
|
serviceStatus: {},
|
||||||
|
uptime: {},
|
||||||
events: [],
|
events: [],
|
||||||
// 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,
|
currentPage: 1,
|
||||||
showAverageResponseTime: true,
|
showAverageResponseTime: true,
|
||||||
|
chartLabels: [],
|
||||||
|
chartValues: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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