mirror of
https://github.com/mentos1386/zdravko.git
synced 2024-11-21 23:33:34 +00:00
feat(index): working history page
This commit is contained in:
parent
bff8f2d027
commit
92e1958588
13 changed files with 340 additions and 169 deletions
|
@ -16,6 +16,7 @@ Mostly just a project to test [temporal.io](https://temporal.io/).
|
|||
- [ ] History and working home page.
|
||||
- [ ] Edit/Delete operations for healthchecks and workers.
|
||||
- [ ] CronJob Healthchecks (via webhooks).
|
||||
- [ ] Notifications (webhooks, slack, etc).
|
||||
|
||||
![Screenshot](docs/screenshot.png)
|
||||
Demo is available at https://zdravko.mnts.dev.
|
||||
|
|
|
@ -16,8 +16,8 @@ primary_region = 'waw'
|
|||
ROOT_URL = 'https://zdravko.mnts.dev'
|
||||
TEMPORAL_SERVER_HOST = 'server.process.zdravko.internal:7233'
|
||||
|
||||
TEMPORAL_DATABASE_PATH = '/data/temporal-1.db'
|
||||
DATABASE_PATH = '/data/zdravko-1.db'
|
||||
TEMPORAL_DATABASE_PATH = '/data/temporal-2.db'
|
||||
DATABASE_PATH = '/data/zdravko-2.db'
|
||||
|
||||
[processes]
|
||||
server = '--temporal --server'
|
||||
|
|
|
@ -1,54 +1,172 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"code.tjo.space/mentos1386/zdravko/internal/models"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/services"
|
||||
"code.tjo.space/mentos1386/zdravko/web/templates/components"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
type IndexData struct {
|
||||
*components.Base
|
||||
HealthChecks []*HealthCheck
|
||||
HealthChecks []*HealthCheck
|
||||
HealthchecksLength int
|
||||
TimeRange string
|
||||
Status string
|
||||
}
|
||||
|
||||
type HealthCheck struct {
|
||||
Domain string
|
||||
Healthy bool
|
||||
Uptime string
|
||||
History []bool
|
||||
Name string
|
||||
Status string
|
||||
HistoryDaily *History
|
||||
HistoryHourly *History
|
||||
}
|
||||
|
||||
func newMockHealthCheck(domain string) *HealthCheck {
|
||||
randBool := func() bool {
|
||||
return rand.Intn(2) == 1
|
||||
type History struct {
|
||||
History []string
|
||||
Uptime int
|
||||
}
|
||||
|
||||
func getDay(date time.Time) string {
|
||||
return date.Format("2006-01-02")
|
||||
}
|
||||
|
||||
func getHour(date time.Time) string {
|
||||
return date.Format("2006-01-02T15:04")
|
||||
}
|
||||
|
||||
func getDailyHistory(history []models.HealthcheckHistory) *History {
|
||||
numDays := 90
|
||||
historyDailyMap := map[string]string{}
|
||||
numOfSuccess := 0
|
||||
numTotal := 1
|
||||
|
||||
for i := 0; i < numDays; i++ {
|
||||
day := getDay(time.Now().AddDate(0, 0, -i).Truncate(time.Hour * 24))
|
||||
historyDailyMap[day] = models.HealthcheckUnknown
|
||||
}
|
||||
|
||||
var history []bool
|
||||
for i := 0; i < 90; i++ {
|
||||
history = append(history, randBool())
|
||||
for _, _history := range history {
|
||||
day := getDay(_history.CreatedAt.Truncate(time.Hour * 24))
|
||||
|
||||
// skip if day is not in the last 90 days
|
||||
if _, ok := historyDailyMap[day]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
numTotal++
|
||||
if _history.Status == models.HealthcheckSuccess {
|
||||
numOfSuccess++
|
||||
}
|
||||
|
||||
// skip if day is already set to failure
|
||||
if historyDailyMap[day] == models.HealthcheckFailure {
|
||||
continue
|
||||
}
|
||||
|
||||
historyDailyMap[day] = _history.Status
|
||||
}
|
||||
|
||||
return &HealthCheck{
|
||||
Domain: domain,
|
||||
Healthy: randBool(),
|
||||
Uptime: "100",
|
||||
History: history,
|
||||
historyDaily := make([]string, numDays)
|
||||
for i := 0; i < numDays; i++ {
|
||||
day := getDay(time.Now().AddDate(0, 0, -numDays+i+1).Truncate(time.Hour * 24))
|
||||
historyDaily[i] = historyDailyMap[day]
|
||||
}
|
||||
|
||||
return &History{
|
||||
History: historyDaily,
|
||||
Uptime: 100 * numOfSuccess / numTotal,
|
||||
}
|
||||
}
|
||||
|
||||
func getHourlyHistory(history []models.HealthcheckHistory) *History {
|
||||
numHours := 48
|
||||
historyHourlyMap := map[string]string{}
|
||||
numOfSuccess := 0
|
||||
numTotal := 1
|
||||
|
||||
for i := 0; i < numHours; i++ {
|
||||
hour := getHour(time.Now().Add(time.Hour * time.Duration(-i)).Truncate(time.Hour))
|
||||
historyHourlyMap[hour] = models.HealthcheckUnknown
|
||||
}
|
||||
|
||||
for _, _history := range history {
|
||||
hour := getHour(_history.CreatedAt.Truncate(time.Hour))
|
||||
|
||||
// skip if day is not in the last 90 days
|
||||
if _, ok := historyHourlyMap[hour]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
numTotal++
|
||||
if _history.Status == models.HealthcheckSuccess {
|
||||
numOfSuccess++
|
||||
}
|
||||
|
||||
// skip if day is already set to failure
|
||||
if historyHourlyMap[hour] == models.HealthcheckFailure {
|
||||
continue
|
||||
}
|
||||
|
||||
historyHourlyMap[hour] = _history.Status
|
||||
}
|
||||
|
||||
historyHourly := make([]string, numHours)
|
||||
for i := 0; i < numHours; i++ {
|
||||
hour := getHour(time.Now().Add(time.Hour * time.Duration(-numHours+i+1)).Truncate(time.Hour))
|
||||
historyHourly[i] = historyHourlyMap[hour]
|
||||
}
|
||||
|
||||
return &History{
|
||||
History: historyHourly,
|
||||
Uptime: 100 * numOfSuccess / numTotal,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *BaseHandler) Index(c echo.Context) error {
|
||||
ctx := context.Background()
|
||||
healthchecks, err := services.GetHealthchecksWithHistory(ctx, h.query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
timeRange := c.QueryParam("time-range")
|
||||
if timeRange != "48hours" && timeRange != "90days" {
|
||||
timeRange = "90days"
|
||||
}
|
||||
|
||||
overallStatus := "SUCCESS"
|
||||
|
||||
healthchecksWithHistory := make([]*HealthCheck, len(healthchecks))
|
||||
for i, healthcheck := range healthchecks {
|
||||
historyDaily := getDailyHistory(healthcheck.History)
|
||||
historyHourly := getHourlyHistory(healthcheck.History)
|
||||
|
||||
status := historyDaily.History[89]
|
||||
if status != models.HealthcheckSuccess {
|
||||
overallStatus = status
|
||||
}
|
||||
|
||||
healthchecksWithHistory[i] = &HealthCheck{
|
||||
Name: healthcheck.Name,
|
||||
Status: status,
|
||||
HistoryDaily: historyDaily,
|
||||
HistoryHourly: historyHourly,
|
||||
}
|
||||
}
|
||||
|
||||
return c.Render(http.StatusOK, "index.tmpl", &IndexData{
|
||||
Base: &components.Base{
|
||||
NavbarActive: GetPageByTitle(Pages, "Status"),
|
||||
Navbar: Pages,
|
||||
},
|
||||
HealthChecks: []*HealthCheck{
|
||||
newMockHealthCheck("example.com"),
|
||||
newMockHealthCheck("example.org"),
|
||||
newMockHealthCheck("example.net"),
|
||||
newMockHealthCheck("foo.example.net"),
|
||||
},
|
||||
HealthChecks: healthchecksWithHistory,
|
||||
HealthchecksLength: len(healthchecks),
|
||||
TimeRange: timeRange,
|
||||
Status: overallStatus,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -20,6 +20,13 @@ type Worker struct {
|
|||
Status string
|
||||
}
|
||||
|
||||
const (
|
||||
HealthcheckSuccess string = "SUCCESS"
|
||||
HealthcheckFailure string = "FAILURE"
|
||||
HealthcheckError string = "ERROR"
|
||||
HealthcheckUnknown string = "UNKNOWN"
|
||||
)
|
||||
|
||||
type Healthcheck struct {
|
||||
gorm.Model
|
||||
Slug string `gorm:"unique"`
|
||||
|
@ -30,7 +37,7 @@ type Healthcheck struct {
|
|||
|
||||
Script string `validate:"required"`
|
||||
|
||||
History []HealthcheckHistory `gorm:"foreignKey:ID"`
|
||||
History []HealthcheckHistory `gorm:"foreignKey:Healthcheck"`
|
||||
}
|
||||
|
||||
type Cronjob struct {
|
||||
|
@ -43,8 +50,8 @@ type Cronjob struct {
|
|||
|
||||
type HealthcheckHistory struct {
|
||||
gorm.Model
|
||||
Healthcheck Healthcheck `gorm:"foreignkey:ID"`
|
||||
Status string // SUCCESS, FAIL, ERROR
|
||||
Healthcheck uint
|
||||
Status string
|
||||
Note string
|
||||
}
|
||||
|
||||
|
|
|
@ -31,25 +31,9 @@ func newHealthcheckHistory(db *gorm.DB, opts ...gen.DOOption) healthcheckHistory
|
|||
_healthcheckHistory.CreatedAt = field.NewTime(tableName, "created_at")
|
||||
_healthcheckHistory.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||
_healthcheckHistory.DeletedAt = field.NewField(tableName, "deleted_at")
|
||||
_healthcheckHistory.Healthcheck = field.NewUint(tableName, "healthcheck")
|
||||
_healthcheckHistory.Status = field.NewString(tableName, "status")
|
||||
_healthcheckHistory.Healthcheck = healthcheckHistoryHasOneHealthcheck{
|
||||
db: db.Session(&gorm.Session{}),
|
||||
|
||||
RelationField: field.NewRelation("Healthcheck", "models.Healthcheck"),
|
||||
History: struct {
|
||||
field.RelationField
|
||||
Healthcheck struct {
|
||||
field.RelationField
|
||||
}
|
||||
}{
|
||||
RelationField: field.NewRelation("Healthcheck.History", "models.HealthcheckHistory"),
|
||||
Healthcheck: struct {
|
||||
field.RelationField
|
||||
}{
|
||||
RelationField: field.NewRelation("Healthcheck.History.Healthcheck", "models.Healthcheck"),
|
||||
},
|
||||
},
|
||||
}
|
||||
_healthcheckHistory.Note = field.NewString(tableName, "note")
|
||||
|
||||
_healthcheckHistory.fillFieldMap()
|
||||
|
||||
|
@ -64,8 +48,9 @@ type healthcheckHistory struct {
|
|||
CreatedAt field.Time
|
||||
UpdatedAt field.Time
|
||||
DeletedAt field.Field
|
||||
Healthcheck field.Uint
|
||||
Status field.String
|
||||
Healthcheck healthcheckHistoryHasOneHealthcheck
|
||||
Note field.String
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
|
@ -86,7 +71,9 @@ func (h *healthcheckHistory) updateTableName(table string) *healthcheckHistory {
|
|||
h.CreatedAt = field.NewTime(table, "created_at")
|
||||
h.UpdatedAt = field.NewTime(table, "updated_at")
|
||||
h.DeletedAt = field.NewField(table, "deleted_at")
|
||||
h.Healthcheck = field.NewUint(table, "healthcheck")
|
||||
h.Status = field.NewString(table, "status")
|
||||
h.Note = field.NewString(table, "note")
|
||||
|
||||
h.fillFieldMap()
|
||||
|
||||
|
@ -115,13 +102,14 @@ func (h *healthcheckHistory) GetFieldByName(fieldName string) (field.OrderExpr,
|
|||
}
|
||||
|
||||
func (h *healthcheckHistory) fillFieldMap() {
|
||||
h.fieldMap = make(map[string]field.Expr, 6)
|
||||
h.fieldMap = make(map[string]field.Expr, 7)
|
||||
h.fieldMap["id"] = h.ID
|
||||
h.fieldMap["created_at"] = h.CreatedAt
|
||||
h.fieldMap["updated_at"] = h.UpdatedAt
|
||||
h.fieldMap["deleted_at"] = h.DeletedAt
|
||||
h.fieldMap["healthcheck"] = h.Healthcheck
|
||||
h.fieldMap["status"] = h.Status
|
||||
|
||||
h.fieldMap["note"] = h.Note
|
||||
}
|
||||
|
||||
func (h healthcheckHistory) clone(db *gorm.DB) healthcheckHistory {
|
||||
|
@ -134,84 +122,6 @@ func (h healthcheckHistory) replaceDB(db *gorm.DB) healthcheckHistory {
|
|||
return h
|
||||
}
|
||||
|
||||
type healthcheckHistoryHasOneHealthcheck struct {
|
||||
db *gorm.DB
|
||||
|
||||
field.RelationField
|
||||
|
||||
History struct {
|
||||
field.RelationField
|
||||
Healthcheck struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a healthcheckHistoryHasOneHealthcheck) Where(conds ...field.Expr) *healthcheckHistoryHasOneHealthcheck {
|
||||
if len(conds) == 0 {
|
||||
return &a
|
||||
}
|
||||
|
||||
exprs := make([]clause.Expression, 0, len(conds))
|
||||
for _, cond := range conds {
|
||||
exprs = append(exprs, cond.BeCond().(clause.Expression))
|
||||
}
|
||||
a.db = a.db.Clauses(clause.Where{Exprs: exprs})
|
||||
return &a
|
||||
}
|
||||
|
||||
func (a healthcheckHistoryHasOneHealthcheck) WithContext(ctx context.Context) *healthcheckHistoryHasOneHealthcheck {
|
||||
a.db = a.db.WithContext(ctx)
|
||||
return &a
|
||||
}
|
||||
|
||||
func (a healthcheckHistoryHasOneHealthcheck) Session(session *gorm.Session) *healthcheckHistoryHasOneHealthcheck {
|
||||
a.db = a.db.Session(session)
|
||||
return &a
|
||||
}
|
||||
|
||||
func (a healthcheckHistoryHasOneHealthcheck) Model(m *models.HealthcheckHistory) *healthcheckHistoryHasOneHealthcheckTx {
|
||||
return &healthcheckHistoryHasOneHealthcheckTx{a.db.Model(m).Association(a.Name())}
|
||||
}
|
||||
|
||||
type healthcheckHistoryHasOneHealthcheckTx struct{ tx *gorm.Association }
|
||||
|
||||
func (a healthcheckHistoryHasOneHealthcheckTx) Find() (result *models.Healthcheck, err error) {
|
||||
return result, a.tx.Find(&result)
|
||||
}
|
||||
|
||||
func (a healthcheckHistoryHasOneHealthcheckTx) Append(values ...*models.Healthcheck) (err error) {
|
||||
targetValues := make([]interface{}, len(values))
|
||||
for i, v := range values {
|
||||
targetValues[i] = v
|
||||
}
|
||||
return a.tx.Append(targetValues...)
|
||||
}
|
||||
|
||||
func (a healthcheckHistoryHasOneHealthcheckTx) Replace(values ...*models.Healthcheck) (err error) {
|
||||
targetValues := make([]interface{}, len(values))
|
||||
for i, v := range values {
|
||||
targetValues[i] = v
|
||||
}
|
||||
return a.tx.Replace(targetValues...)
|
||||
}
|
||||
|
||||
func (a healthcheckHistoryHasOneHealthcheckTx) Delete(values ...*models.Healthcheck) (err error) {
|
||||
targetValues := make([]interface{}, len(values))
|
||||
for i, v := range values {
|
||||
targetValues[i] = v
|
||||
}
|
||||
return a.tx.Delete(targetValues...)
|
||||
}
|
||||
|
||||
func (a healthcheckHistoryHasOneHealthcheckTx) Clear() error {
|
||||
return a.tx.Clear()
|
||||
}
|
||||
|
||||
func (a healthcheckHistoryHasOneHealthcheckTx) Count() int64 {
|
||||
return a.tx.Count()
|
||||
}
|
||||
|
||||
type healthcheckHistoryDo struct{ gen.DO }
|
||||
|
||||
type IHealthcheckHistoryDo interface {
|
||||
|
|
|
@ -40,19 +40,6 @@ func newHealthcheck(db *gorm.DB, opts ...gen.DOOption) healthcheck {
|
|||
db: db.Session(&gorm.Session{}),
|
||||
|
||||
RelationField: field.NewRelation("History", "models.HealthcheckHistory"),
|
||||
Healthcheck: struct {
|
||||
field.RelationField
|
||||
History struct {
|
||||
field.RelationField
|
||||
}
|
||||
}{
|
||||
RelationField: field.NewRelation("History.Healthcheck", "models.Healthcheck"),
|
||||
History: struct {
|
||||
field.RelationField
|
||||
}{
|
||||
RelationField: field.NewRelation("History.Healthcheck.History", "models.HealthcheckHistory"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_healthcheck.fillFieldMap()
|
||||
|
@ -152,13 +139,6 @@ type healthcheckHasManyHistory struct {
|
|||
db *gorm.DB
|
||||
|
||||
field.RelationField
|
||||
|
||||
Healthcheck struct {
|
||||
field.RelationField
|
||||
History struct {
|
||||
field.RelationField
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a healthcheckHasManyHistory) Where(conds ...field.Expr) *healthcheckHasManyHistory {
|
||||
|
|
|
@ -3,6 +3,7 @@ package services
|
|||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"code.tjo.space/mentos1386/zdravko/internal/models"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/models/query"
|
||||
|
@ -17,12 +18,20 @@ func CreateHealthcheck(ctx context.Context, db *gorm.DB, healthcheck *models.Hea
|
|||
}
|
||||
|
||||
func GetHealthcheck(ctx context.Context, q *query.Query, slug string) (*models.Healthcheck, error) {
|
||||
log.Println("GetHealthcheck")
|
||||
return q.Healthcheck.WithContext(ctx).Where(
|
||||
q.Healthcheck.Slug.Eq(slug),
|
||||
q.Healthcheck.DeletedAt.IsNull(),
|
||||
).First()
|
||||
}
|
||||
|
||||
func GetHealthchecksWithHistory(ctx context.Context, q *query.Query) ([]*models.Healthcheck, error) {
|
||||
return q.Healthcheck.WithContext(ctx).Preload(
|
||||
q.Healthcheck.History,
|
||||
).Where(
|
||||
q.Healthcheck.DeletedAt.IsNull(),
|
||||
).Find()
|
||||
}
|
||||
|
||||
func StartHealthcheck(ctx context.Context, t client.Client, healthcheck *models.Healthcheck) error {
|
||||
log.Println("Starting Healthcheck Workflow")
|
||||
|
||||
|
@ -38,6 +47,7 @@ func StartHealthcheck(ctx context.Context, t client.Client, healthcheck *models.
|
|||
ID: id + "-" + group,
|
||||
Spec: client.ScheduleSpec{
|
||||
CronExpressions: []string{healthcheck.Schedule},
|
||||
Jitter: time.Second * 10,
|
||||
},
|
||||
Action: &client.ScheduleWorkflowAction{
|
||||
ID: id + "-" + group,
|
||||
|
|
|
@ -31,7 +31,7 @@ func ConnectServerToTemporal(cfg *config.ServerConfig) (client.Client, error) {
|
|||
provider := &AuthHeadersProvider{token}
|
||||
|
||||
// Try to connect to the Temporal Server
|
||||
return retry.Retry(10, 3*time.Second, func() (client.Client, error) {
|
||||
return retry.Retry(10, 2*time.Second, func() (client.Client, error) {
|
||||
return client.Dial(client.Options{
|
||||
HostPort: cfg.Temporal.ServerHost,
|
||||
HeadersProvider: provider,
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"time"
|
||||
|
||||
"code.tjo.space/mentos1386/zdravko/internal/activities"
|
||||
"code.tjo.space/mentos1386/zdravko/internal/models"
|
||||
"go.temporal.io/sdk/workflow"
|
||||
)
|
||||
|
||||
|
@ -28,9 +29,9 @@ func (w *Workflows) HealthcheckWorkflowDefinition(ctx workflow.Context, param He
|
|||
return err
|
||||
}
|
||||
|
||||
status := "failure"
|
||||
status := models.HealthcheckFailure
|
||||
if healthcheckResult.Success {
|
||||
status = "success"
|
||||
status = models.HealthcheckSuccess
|
||||
}
|
||||
|
||||
historyParam := activities.HealtcheckAddToHistoryParam{
|
||||
|
|
|
@ -57,3 +57,16 @@
|
|||
.healthchecks > div:not(:last-child) {
|
||||
@apply mb-3;
|
||||
}
|
||||
.healthchecks .time-range > a {
|
||||
@apply font-medium text-sm px-2.5 py-1;
|
||||
@apply text-black bg-gray-100 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-300;
|
||||
}
|
||||
.healthchecks .time-range > a.active {
|
||||
@apply text-white bg-blue-700 hover:bg-blue-800;
|
||||
}
|
||||
.healthchecks .time-range > a:first-child {
|
||||
@apply rounded-s-lg;
|
||||
}
|
||||
.healthchecks .time-range > a:last-child {
|
||||
@apply rounded-e-lg;
|
||||
}
|
||||
|
|
|
@ -818,6 +818,10 @@ video {
|
|||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.rounded-md {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
@ -837,6 +841,11 @@ video {
|
|||
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-400 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(156 163 175 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
|
||||
|
@ -1053,6 +1062,12 @@ video {
|
|||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.shadow-sm {
|
||||
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.navbar {
|
||||
margin-top: 2.5rem;
|
||||
display: flex;
|
||||
|
@ -1197,6 +1212,57 @@ video {
|
|||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.healthchecks .time-range > a {
|
||||
padding-left: 0.625rem;
|
||||
padding-right: 0.625rem;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
font-weight: 500;
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(0 0 0 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.healthchecks .time-range > a:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(209 213 219 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.healthchecks .time-range > a:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(147 197 253 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.healthchecks .time-range > a.active {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.healthchecks .time-range > a.active:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(30 64 175 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.healthchecks .time-range > a:first-child {
|
||||
border-start-start-radius: 0.5rem;
|
||||
border-end-start-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.healthchecks .time-range > a:last-child {
|
||||
border-start-end-radius: 0.5rem;
|
||||
border-end-end-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.odd\:bg-white:nth-child(odd) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||
|
@ -1212,6 +1278,11 @@ video {
|
|||
background-color: rgb(30 64 175 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-gray-500:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(107 114 128 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-green-500:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(34 197 94 / var(--tw-bg-opacity));
|
||||
|
|
|
@ -1,38 +1,96 @@
|
|||
{{define "daily"}}
|
||||
<div class="justify-self-end text-sm">{{ .HistoryDaily.Uptime }}% uptime</div>
|
||||
<div class="grid gap-px col-span-2 grid-flow-col h-8 rounded overflow-hidden">
|
||||
{{ range .HistoryDaily.History }}
|
||||
{{ if eq . "SUCCESS" }}
|
||||
<div class="bg-green-400 hover:bg-green-500 flex-auto"></div>
|
||||
{{ else if eq . "FAILURE" }}
|
||||
<div class="bg-red-400 hover:bg-red-500 flex-auto"></div>
|
||||
{{ else }}
|
||||
<div class="bg-gray-400 hover:bg-gray-500 flex-auto"></div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="text-slate-500 justify-self-start text-sm">90 days ago</div>
|
||||
<div class="text-slate-500 justify-self-end text-sm">Today</div>
|
||||
{{end}}
|
||||
|
||||
{{define "hourly"}}
|
||||
<div class="justify-self-end text-sm">{{ .HistoryHourly.Uptime }}% uptime</div>
|
||||
<div class="grid gap-px col-span-2 grid-flow-col h-8 rounded overflow-hidden">
|
||||
{{ range .HistoryHourly.History }}
|
||||
{{ if eq . "SUCCESS" }}
|
||||
<div class="bg-green-400 hover:bg-green-500 flex-auto"></div>
|
||||
{{ else if eq . "FAILURE" }}
|
||||
<div class="bg-red-400 hover:bg-red-500 flex-auto"></div>
|
||||
{{ else }}
|
||||
<div class="bg-gray-400 hover:bg-gray-500 flex-auto"></div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="text-slate-500 justify-self-start text-sm">48 hours ago</div>
|
||||
<div class="text-slate-500 justify-self-end text-sm">Now</div>
|
||||
{{end}}
|
||||
|
||||
{{define "main"}}
|
||||
{{ if eq .HealthchecksLength 0 }}
|
||||
<section>
|
||||
<div class="py-8 px-4 mx-auto max-w-screen-xl text-center lg:py-16">
|
||||
<h1 class="mb-4 text-2xl font-extrabold tracking-tight leading-none text-gray-900 md:text-3xl lg:text-4xl">
|
||||
There are no healthchecks yet.
|
||||
</h1>
|
||||
<p class="mb-8 text-l font-normal text-gray-500 lg:text-l sm:px-8 lg:px-40">
|
||||
Create a healthcheck to monitor your services and get notified when they are down.
|
||||
</p>
|
||||
<div class="flex flex-col space-y-4 sm:flex-row sm:justify-center sm:space-y-0">
|
||||
<a href="/settings/healthchecks/create" class="inline-flex justify-center items-center py-3 px-5 text-base font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300">
|
||||
Create First Healthcheck
|
||||
<svg class="feather ml-1 h-5 w-5 overflow-visible"><use href="/static/icons/feather-sprite.svg#plus" /></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{ else }}
|
||||
{{ if eq .Status "SUCCESS" }}
|
||||
<div class="flex flex-col items-center">
|
||||
<svg class="feather h-20 w-20 rounded-full bg-green-300 p-4 overflow-visible"><use href="/static/icons/feather-sprite.svg#check" /></svg>
|
||||
<h1 class="text-slate-500">All services are online</h1>
|
||||
<h1 class="text-slate-500 mt-4">All services are online</h1>
|
||||
<p class="text-slate-500 text-sm">Last updated on Feb 10 at 10:55am UTC</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-center mt-20">
|
||||
{{ else }}
|
||||
<div class="flex flex-col items-center">
|
||||
<svg class="feather h-20 w-20 rounded-full bg-red-300 p-4 overflow-visible"><use href="/static/icons/feather-sprite.svg#alert-triangle" /></svg>
|
||||
<h3 class="text-slate-500">Degraded performance</h3>
|
||||
<h3 class="text-slate-500 mt-4">Degraded performance</h3>
|
||||
<p class="text-slate-500 text-sm">Last updated on Feb 10 at 10:55am UTC</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="healthchecks">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<p class="text-l font-normal text-gray-500">Healthchecks</p>
|
||||
<div class="inline-flex rounded-md shadow-sm justify-self-end time-range" role="group">
|
||||
<a href="/?time-range=90days" class="{{ if eq .TimeRange "90days" }}active{{end}}" type="button">90 Days</a>
|
||||
<a href="/?time-range=48hours" class="{{ if eq .TimeRange "48hours" }}active{{end}}" type="button">48 Hours</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ range .HealthChecks }}
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="flex items-center">
|
||||
{{ if .Healthy }}
|
||||
{{ if eq .Status "SUCCESS" }}
|
||||
<span class="flex w-3 h-3 me-2 bg-green-400 rounded-full"></span>
|
||||
{{ else }}
|
||||
{{ else if eq .Status "FAILURE" }}
|
||||
<span class="flex w-3 h-3 me-2 bg-red-400 rounded-full"></span>
|
||||
{{ end }}
|
||||
<p>{{ .Domain }}</p>
|
||||
</div>
|
||||
<div class="justify-self-end text-sm">{{ .Uptime }}% uptime</div>
|
||||
<div class="grid gap-px col-span-2 grid-flow-col h-8 rounded overflow-hidden">
|
||||
{{ range .History }}
|
||||
{{ if . }}
|
||||
<div class="bg-green-400 hover:bg-green-500 flex-auto"></div>
|
||||
{{ else }}
|
||||
<div class="bg-red-400 hover:bg-red-500 flex-auto"></div>
|
||||
<span class="flex w-3 h-3 me-2 bg-gray-400 rounded-full"></span>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="text-slate-500 justify-self-start text-sm">90 days ago</div>
|
||||
<div class="text-slate-500 justify-self-end text-sm">Today</div>
|
||||
<p>{{ .Name }}</p>
|
||||
</div>
|
||||
{{ if eq $.TimeRange "90days" }}
|
||||
{{ template "daily" . }}
|
||||
{{ else }}
|
||||
{{ template "hourly" . }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{end}}
|
||||
|
|
|
@ -55,6 +55,8 @@ export default function () {
|
|||
value: script,
|
||||
language: 'javascript',
|
||||
minimap: { enabled: false },
|
||||
codeLens: false,
|
||||
contextmenu: false,
|
||||
});
|
||||
|
||||
const divElem = document.getElementById('editor');
|
||||
|
|
Loading…
Reference in a new issue