mirror of
https://github.com/mentos1386/zdravko.git
synced 2024-11-21 23:33:34 +00:00
feat(index): more information about checks and worker groups
This commit is contained in:
parent
2a0447bc7b
commit
3e8832709a
8 changed files with 232 additions and 117 deletions
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
|
"github.com/mentos1386/zdravko/database/models"
|
||||||
"github.com/mentos1386/zdravko/internal/server/services"
|
"github.com/mentos1386/zdravko/internal/server/services"
|
||||||
"github.com/mentos1386/zdravko/internal/temporal"
|
"github.com/mentos1386/zdravko/internal/temporal"
|
||||||
"github.com/mentos1386/zdravko/pkg/script"
|
"github.com/mentos1386/zdravko/pkg/script"
|
||||||
|
@ -25,6 +26,10 @@ func (a *Activities) TargetsFilter(ctx context.Context, param temporal.ActivityT
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, target := range allTargets {
|
for _, target := range allTargets {
|
||||||
|
if target.State == models.TargetStatePaused {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
vm := goja.New()
|
vm := goja.New()
|
||||||
vm.SetFieldNameMapper(goja.UncapFieldNameMapper())
|
vm.SetFieldNameMapper(goja.UncapFieldNameMapper())
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
@ -24,19 +25,33 @@ type Target struct {
|
||||||
Visibility models.TargetVisibility
|
Visibility models.TargetVisibility
|
||||||
Group string
|
Group string
|
||||||
Status models.TargetStatus
|
Status models.TargetStatus
|
||||||
History *History
|
History []*HistoryItem
|
||||||
}
|
Uptime float64
|
||||||
|
|
||||||
type HistoryItem struct {
|
|
||||||
Status models.TargetStatus
|
|
||||||
Date time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type History struct {
|
type History struct {
|
||||||
List []HistoryItem
|
List []*HistoryItem
|
||||||
Uptime float64
|
Uptime float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HistoryItem struct {
|
||||||
|
Status models.TargetStatus
|
||||||
|
StatusCounts map[models.TargetStatus]int
|
||||||
|
Counts int
|
||||||
|
Date time.Time
|
||||||
|
Checks []*HistoryItemCheck
|
||||||
|
SuccessRate float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type HistoryItemCheck struct {
|
||||||
|
Name string
|
||||||
|
WorkerGroupName string
|
||||||
|
Status models.TargetStatus
|
||||||
|
StatusCounts map[models.TargetStatus]int
|
||||||
|
Counts int
|
||||||
|
SuccessRate float64
|
||||||
|
}
|
||||||
|
|
||||||
type TargetsAndStatus struct {
|
type TargetsAndStatus struct {
|
||||||
Status models.TargetStatus
|
Status models.TargetStatus
|
||||||
Targets []*Target
|
Targets []*Target
|
||||||
|
@ -47,20 +62,31 @@ func getDateString(date time.Time) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getHistory(history []*services.TargetHistory, period time.Duration, buckets int) *History {
|
func getHistory(history []*services.TargetHistory, period time.Duration, buckets int) *History {
|
||||||
historyMap := map[string]models.TargetStatus{}
|
historyMap := map[string]*HistoryItem{}
|
||||||
numOfSuccess := 0.0
|
numOfSuccess := 0.0
|
||||||
numTotal := 0.0
|
numTotal := 0.0
|
||||||
|
|
||||||
|
mapKeys := make([]string, buckets)
|
||||||
|
|
||||||
for i := 0; i < buckets; i++ {
|
for i := 0; i < buckets; i++ {
|
||||||
dateString := getDateString(time.Now().Add(period * time.Duration(-i)).Truncate(period))
|
date := time.Now().Add(period * time.Duration(-i)).Truncate(period)
|
||||||
historyMap[dateString] = models.TargetStatusUnknown
|
dateString := getDateString(date)
|
||||||
|
mapKeys[i] = dateString
|
||||||
|
|
||||||
|
historyMap[dateString] = &HistoryItem{
|
||||||
|
Status: models.TargetStatusUnknown,
|
||||||
|
StatusCounts: map[models.TargetStatus]int{},
|
||||||
|
Date: date,
|
||||||
|
Checks: []*HistoryItemCheck{},
|
||||||
|
SuccessRate: 0.0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, _history := range history {
|
for _, _history := range history {
|
||||||
dateString := getDateString(_history.CreatedAt.Time.Truncate(period))
|
dateString := getDateString(_history.CreatedAt.Time.Truncate(period))
|
||||||
|
|
||||||
// Skip if not part of the "buckets"
|
entry, ok := historyMap[dateString]
|
||||||
if _, ok := historyMap[dateString]; !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,24 +95,58 @@ func getHistory(history []*services.TargetHistory, period time.Duration, buckets
|
||||||
numOfSuccess++
|
numOfSuccess++
|
||||||
}
|
}
|
||||||
|
|
||||||
// skip if it is already set to failure
|
if entry.Status == models.TargetStatusUnknown {
|
||||||
if historyMap[dateString] == models.TargetStatusFailure {
|
entry.Status = _history.Status
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: This is wrong! As we can have multiple targets in dateString.
|
// If not yet failure, and failing check. Mark as failing.
|
||||||
// We should look at only the newest one.
|
if _history.Status == models.TargetStatusFailure && entry.Status != models.TargetStatusFailure {
|
||||||
historyMap[dateString] = _history.Status
|
entry.Status = models.TargetStatusFailure
|
||||||
}
|
|
||||||
|
|
||||||
historyItems := make([]HistoryItem, buckets)
|
|
||||||
for i := 0; i < buckets; i++ {
|
|
||||||
date := time.Now().Add(period * time.Duration(-buckets+i+1)).Truncate(period)
|
|
||||||
datestring := getDateString(date)
|
|
||||||
historyItems[i] = HistoryItem{
|
|
||||||
Status: historyMap[datestring],
|
|
||||||
Date: date,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry.StatusCounts[_history.Status]++
|
||||||
|
entry.Counts++
|
||||||
|
entry.SuccessRate = 100.0 * float64(entry.StatusCounts[models.TargetStatusSuccess]) / float64(entry.Counts)
|
||||||
|
|
||||||
|
foundCheck := false
|
||||||
|
for _, check := range entry.Checks {
|
||||||
|
if check.Name == _history.CheckName && check.WorkerGroupName == _history.WorkerGroupName {
|
||||||
|
foundCheck = true
|
||||||
|
|
||||||
|
check.StatusCounts[_history.Status]++
|
||||||
|
check.Counts++
|
||||||
|
check.SuccessRate = 100.0 * float64(check.StatusCounts[models.TargetStatusSuccess]) / float64(check.Counts)
|
||||||
|
|
||||||
|
if check.Status != models.TargetStatusFailure && _history.Status == models.TargetStatusFailure {
|
||||||
|
check.Status = models.TargetStatusFailure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundCheck {
|
||||||
|
successRate := 0.0
|
||||||
|
if _history.Status == models.TargetStatusSuccess {
|
||||||
|
successRate = 100.0
|
||||||
|
}
|
||||||
|
entry.Checks = append(entry.Checks, &HistoryItemCheck{
|
||||||
|
Name: _history.CheckName,
|
||||||
|
WorkerGroupName: _history.WorkerGroupName,
|
||||||
|
Status: _history.Status,
|
||||||
|
StatusCounts: map[models.TargetStatus]int{
|
||||||
|
_history.Status: 1,
|
||||||
|
},
|
||||||
|
Counts: 1,
|
||||||
|
SuccessRate: successRate,
|
||||||
|
})
|
||||||
|
|
||||||
|
sort.Slice(entry.Checks, func(i, j int) bool {
|
||||||
|
byName := entry.Checks[i].Name < entry.Checks[j].Name
|
||||||
|
byWorkerGroupName := entry.Checks[i].WorkerGroupName < entry.Checks[j].WorkerGroupName
|
||||||
|
return byName || (entry.Checks[i].Name == entry.Checks[j].Name && byWorkerGroupName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
historyMap[dateString] = entry
|
||||||
}
|
}
|
||||||
|
|
||||||
uptime := 0.0
|
uptime := 0.0
|
||||||
|
@ -94,6 +154,12 @@ func getHistory(history []*services.TargetHistory, period time.Duration, buckets
|
||||||
uptime = 100.0 * numOfSuccess / numTotal
|
uptime = 100.0 * numOfSuccess / numTotal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
historyItems := make([]*HistoryItem, 0, len(historyMap))
|
||||||
|
for i := buckets - 1; i >= 0; i-- {
|
||||||
|
key := mapKeys[i]
|
||||||
|
historyItems = append(historyItems, historyMap[key])
|
||||||
|
}
|
||||||
|
|
||||||
return &History{
|
return &History{
|
||||||
List: historyItems,
|
List: historyItems,
|
||||||
Uptime: uptime,
|
Uptime: uptime,
|
||||||
|
@ -107,9 +173,27 @@ func (h *BaseHandler) Index(c echo.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
timeRange := c.QueryParam("time-range")
|
timeRangeQuery := c.QueryParam("time-range")
|
||||||
if timeRange != "48hours" && timeRange != "90days" && timeRange != "90minutes" {
|
if timeRangeQuery != "48hours" && timeRangeQuery != "90days" && timeRangeQuery != "90minutes" {
|
||||||
timeRange = "90days"
|
timeRangeQuery = "90days"
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeBuckets int
|
||||||
|
var timeInterval time.Duration
|
||||||
|
var timeRange services.TargetHistoryDateRange
|
||||||
|
switch timeRangeQuery {
|
||||||
|
case "90days":
|
||||||
|
timeRange = services.TargetHistoryDateRange90Days
|
||||||
|
timeInterval = time.Hour * 24
|
||||||
|
timeBuckets = 90
|
||||||
|
case "48hours":
|
||||||
|
timeRange = services.TargetHistoryDateRange48Hours
|
||||||
|
timeInterval = time.Hour
|
||||||
|
timeBuckets = 48
|
||||||
|
case "90minutes":
|
||||||
|
timeRange = services.TargetHistoryDateRange90Minutes
|
||||||
|
timeInterval = time.Minute
|
||||||
|
timeBuckets = 90
|
||||||
}
|
}
|
||||||
|
|
||||||
overallStatus := models.TargetStatusUnknown
|
overallStatus := models.TargetStatusUnknown
|
||||||
|
@ -117,20 +201,12 @@ func (h *BaseHandler) Index(c echo.Context) error {
|
||||||
|
|
||||||
targetsWithHistory := make([]*Target, len(targets))
|
targetsWithHistory := make([]*Target, len(targets))
|
||||||
for i, target := range targets {
|
for i, target := range targets {
|
||||||
history, err := services.GetTargetHistoryForTarget(ctx, h.db, target.Id)
|
history, err := services.GetTargetHistoryForTarget(ctx, h.db, target.Id, timeRange)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var historyResult *History
|
historyResult := getHistory(history, timeInterval, timeBuckets)
|
||||||
switch timeRange {
|
|
||||||
case "48hours":
|
|
||||||
historyResult = getHistory(history, time.Hour, 48)
|
|
||||||
case "90days":
|
|
||||||
historyResult = getHistory(history, time.Hour*24, 90)
|
|
||||||
case "90minutes":
|
|
||||||
historyResult = getHistory(history, time.Minute, 90)
|
|
||||||
}
|
|
||||||
|
|
||||||
if statusByGroup[target.Group] == "" {
|
if statusByGroup[target.Group] == "" {
|
||||||
statusByGroup[target.Group] = models.TargetStatusUnknown
|
statusByGroup[target.Group] = models.TargetStatusUnknown
|
||||||
|
@ -155,7 +231,8 @@ func (h *BaseHandler) Index(c echo.Context) error {
|
||||||
Visibility: target.Visibility,
|
Visibility: target.Visibility,
|
||||||
Group: target.Group,
|
Group: target.Group,
|
||||||
Status: status.Status,
|
Status: status.Status,
|
||||||
History: historyResult,
|
History: historyResult.List,
|
||||||
|
Uptime: historyResult.Uptime,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,7 +252,7 @@ func (h *BaseHandler) Index(c echo.Context) error {
|
||||||
Navbar: Pages,
|
Navbar: Pages,
|
||||||
},
|
},
|
||||||
Targets: targetsByGroup,
|
Targets: targetsByGroup,
|
||||||
TimeRange: timeRange,
|
TimeRange: timeRangeQuery,
|
||||||
Status: overallStatus,
|
Status: overallStatus,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,7 +83,7 @@ func (h *BaseHandler) SettingsTargetsDescribeGET(c echo.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
history, err := services.GetTargetHistoryForTarget(context.Background(), h.db, slug)
|
history, err := services.GetTargetHistoryForTarget(context.Background(), h.db, slug, services.TargetHistoryDateRange90Minutes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/mentos1386/zdravko/database/models"
|
"github.com/mentos1386/zdravko/database/models"
|
||||||
|
@ -14,28 +15,30 @@ type TargetHistory struct {
|
||||||
CheckName string `db:"check_name"`
|
CheckName string `db:"check_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetLastNTargetHistory(ctx context.Context, db *sqlx.DB, n int) ([]*TargetHistory, error) {
|
type TargetHistoryDateRange string
|
||||||
var targetHistory []*TargetHistory
|
|
||||||
err := db.SelectContext(ctx, &targetHistory, `
|
const (
|
||||||
SELECT
|
TargetHistoryDateRange90Days TargetHistoryDateRange = "90_DAYS"
|
||||||
th.*,
|
TargetHistoryDateRange48Hours TargetHistoryDateRange = "48_HOURS"
|
||||||
t.name AS target_name,
|
TargetHistoryDateRange90Minutes TargetHistoryDateRange = "90_MINUTES"
|
||||||
wg.name AS worker_group_name,
|
)
|
||||||
c.name AS check_name
|
|
||||||
FROM target_histories th
|
func GetTargetHistoryForTarget(ctx context.Context, db *sqlx.DB, targetId string, dateRange TargetHistoryDateRange) ([]*TargetHistory, error) {
|
||||||
LEFT JOIN targets t ON th.target_id = t.id
|
dateRangeFilter := ""
|
||||||
LEFT JOIN worker_groups wg ON th.worker_group_id = wg.id
|
switch dateRange {
|
||||||
LEFT JOIN checks c ON th.check_id = c.id
|
case TargetHistoryDateRange90Days:
|
||||||
WHERE th.target_id = $1
|
dateRangeFilter = "AND strftime('%Y-%m-%dT%H:%M:%fZ', th.created_at) >= datetime('now', 'localtime', '-90 days')"
|
||||||
ORDER BY th.created_at DESC
|
case TargetHistoryDateRange48Hours:
|
||||||
LIMIT $1
|
dateRangeFilter = "AND strftime('%Y-%m-%dT%H:%M:%fZ', th.created_at) >= datetime('now', 'localtime', '-48 hours')"
|
||||||
`, n)
|
case TargetHistoryDateRange90Minutes:
|
||||||
return targetHistory, err
|
dateRangeFilter = "AND strftime('%Y-%m-%dT%H:%M:%fZ', th.created_at) >= datetime('now', 'localtime', '-90 minutes')"
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTargetHistoryForTarget(ctx context.Context, db *sqlx.DB, targetId string) ([]*TargetHistory, error) {
|
|
||||||
var targetHistory []*TargetHistory
|
var targetHistory []*TargetHistory
|
||||||
err := db.SelectContext(ctx, &targetHistory, `
|
err := db.SelectContext(
|
||||||
|
ctx,
|
||||||
|
&targetHistory,
|
||||||
|
fmt.Sprintf(`
|
||||||
SELECT
|
SELECT
|
||||||
th.*,
|
th.*,
|
||||||
t.name AS target_name,
|
t.name AS target_name,
|
||||||
|
@ -45,9 +48,13 @@ func GetTargetHistoryForTarget(ctx context.Context, db *sqlx.DB, targetId string
|
||||||
LEFT JOIN targets t ON th.target_id = t.id
|
LEFT JOIN targets t ON th.target_id = t.id
|
||||||
LEFT JOIN worker_groups wg ON th.worker_group_id = wg.id
|
LEFT JOIN worker_groups wg ON th.worker_group_id = wg.id
|
||||||
LEFT JOIN checks c ON th.check_id = c.id
|
LEFT JOIN checks c ON th.check_id = c.id
|
||||||
WHERE th.target_id = $1
|
WHERE
|
||||||
|
th.target_id = $1
|
||||||
|
%s
|
||||||
ORDER BY th.created_at DESC
|
ORDER BY th.created_at DESC
|
||||||
`, targetId)
|
`, dateRangeFilter),
|
||||||
|
targetId,
|
||||||
|
)
|
||||||
return targetHistory, err
|
return targetHistory, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -730,10 +730,6 @@ video {
|
||||||
height: 5rem;
|
height: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-3 {
|
|
||||||
height: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-4 {
|
.h-4 {
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -768,10 +764,6 @@ video {
|
||||||
width: 5rem;
|
width: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-3 {
|
|
||||||
width: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-4 {
|
.w-4 {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -845,6 +837,10 @@ video {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.justify-between {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
.gap-1 {
|
.gap-1 {
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,19 +101,17 @@
|
||||||
<summary
|
<summary
|
||||||
class="flex flex-row gap-2 p-3 py-2 -mx-3 cursor-pointer hover:bg-blue-50 rounded-lg"
|
class="flex flex-row gap-2 p-3 py-2 -mx-3 cursor-pointer hover:bg-blue-50 rounded-lg"
|
||||||
>
|
>
|
||||||
{{ if eq $targetsAndStatus.Status "SUCCESS" }}
|
<span
|
||||||
<span
|
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
|
||||||
class="flex w-3 h-3 bg-green-400 rounded-full self-center"
|
{{ if eq $targetsAndStatus.Status "SUCCESS" }}
|
||||||
></span>
|
bg-green-100 text-green-800
|
||||||
{{ else if eq $targetsAndStatus.Status "FAILURE" }}
|
{{ else if eq $targetsAndStatus.Status "FAILURE" }}
|
||||||
<span
|
bg-red-100 text-red-800
|
||||||
class="flex w-3 h-3 bg-red-400 rounded-full self-center"
|
{{ else }}
|
||||||
></span>
|
bg-gray-100 text-gray-800
|
||||||
{{ else }}
|
{{ end }}"
|
||||||
<span
|
>{{ .Status }}</span
|
||||||
class="flex w-3 h-3 bg-gray-200 rounded-full self-center"
|
>
|
||||||
></span>
|
|
||||||
{{ end }}
|
|
||||||
<h2 class="flex-1 font-semibold capitalize">
|
<h2 class="flex-1 font-semibold capitalize">
|
||||||
{{ $group }}
|
{{ $group }}
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -128,13 +126,17 @@
|
||||||
class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-2 pb-2 border-b last-of-type:pb-0 last-of-type:border-0 border-gray-100"
|
class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-2 pb-2 border-b last-of-type:pb-0 last-of-type:border-0 border-gray-100"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{{ if eq .Status "SUCCESS" }}
|
<span
|
||||||
<span class="flex w-3 h-3 bg-green-400 rounded-full"></span>
|
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
|
||||||
{{ else if eq .Status "FAILURE" }}
|
{{ if eq .Status "SUCCESS" }}
|
||||||
<span class="flex w-3 h-3 bg-red-400 rounded-full"></span>
|
bg-green-100 text-green-800
|
||||||
{{ else }}
|
{{ else if eq .Status "FAILURE" }}
|
||||||
<span class="flex w-3 h-3 bg-gray-200 rounded-full"></span>
|
bg-red-100 text-red-800
|
||||||
{{ end }}
|
{{ else }}
|
||||||
|
bg-gray-100 text-gray-800
|
||||||
|
{{ end }}"
|
||||||
|
>{{ .Status }}</span
|
||||||
|
>
|
||||||
<h4>
|
<h4>
|
||||||
{{ .Name }}
|
{{ .Name }}
|
||||||
{{ if eq .Visibility "PUBLIC" }}
|
{{ if eq .Visibility "PUBLIC" }}
|
||||||
|
@ -159,12 +161,12 @@
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="justify-self-end text-sm">
|
<div class="justify-self-end text-sm">
|
||||||
{{ printf "%.2f" .History.Uptime }}% uptime
|
{{ printf "%.2f" .Uptime }}% uptime
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="grid gap-px col-span-2 grid-flow-col h-8 rounded overflow-hidden"
|
class="grid gap-px col-span-2 grid-flow-col h-8 rounded overflow-hidden"
|
||||||
>
|
>
|
||||||
{{ range .History.List }}
|
{{ range .History }}
|
||||||
<div
|
<div
|
||||||
class="has-tooltip [&_.tooltip]:hover:flex [&_.tooltip]:hover:visible flex"
|
class="has-tooltip [&_.tooltip]:hover:flex [&_.tooltip]:hover:visible flex"
|
||||||
>
|
>
|
||||||
|
@ -182,27 +184,51 @@
|
||||||
></div>
|
></div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<div
|
<div
|
||||||
class="tooltip gap-2 bg-white border border-gray-200 rounded p-2 shadow-lg hidden z-50 absolute mt-10 -ml-4 flex-row text-xs"
|
class="tooltip flex flex-col gap-2 bg-white border border-gray-200 rounded p-2 shadow-lg hidden z-50 absolute mt-10 -ml-4 flex-row text-xs"
|
||||||
>
|
>
|
||||||
{{ if eq .Status "SUCCESS" }}
|
<div
|
||||||
|
class="flex flex-row gap-2 justify-between pb-2 border-b border-gray-200"
|
||||||
|
>
|
||||||
|
{{ if eq $.TimeRange "90days" }}
|
||||||
|
{{ .Date.Format "Jan 02" }}
|
||||||
|
{{ else if eq $.TimeRange "48hours" }}
|
||||||
|
{{ .Date.Format "Jan 02, 15:00 MST" }}
|
||||||
|
{{ else if eq $.TimeRange "90minutes" }}
|
||||||
|
{{ .Date.Format "Jan 02, 15:04 MST" }}
|
||||||
|
{{ end }}
|
||||||
<span
|
<span
|
||||||
class="flex w-3 h-3 bg-green-400 rounded-full self-center"
|
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
|
||||||
></span>
|
{{ if eq .Status "SUCCESS" }}
|
||||||
{{ else if eq .Status "FAILURE" }}
|
bg-green-100 text-green-800
|
||||||
<span
|
{{ else if eq .Status "FAILURE" }}
|
||||||
class="flex w-3 h-3 bg-red-400 rounded-full self-center"
|
bg-red-100 text-red-800
|
||||||
></span>
|
{{ else }}
|
||||||
{{ else }}
|
bg-gray-100 text-gray-800
|
||||||
<span
|
{{ end }}"
|
||||||
class="flex w-3 h-3 bg-gray-200 rounded-full self-center"
|
>{{ .Status }}
|
||||||
></span>
|
({{ printf "%.2f" .SuccessRate }}%)
|
||||||
{{ end }}
|
</span>
|
||||||
{{ if eq $.TimeRange "90days" }}
|
</div>
|
||||||
{{ .Date.Format "Jan 02" }}
|
|
||||||
{{ else if eq $.TimeRange "48hours" }}
|
{{ range .Checks }}
|
||||||
{{ .Date.Format "Jan 02, 15:00 MST" }}
|
<div class="flex flex-row gap-2 justify-between">
|
||||||
{{ else if eq $.TimeRange "90minutes" }}
|
<p>
|
||||||
{{ .Date.Format "Jan 02, 15:04 MST" }}
|
{{ .Name }} on
|
||||||
|
{{ .WorkerGroupName }}
|
||||||
|
</p>
|
||||||
|
<span
|
||||||
|
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
|
||||||
|
{{ if eq .Status "SUCCESS" }}
|
||||||
|
bg-green-100 text-green-800
|
||||||
|
{{ else if eq .Status "FAILURE" }}
|
||||||
|
bg-red-100 text-red-800
|
||||||
|
{{ else }}
|
||||||
|
bg-gray-100 text-gray-800
|
||||||
|
{{ end }}"
|
||||||
|
>{{ .Status }}
|
||||||
|
({{ printf "%.2f" .SuccessRate }}%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -117,8 +117,7 @@
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Worker Group</th>
|
<th>Worker Group</th>
|
||||||
<th>Check</th>
|
<th>Check</th>
|
||||||
<th>Created At</th>
|
<th>Checked At</th>
|
||||||
<th>Duration</th>
|
|
||||||
<th>Note</th>
|
<th>Note</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -153,7 +152,6 @@
|
||||||
<td>
|
<td>
|
||||||
{{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }}
|
{{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }}
|
||||||
</td>
|
</td>
|
||||||
<td>{ .Duration }</td>
|
|
||||||
<td class="whitespace-normal">
|
<td class="whitespace-normal">
|
||||||
{{ .Note }}
|
{{ .Note }}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -26,6 +26,12 @@ func load(files ...string) *template.Template {
|
||||||
|
|
||||||
t := template.New("default").Funcs(
|
t := template.New("default").Funcs(
|
||||||
template.FuncMap{
|
template.FuncMap{
|
||||||
|
"MathDivide": func(a, b int) float64 {
|
||||||
|
if b == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return float64(a) / float64(b)
|
||||||
|
},
|
||||||
"DurationRoundSecond": func(d time.Duration) time.Duration {
|
"DurationRoundSecond": func(d time.Duration) time.Duration {
|
||||||
return d.Round(time.Second)
|
return d.Round(time.Second)
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue