refactor: schema changes etc.

This commit is contained in:
Tine 2024-02-29 23:42:56 +01:00
parent 5223aef1ca
commit 07c7c716c5
Signed by: mentos1386
SSH key fingerprint: SHA256:MNtTsLbihYaWF8j1fkOHfkKNlnN1JQfxEU/rBU8nCGw
26 changed files with 495 additions and 375 deletions

5
.gitignore vendored
View file

@ -6,9 +6,8 @@ package.json
node_modules/ node_modules/
# Database # Database
zdravko.db zdravko.db*
temporal.db temporal.db*
temporal.db-journal
# Keys # Keys
*.pem *.pem

View file

@ -2,6 +2,7 @@ package database
import ( import (
"embed" "embed"
"fmt"
"log/slog" "log/slog"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
@ -13,7 +14,7 @@ import (
var sqliteMigrations embed.FS var sqliteMigrations embed.FS
func ConnectToDatabase(logger *slog.Logger, path string) (*sqlx.DB, error) { func ConnectToDatabase(logger *slog.Logger, path string) (*sqlx.DB, error) {
db, err := sqlx.Connect("sqlite3", path) db, err := sqlx.Connect("sqlite3", fmt.Sprintf("%s?_journal=WAL&_timeout=5000&_fk=true", path))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -1,27 +1,64 @@
package models package models
import ( import (
"database/sql/driver"
"fmt"
"time" "time"
) )
type OAuth2State struct { type OAuth2State struct {
State string `db:"state"` State string `db:"state"`
ExpiresAt time.Time `db:"expires_at"` ExpiresAt Time `db:"expires_at"`
} }
type MonitorStatus string
const ( const (
MonitorSuccess string = "SUCCESS" MonitorSuccess MonitorStatus = "SUCCESS"
MonitorFailure string = "FAILURE" MonitorFailure MonitorStatus = "FAILURE"
MonitorError string = "ERROR" MonitorError MonitorStatus = "ERROR"
MonitorUnknown string = "UNKNOWN" MonitorUnknown MonitorStatus = "UNKNOWN"
) )
type Monitor struct { type Time struct {
CreatedAt time.Time `db:"created_at"` Time time.Time
UpdatedAt time.Time `db:"updated_at"` }
DeletedAt *time.Time `db:"deleted_at"`
Slug string `db:"slug"` // rfc3339Milli is like time.RFC3339Nano, but with millisecond precision, and fractional seconds do not have trailing
// zeros removed.
const rfc3339Milli = "2006-01-02T15:04:05.000Z07:00"
// Value satisfies driver.Valuer interface.
func (t *Time) Value() (driver.Value, error) {
return t.Time.UTC().Format(rfc3339Milli), nil
}
// Scan satisfies sql.Scanner interface.
func (t *Time) Scan(src any) error {
if src == nil {
return nil
}
s, ok := src.(string)
if !ok {
return fmt.Errorf("error scanning time, got %+v", src)
}
parsedT, err := time.Parse(rfc3339Milli, s)
if err != nil {
return err
}
t.Time = parsedT.UTC()
return nil
}
type Monitor struct {
CreatedAt Time `db:"created_at"`
UpdatedAt Time `db:"updated_at"`
Id string `db:"id"`
Name string `db:"name"` Name string `db:"name"`
Schedule string `db:"schedule"` Schedule string `db:"schedule"`
@ -36,19 +73,21 @@ type MonitorWithWorkerGroups struct {
} }
type MonitorHistory struct { type MonitorHistory struct {
CreatedAt time.Time `db:"created_at"` CreatedAt Time `db:"created_at"`
MonitorSlug string `db:"monitor_slug"` MonitorId string `db:"monitor_id"`
Status string `db:"status"` Status MonitorStatus `db:"status"`
Note string `db:"note"` Note string `db:"note"`
WorkerGroupId string `db:"worker_group_id"`
WorkerGroupName string `db:"worker_group_name"`
} }
type WorkerGroup struct { type WorkerGroup struct {
CreatedAt time.Time `db:"created_at"` CreatedAt Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"` UpdatedAt Time `db:"updated_at"`
DeletedAt *time.Time `db:"deleted_at"`
Slug string `db:"slug"` Id string `db:"id"`
Name string `db:"name"` Name string `db:"name"`
} }

View file

@ -1,56 +1,66 @@
-- +migrate Up -- +migrate Up
CREATE TABLE oauth2_states ( CREATE TABLE oauth2_states (
state TEXT, state TEXT NOT NULL,
expires_at DATETIME DEFAULT CURRENT_TIMESTAMP, expires_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
PRIMARY KEY (state) PRIMARY KEY (state)
); ) STRICT;
CREATE TABLE monitors ( CREATE TABLE monitors (
slug TEXT, id TEXT NOT NULL,
name TEXT, name TEXT NOT NULL,
schedule TEXT, schedule TEXT NOT NULL,
script TEXT, script TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
deleted_at DATETIME,
PRIMARY KEY (slug), PRIMARY KEY (id),
CONSTRAINT unique_monitors_name UNIQUE (name) CONSTRAINT unique_monitors_name UNIQUE (name)
); ) STRICT;
--CREATE TRIGGER monitors_updated_timestamp AFTER UPDATE ON monitors BEGIN
-- update monitors set updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') where id = new.id;
--END;
CREATE TABLE worker_groups ( CREATE TABLE worker_groups (
slug TEXT, id TEXT NOT NULL,
name TEXT, name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
deleted_at DATETIME,
PRIMARY KEY (slug), PRIMARY KEY (id),
CONSTRAINT unique_worker_groups_name UNIQUE (name) CONSTRAINT unique_worker_groups_name UNIQUE (name)
); ) STRICT;
--CREATE TRIGGER worker_groups_updated_timestamp AFTER UPDATE ON worker_groups BEGIN
-- update worker_groups set updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') where id = new.id;
--END;
CREATE TABLE monitor_worker_groups ( CREATE TABLE monitor_worker_groups (
worker_group_slug TEXT, worker_group_id TEXT NOT NULL,
monitor_slug TEXT, monitor_id TEXT NOT NULL,
PRIMARY KEY (worker_group_slug,monitor_slug), PRIMARY KEY (worker_group_id,monitor_id),
CONSTRAINT fk_monitor_worker_groups_worker_group FOREIGN KEY (worker_group_slug) REFERENCES worker_groups(slug), CONSTRAINT fk_monitor_worker_groups_worker_group FOREIGN KEY (worker_group_id) REFERENCES worker_groups(id),
CONSTRAINT fk_monitor_worker_groups_monitor FOREIGN KEY (monitor_slug) REFERENCES monitors(slug) CONSTRAINT fk_monitor_worker_groups_monitor FOREIGN KEY (monitor_id) REFERENCES monitors(id)
); ) STRICT;
CREATE TABLE monitor_histories ( CREATE TABLE monitor_histories (
monitor_slug TEXT, monitor_id TEXT NOT NULL,
status TEXT, worker_group_id TEXT NOT NULL,
note TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, status TEXT NOT NULL,
note TEXT NOT NULL,
PRIMARY KEY (monitor_slug, created_at), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
CONSTRAINT fk_monitors_history FOREIGN KEY (monitor_slug) REFERENCES monitors(slug)
); PRIMARY KEY (monitor_id, worker_group_id, created_at),
CONSTRAINT fk_monitor_histories_monitor FOREIGN KEY (monitor_id) REFERENCES monitors(id),
CONSTRAINT fk_monitor_histories_worker_group FOREIGN KEY (worker_group_id) REFERENCES worker_groups(id)
) STRICT;
-- +migrate Down -- +migrate Down
DROP TABLE oauth2_states; DROP TABLE oauth2_states;

View file

@ -17,8 +17,8 @@ primary_region = 'waw'
ROOT_URL = 'https://zdravko.mnts.dev' ROOT_URL = 'https://zdravko.mnts.dev'
TEMPORAL_SERVER_HOST = 'server.process.zdravko.internal:7233' TEMPORAL_SERVER_HOST = 'server.process.zdravko.internal:7233'
TEMPORAL_DATABASE_PATH = '/data/temporal-6.db' TEMPORAL_DATABASE_PATH = '/data/temporal-7.db'
DATABASE_PATH = '/data/zdravko-6.db' DATABASE_PATH = '/data/zdravko-7.db'
[processes] [processes]
server = '--temporal --server' server = '--temporal --server'

View file

@ -8,6 +8,7 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"code.tjo.space/mentos1386/zdravko/database/models"
"code.tjo.space/mentos1386/zdravko/pkg/api" "code.tjo.space/mentos1386/zdravko/pkg/api"
"code.tjo.space/mentos1386/zdravko/pkg/k6" "code.tjo.space/mentos1386/zdravko/pkg/k6"
) )
@ -33,22 +34,22 @@ func (a *Activities) Monitor(ctx context.Context, param HealtcheckParam) (*Monit
} }
type HealtcheckAddToHistoryParam struct { type HealtcheckAddToHistoryParam struct {
Slug string MonitorId string
Status string Status models.MonitorStatus
Note string Note string
WorkerGroup string WorkerGroupId string
} }
type MonitorAddToHistoryResult struct { type MonitorAddToHistoryResult struct {
} }
func (a *Activities) MonitorAddToHistory(ctx context.Context, param HealtcheckAddToHistoryParam) (*MonitorAddToHistoryResult, error) { func (a *Activities) MonitorAddToHistory(ctx context.Context, param HealtcheckAddToHistoryParam) (*MonitorAddToHistoryResult, error) {
url := fmt.Sprintf("%s/api/v1/monitors/%s/history", a.config.ApiUrl, param.Slug) url := fmt.Sprintf("%s/api/v1/monitors/%s/history", a.config.ApiUrl, param.MonitorId)
body := api.ApiV1MonitorsHistoryPOSTBody{ body := api.ApiV1MonitorsHistoryPOSTBody{
Status: param.Status, Status: param.Status,
Note: param.Note, Note: param.Note,
WorkerGroup: param.WorkerGroup, WorkerGroupId: param.WorkerGroupId,
} }
jsonBody, err := json.Marshal(body) jsonBody, err := json.Marshal(body)

View file

@ -31,7 +31,7 @@ func (h *BaseHandler) ApiV1WorkersConnectGET(c echo.Context) error {
response := ApiV1WorkersConnectGETResponse{ response := ApiV1WorkersConnectGETResponse{
Endpoint: h.config.Temporal.ServerHost, Endpoint: h.config.Temporal.ServerHost,
Group: workerGroup.Slug, Group: workerGroup.Id,
} }
return c.JSON(http.StatusOK, response) return c.JSON(http.StatusOK, response)
@ -42,8 +42,7 @@ func (h *BaseHandler) ApiV1WorkersConnectGET(c echo.Context) error {
// To somehow listen for the outcomes and then store them automatically. // To somehow listen for the outcomes and then store them automatically.
func (h *BaseHandler) ApiV1MonitorsHistoryPOST(c echo.Context) error { func (h *BaseHandler) ApiV1MonitorsHistoryPOST(c echo.Context) error {
ctx := context.Background() ctx := context.Background()
id := c.Param("id")
slug := c.Param("slug")
var body api.ApiV1MonitorsHistoryPOSTBody var body api.ApiV1MonitorsHistoryPOSTBody
err := (&echo.DefaultBinder{}).BindBody(c, &body) err := (&echo.DefaultBinder{}).BindBody(c, &body)
@ -51,7 +50,7 @@ func (h *BaseHandler) ApiV1MonitorsHistoryPOST(c echo.Context) error {
return err return err
} }
_, err = services.GetMonitor(ctx, h.db, slug) _, err = services.GetMonitor(ctx, h.db, id)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return echo.NewHTTPError(http.StatusNotFound, "Monitor not found") return echo.NewHTTPError(http.StatusNotFound, "Monitor not found")
@ -60,7 +59,8 @@ func (h *BaseHandler) ApiV1MonitorsHistoryPOST(c echo.Context) error {
} }
err = services.AddHistoryForMonitor(ctx, h.db, &models.MonitorHistory{ err = services.AddHistoryForMonitor(ctx, h.db, &models.MonitorHistory{
MonitorSlug: slug, MonitorId: id,
WorkerGroupId: body.WorkerGroupId,
Status: body.Status, Status: body.Status,
Note: body.Note, Note: body.Note,
}) })

View file

@ -16,45 +16,39 @@ type IndexData struct {
HealthChecks []*HealthCheck HealthChecks []*HealthCheck
MonitorsLength int MonitorsLength int
TimeRange string TimeRange string
Status string Status models.MonitorStatus
} }
type HealthCheck struct { type HealthCheck struct {
Name string Name string
Status string Status models.MonitorStatus
HistoryDaily *History History *History
HistoryHourly *History
} }
type History struct { type History struct {
History []string List []models.MonitorStatus
Uptime int Uptime int
} }
func getDay(date time.Time) string {
return date.Format("2006-01-02")
}
func getHour(date time.Time) string { func getHour(date time.Time) string {
return date.Format("2006-01-02T15:04") return date.UTC().Format("2006-01-02T15:04")
} }
func getDailyHistory(history []*models.MonitorHistory) *History { func getHistory(history []*models.MonitorHistory, period time.Duration, buckets int) *History {
numDays := 90 historyMap := map[string]models.MonitorStatus{}
historyDailyMap := map[string]string{}
numOfSuccess := 0 numOfSuccess := 0
numTotal := 0 numTotal := 0
for i := 0; i < numDays; i++ { for i := 0; i < buckets; i++ {
day := getDay(time.Now().AddDate(0, 0, -i).Truncate(time.Hour * 24)) datetime := getHour(time.Now().Add(period * time.Duration(-i)).Truncate(period))
historyDailyMap[day] = models.MonitorUnknown historyMap[datetime] = models.MonitorUnknown
} }
for _, _history := range history { for _, _history := range history {
day := getDay(_history.CreatedAt.Truncate(time.Hour * 24)) hour := getHour(_history.CreatedAt.Time.Truncate(time.Hour))
// skip if day is not in the last 90 days // Skip if not part of the "buckets"
if _, ok := historyDailyMap[day]; !ok { if _, ok := historyMap[hour]; !ok {
continue continue
} }
@ -63,18 +57,18 @@ func getDailyHistory(history []*models.MonitorHistory) *History {
numOfSuccess++ numOfSuccess++
} }
// skip if day is already set to failure // skip if it is already set to failure
if historyDailyMap[day] == models.MonitorFailure { if historyMap[hour] == models.MonitorFailure {
continue continue
} }
historyDailyMap[day] = _history.Status historyMap[hour] = _history.Status
} }
historyDaily := make([]string, numDays) historyHourly := make([]models.MonitorStatus, buckets)
for i := 0; i < numDays; i++ { for i := 0; i < buckets; i++ {
day := getDay(time.Now().AddDate(0, 0, -numDays+i+1).Truncate(time.Hour * 24)) datetime := getHour(time.Now().Add(period * time.Duration(-buckets+i+1)).Truncate(period))
historyDaily[i] = historyDailyMap[day] historyHourly[i] = historyMap[datetime]
} }
uptime := 0 uptime := 0
@ -83,56 +77,7 @@ func getDailyHistory(history []*models.MonitorHistory) *History {
} }
return &History{ return &History{
History: historyDaily, List: historyHourly,
Uptime: uptime,
}
}
func getHourlyHistory(history []*models.MonitorHistory) *History {
numHours := 48
historyHourlyMap := map[string]string{}
numOfSuccess := 0
numTotal := 0
for i := 0; i < numHours; i++ {
hour := getHour(time.Now().Add(time.Hour * time.Duration(-i)).Truncate(time.Hour))
historyHourlyMap[hour] = models.MonitorUnknown
}
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.MonitorSuccess {
numOfSuccess++
}
// skip if day is already set to failure
if historyHourlyMap[hour] == models.MonitorFailure {
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]
}
uptime := 0
if numTotal > 0 {
uptime = 100 * numOfSuccess / numTotal
}
return &History{
History: historyHourly,
Uptime: uptime, Uptime: uptime,
} }
} }
@ -145,23 +90,30 @@ func (h *BaseHandler) Index(c echo.Context) error {
} }
timeRange := c.QueryParam("time-range") timeRange := c.QueryParam("time-range")
if timeRange != "48hours" && timeRange != "90days" { if timeRange != "48hours" && timeRange != "90days" && timeRange != "90minutes" {
timeRange = "90days" timeRange = "90days"
} }
overallStatus := "SUCCESS" overallStatus := models.MonitorSuccess
monitorsWithHistory := make([]*HealthCheck, len(monitors)) monitorsWithHistory := make([]*HealthCheck, len(monitors))
for i, monitor := range monitors { for i, monitor := range monitors {
history, err := services.GetMonitorHistoryForMonitor(ctx, h.db, monitor.Slug) history, err := services.GetMonitorHistoryForMonitor(ctx, h.db, monitor.Id)
if err != nil { if err != nil {
return err return err
} }
historyDaily := getDailyHistory(history) var historyResult *History
historyHourly := getHourlyHistory(history) 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)
}
status := historyDaily.History[89] status := historyResult.List[len(historyResult.List)-1]
if status != models.MonitorSuccess { if status != models.MonitorSuccess {
overallStatus = status overallStatus = status
} }
@ -169,8 +121,7 @@ func (h *BaseHandler) Index(c echo.Context) error {
monitorsWithHistory[i] = &HealthCheck{ monitorsWithHistory[i] = &HealthCheck{
Name: monitor.Name, Name: monitor.Name,
Status: status, Status: status,
HistoryDaily: historyDaily, History: historyResult,
HistoryHourly: historyHourly,
} }
} }

View file

@ -87,7 +87,10 @@ func (h *BaseHandler) OAuth2LoginGET(c echo.Context) error {
conf := newOAuth2(h.config) conf := newOAuth2(h.config)
state := newRandomState() state := newRandomState()
err := services.CreateOAuth2State(ctx, h.db, &models.OAuth2State{State: state, ExpiresAt: time.Now().Add(5 * time.Minute)}) err := services.CreateOAuth2State(ctx, h.db, &models.OAuth2State{
State: state,
ExpiresAt: models.Time{Time: time.Now().Add(5 * time.Minute)},
})
if err != nil { if err != nil {
return err return err
} }
@ -108,7 +111,7 @@ func (h *BaseHandler) OAuth2CallbackGET(c echo.Context) error {
if err != nil { if err != nil {
return err return err
} }
if deleted == false { if !deleted {
return errors.New("invalid state") return errors.New("invalid state")
} }

View file

@ -3,6 +3,7 @@ package handlers
import ( import (
"net/http" "net/http"
"code.tjo.space/mentos1386/zdravko/internal/services"
"code.tjo.space/mentos1386/zdravko/web/templates/components" "code.tjo.space/mentos1386/zdravko/web/templates/components"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
@ -48,12 +49,42 @@ var SettingsNavbar = []*components.Page{
GetPageByTitle(SettingsPages, "Logout"), GetPageByTitle(SettingsPages, "Logout"),
} }
type SettingsOverview struct {
*Settings
WorkerGroupsCount int
MonitorsCount int
NotificationsCount int
History []*services.MonitorHistoryWithMonitor
}
func (h *BaseHandler) SettingsOverviewGET(c echo.Context) error { func (h *BaseHandler) SettingsOverviewGET(c echo.Context) error {
cc := c.(AuthenticatedContext) cc := c.(AuthenticatedContext)
ctx := c.Request().Context()
return c.Render(http.StatusOK, "settings_overview.tmpl", NewSettings( workerGroups, err := services.CountWorkerGroups(ctx, h.db)
if err != nil {
return err
}
monitors, err := services.CountMonitors(ctx, h.db)
if err != nil {
return err
}
history, err := services.GetLastNMonitorHistory(ctx, h.db, 10)
if err != nil {
return err
}
return c.Render(http.StatusOK, "settings_overview.tmpl", SettingsOverview{
Settings: NewSettings(
cc.Principal.User, cc.Principal.User,
GetPageByTitle(SettingsPages, "Overview"), GetPageByTitle(SettingsPages, "Overview"),
[]*components.Page{GetPageByTitle(SettingsPages, "Overview")}, []*components.Page{GetPageByTitle(SettingsPages, "Overview")},
)) ),
WorkerGroupsCount: workerGroups,
MonitorsCount: monitors,
NotificationsCount: 42,
History: history,
})
} }

View file

@ -55,7 +55,7 @@ func (h *BaseHandler) SettingsMonitorsGET(c echo.Context) error {
monitorsWithStatus := make([]*MonitorWithWorkerGroupsAndStatus, len(monitors)) monitorsWithStatus := make([]*MonitorWithWorkerGroupsAndStatus, len(monitors))
for i, monitor := range monitors { for i, monitor := range monitors {
status, err := services.GetMonitorStatus(context.Background(), h.temporal, monitor.Slug) status, err := services.GetMonitorStatus(context.Background(), h.temporal, monitor.Id)
if err != nil { if err != nil {
return err return err
} }
@ -79,14 +79,14 @@ func (h *BaseHandler) SettingsMonitorsGET(c echo.Context) error {
func (h *BaseHandler) SettingsMonitorsDescribeGET(c echo.Context) error { func (h *BaseHandler) SettingsMonitorsDescribeGET(c echo.Context) error {
cc := c.(AuthenticatedContext) cc := c.(AuthenticatedContext)
slug := c.Param("slug") slug := c.Param("id")
monitor, err := services.GetMonitorWithWorkerGroups(context.Background(), h.db, slug) monitor, err := services.GetMonitorWithWorkerGroups(context.Background(), h.db, slug)
if err != nil { if err != nil {
return err return err
} }
status, err := services.GetMonitorStatus(context.Background(), h.temporal, monitor.Slug) status, err := services.GetMonitorStatus(context.Background(), h.temporal, monitor.Id)
if err != nil { if err != nil {
return err return err
} }
@ -124,7 +124,7 @@ func (h *BaseHandler) SettingsMonitorsDescribeGET(c echo.Context) error {
} }
func (h *BaseHandler) SettingsMonitorsDescribeDELETE(c echo.Context) error { func (h *BaseHandler) SettingsMonitorsDescribeDELETE(c echo.Context) error {
slug := c.Param("slug") slug := c.Param("id")
err := services.DeleteMonitor(context.Background(), h.db, slug) err := services.DeleteMonitor(context.Background(), h.db, slug)
if err != nil { if err != nil {
@ -140,14 +140,14 @@ func (h *BaseHandler) SettingsMonitorsDescribeDELETE(c echo.Context) error {
} }
func (h *BaseHandler) SettingsMonitorsDisableGET(c echo.Context) error { func (h *BaseHandler) SettingsMonitorsDisableGET(c echo.Context) error {
slug := c.Param("slug") slug := c.Param("id")
monitor, err := services.GetMonitor(context.Background(), h.db, slug) monitor, err := services.GetMonitor(context.Background(), h.db, slug)
if err != nil { if err != nil {
return err return err
} }
err = services.SetMonitorStatus(context.Background(), h.temporal, monitor.Slug, services.MonitorStatusPaused) err = services.SetMonitorStatus(context.Background(), h.temporal, monitor.Id, services.MonitorStatusPaused)
if err != nil { if err != nil {
return err return err
} }
@ -156,14 +156,14 @@ func (h *BaseHandler) SettingsMonitorsDisableGET(c echo.Context) error {
} }
func (h *BaseHandler) SettingsMonitorsEnableGET(c echo.Context) error { func (h *BaseHandler) SettingsMonitorsEnableGET(c echo.Context) error {
slug := c.Param("slug") slug := c.Param("id")
monitor, err := services.GetMonitor(context.Background(), h.db, slug) monitor, err := services.GetMonitor(context.Background(), h.db, slug)
if err != nil { if err != nil {
return err return err
} }
err = services.SetMonitorStatus(context.Background(), h.temporal, monitor.Slug, services.MonitorStatusActive) err = services.SetMonitorStatus(context.Background(), h.temporal, monitor.Id, services.MonitorStatusActive)
if err != nil { if err != nil {
return err return err
} }
@ -173,7 +173,7 @@ func (h *BaseHandler) SettingsMonitorsEnableGET(c echo.Context) error {
func (h *BaseHandler) SettingsMonitorsDescribePOST(c echo.Context) error { func (h *BaseHandler) SettingsMonitorsDescribePOST(c echo.Context) error {
ctx := context.Background() ctx := context.Background()
monitorSlug := c.Param("slug") monitorId := c.Param("id")
update := UpdateMonitor{ update := UpdateMonitor{
WorkerGroups: strings.TrimSpace(c.FormValue("workergroups")), WorkerGroups: strings.TrimSpace(c.FormValue("workergroups")),
@ -185,7 +185,7 @@ func (h *BaseHandler) SettingsMonitorsDescribePOST(c echo.Context) error {
return err return err
} }
monitor, err := services.GetMonitor(ctx, h.db, monitorSlug) monitor, err := services.GetMonitor(ctx, h.db, monitorId)
if err != nil { if err != nil {
return err return err
} }
@ -209,7 +209,7 @@ func (h *BaseHandler) SettingsMonitorsDescribePOST(c echo.Context) error {
workerGroup, err := services.GetWorkerGroup(ctx, h.db, slug.Make(group)) workerGroup, err := services.GetWorkerGroup(ctx, h.db, slug.Make(group))
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
workerGroup = &models.WorkerGroup{Name: group, Slug: slug.Make(group)} workerGroup = &models.WorkerGroup{Name: group, Id: slug.Make(group)}
err = services.CreateWorkerGroup(ctx, h.db, workerGroup) err = services.CreateWorkerGroup(ctx, h.db, workerGroup)
if err != nil { if err != nil {
return err return err
@ -231,7 +231,7 @@ func (h *BaseHandler) SettingsMonitorsDescribePOST(c echo.Context) error {
return err return err
} }
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/monitors/%s", monitorSlug)) return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/monitors/%s", monitorId))
} }
func (h *BaseHandler) SettingsMonitorsCreateGET(c echo.Context) error { func (h *BaseHandler) SettingsMonitorsCreateGET(c echo.Context) error {
@ -249,7 +249,7 @@ func (h *BaseHandler) SettingsMonitorsCreateGET(c echo.Context) error {
func (h *BaseHandler) SettingsMonitorsCreatePOST(c echo.Context) error { func (h *BaseHandler) SettingsMonitorsCreatePOST(c echo.Context) error {
ctx := context.Background() ctx := context.Background()
monitorSlug := slug.Make(c.FormValue("name")) monitorId := slug.Make(c.FormValue("name"))
create := CreateMonitor{ create := CreateMonitor{
Name: c.FormValue("name"), Name: c.FormValue("name"),
@ -270,7 +270,7 @@ func (h *BaseHandler) SettingsMonitorsCreatePOST(c echo.Context) error {
workerGroup, err := services.GetWorkerGroup(ctx, h.db, slug.Make(group)) workerGroup, err := services.GetWorkerGroup(ctx, h.db, slug.Make(group))
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
workerGroup = &models.WorkerGroup{Name: group, Slug: slug.Make(group)} workerGroup = &models.WorkerGroup{Name: group, Id: slug.Make(group)}
err = services.CreateWorkerGroup(ctx, h.db, workerGroup) err = services.CreateWorkerGroup(ctx, h.db, workerGroup)
if err != nil { if err != nil {
return err return err
@ -284,7 +284,7 @@ func (h *BaseHandler) SettingsMonitorsCreatePOST(c echo.Context) error {
monitor := &models.Monitor{ monitor := &models.Monitor{
Name: create.Name, Name: create.Name,
Slug: monitorSlug, Id: monitorId,
Schedule: create.Schedule, Schedule: create.Schedule,
Script: create.Script, Script: create.Script,
} }

View file

@ -46,7 +46,7 @@ func (h *BaseHandler) SettingsWorkerGroupsGET(c echo.Context) error {
workerGroupsWithActiveWorkers := make([]*WorkerGroupWithActiveWorkers, len(workerGroups)) workerGroupsWithActiveWorkers := make([]*WorkerGroupWithActiveWorkers, len(workerGroups))
for i, workerGroup := range workerGroups { for i, workerGroup := range workerGroups {
activeWorkers, err := services.GetActiveWorkers(context.Background(), workerGroup.Slug, h.temporal) activeWorkers, err := services.GetActiveWorkers(context.Background(), workerGroup.Id, h.temporal)
if err != nil { if err != nil {
return err return err
} }
@ -69,10 +69,9 @@ func (h *BaseHandler) SettingsWorkerGroupsGET(c echo.Context) error {
func (h *BaseHandler) SettingsWorkerGroupsDescribeGET(c echo.Context) error { func (h *BaseHandler) SettingsWorkerGroupsDescribeGET(c echo.Context) error {
cc := c.(AuthenticatedContext) cc := c.(AuthenticatedContext)
id := c.Param("id")
slug := c.Param("slug") worker, err := services.GetWorkerGroup(context.Background(), h.db, id)
worker, err := services.GetWorkerGroup(context.Background(), h.db, slug)
if err != nil { if err != nil {
return err return err
} }
@ -83,7 +82,7 @@ func (h *BaseHandler) SettingsWorkerGroupsDescribeGET(c echo.Context) error {
return err return err
} }
activeWorkers, err := services.GetActiveWorkers(context.Background(), worker.Slug, h.temporal) activeWorkers, err := services.GetActiveWorkers(context.Background(), worker.Id, h.temporal)
if err != nil { if err != nil {
return err return err
} }
@ -95,7 +94,7 @@ func (h *BaseHandler) SettingsWorkerGroupsDescribeGET(c echo.Context) error {
[]*components.Page{ []*components.Page{
GetPageByTitle(SettingsPages, "Worker Groups"), GetPageByTitle(SettingsPages, "Worker Groups"),
{ {
Path: fmt.Sprintf("/settings/worker-groups/%s", slug), Path: fmt.Sprintf("/settings/worker-groups/%s", id),
Title: "Describe", Title: "Describe",
Breadcrumb: worker.Name, Breadcrumb: worker.Name,
}, },
@ -109,9 +108,9 @@ func (h *BaseHandler) SettingsWorkerGroupsDescribeGET(c echo.Context) error {
} }
func (h *BaseHandler) SettingsWorkerGroupsDescribeDELETE(c echo.Context) error { func (h *BaseHandler) SettingsWorkerGroupsDescribeDELETE(c echo.Context) error {
slug := c.Param("slug") id := c.Param("id")
err := services.DeleteWorkerGroup(context.Background(), h.db, slug) err := services.DeleteWorkerGroup(context.Background(), h.db, id)
if err != nil { if err != nil {
return err return err
} }
@ -134,11 +133,11 @@ func (h *BaseHandler) SettingsWorkerGroupsCreateGET(c echo.Context) error {
func (h *BaseHandler) SettingsWorkerGroupsCreatePOST(c echo.Context) error { func (h *BaseHandler) SettingsWorkerGroupsCreatePOST(c echo.Context) error {
ctx := context.Background() ctx := context.Background()
slug := slug.Make(c.FormValue("name")) id := slug.Make(c.FormValue("name"))
workerGroup := &models.WorkerGroup{ workerGroup := &models.WorkerGroup{
Name: c.FormValue("name"), Name: c.FormValue("name"),
Slug: slug, Id: id,
} }
err := validator.New(validator.WithRequiredStructEnabled()).Struct(workerGroup) err := validator.New(validator.WithRequiredStructEnabled()).Struct(workerGroup)
@ -155,5 +154,5 @@ func (h *BaseHandler) SettingsWorkerGroupsCreatePOST(c echo.Context) error {
return err return err
} }
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/worker-groups/%s", slug)) return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/worker-groups/%s", id))
} }

View file

@ -79,7 +79,7 @@ func NewTokenForWorker(privateKey string, publicKey string, workerGroup *models.
IssuedAt: jwt.NewNumericDate(time.Now()), IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "zdravko", Issuer: "zdravko",
Subject: "worker-group:" + workerGroup.Slug, Subject: "worker-group:" + workerGroup.Id,
}, },
// Ref: https://docs.temporal.io/self-hosted-guide/security#authorization // Ref: https://docs.temporal.io/self-hosted-guide/security#authorization
[]string{"default:read", "default:write", "default:worker"}, []string{"default:read", "default:write", "default:worker"},

View file

@ -21,12 +21,18 @@ const (
MonitorStatusActive MonitorStatus = "ACTIVE" MonitorStatusActive MonitorStatus = "ACTIVE"
) )
func getScheduleId(slug string) string { func getScheduleId(id string) string {
return "monitor-" + slug return "monitor-" + id
} }
func GetMonitorStatus(ctx context.Context, temporal client.Client, slug string) (MonitorStatus, error) { func CountMonitors(ctx context.Context, db *sqlx.DB) (int, error) {
schedule := temporal.ScheduleClient().GetHandle(ctx, getScheduleId(slug)) var count int
err := db.GetContext(ctx, &count, "SELECT COUNT(*) FROM monitors")
return count, err
}
func GetMonitorStatus(ctx context.Context, temporal client.Client, id string) (MonitorStatus, error) {
schedule := temporal.ScheduleClient().GetHandle(ctx, getScheduleId(id))
description, err := schedule.Describe(ctx) description, err := schedule.Describe(ctx)
if err != nil { if err != nil {
@ -40,8 +46,8 @@ func GetMonitorStatus(ctx context.Context, temporal client.Client, slug string)
return MonitorStatusActive, nil return MonitorStatusActive, nil
} }
func SetMonitorStatus(ctx context.Context, temporal client.Client, slug string, status MonitorStatus) error { func SetMonitorStatus(ctx context.Context, temporal client.Client, id string, status MonitorStatus) error {
schedule := temporal.ScheduleClient().GetHandle(ctx, getScheduleId(slug)) schedule := temporal.ScheduleClient().GetHandle(ctx, getScheduleId(id))
if status == MonitorStatusActive { if status == MonitorStatusActive {
return schedule.Unpause(ctx, client.ScheduleUnpauseOptions{Note: "Unpaused by user"}) return schedule.Unpause(ctx, client.ScheduleUnpauseOptions{Note: "Unpaused by user"})
@ -56,7 +62,7 @@ func SetMonitorStatus(ctx context.Context, temporal client.Client, slug string,
func CreateMonitor(ctx context.Context, db *sqlx.DB, monitor *models.Monitor) error { func CreateMonitor(ctx context.Context, db *sqlx.DB, monitor *models.Monitor) error {
_, err := db.NamedExecContext(ctx, _, err := db.NamedExecContext(ctx,
"INSERT INTO monitors (slug, name, script, schedule) VALUES (:slug, :name, :script, :schedule)", "INSERT INTO monitors (id, name, script, schedule) VALUES (:id, :name, :script, :schedule)",
monitor, monitor,
) )
return err return err
@ -64,16 +70,16 @@ func CreateMonitor(ctx context.Context, db *sqlx.DB, monitor *models.Monitor) er
func UpdateMonitor(ctx context.Context, db *sqlx.DB, monitor *models.Monitor) error { func UpdateMonitor(ctx context.Context, db *sqlx.DB, monitor *models.Monitor) error {
_, err := db.NamedExecContext(ctx, _, err := db.NamedExecContext(ctx,
"UPDATE monitors SET name=:name, script=:script, schedule=:schedule WHERE slug=:slug", "UPDATE monitors SET name=:name, script=:script, schedule=:schedule WHERE id=:id",
monitor, monitor,
) )
return err return err
} }
func DeleteMonitor(ctx context.Context, db *sqlx.DB, slug string) error { func DeleteMonitor(ctx context.Context, db *sqlx.DB, id string) error {
_, err := db.ExecContext(ctx, _, err := db.ExecContext(ctx,
"UPDATE monitors SET deleted_at = datetime('now') WHERE slug=$1", "DELETE FROM monitors WHERE id=$1",
slug, id,
) )
return err return err
} }
@ -84,8 +90,8 @@ func UpdateMonitorWorkerGroups(ctx context.Context, db *sqlx.DB, monitor *models
return err return err
} }
_, err = tx.ExecContext(ctx, _, err = tx.ExecContext(ctx,
"DELETE FROM monitor_worker_groups WHERE monitor_slug=$1", "DELETE FROM monitor_worker_groups WHERE monitor_id=$1",
monitor.Slug, monitor.Id,
) )
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()
@ -93,9 +99,9 @@ func UpdateMonitorWorkerGroups(ctx context.Context, db *sqlx.DB, monitor *models
} }
for _, group := range workerGroups { for _, group := range workerGroups {
_, err = tx.ExecContext(ctx, _, err = tx.ExecContext(ctx,
"INSERT INTO monitor_worker_groups (monitor_slug, worker_group_slug) VALUES ($1, $2)", "INSERT INTO monitor_worker_groups (monitor_id, worker_group_id) VALUES ($1, $2)",
monitor.Slug, monitor.Id,
group.Slug, group.Id,
) )
if err != nil { if err != nil {
tx.Rollback() tx.Rollback()
@ -105,33 +111,32 @@ func UpdateMonitorWorkerGroups(ctx context.Context, db *sqlx.DB, monitor *models
return tx.Commit() return tx.Commit()
} }
func GetMonitor(ctx context.Context, db *sqlx.DB, slug string) (*models.Monitor, error) { func GetMonitor(ctx context.Context, db *sqlx.DB, id string) (*models.Monitor, error) {
monitor := &models.Monitor{} monitor := &models.Monitor{}
err := db.GetContext(ctx, monitor, err := db.GetContext(ctx, monitor,
"SELECT * FROM monitors WHERE slug=$1 AND deleted_at IS NULL", "SELECT * FROM monitors WHERE id=$1",
slug, id,
) )
return monitor, err return monitor, err
} }
func GetMonitorWithWorkerGroups(ctx context.Context, db *sqlx.DB, slug string) (*models.MonitorWithWorkerGroups, error) { func GetMonitorWithWorkerGroups(ctx context.Context, db *sqlx.DB, id string) (*models.MonitorWithWorkerGroups, error) {
rows, err := db.QueryContext(ctx, rows, err := db.QueryContext(ctx,
` `
SELECT SELECT
monitors.slug, monitors.id,
monitors.name, monitors.name,
monitors.script, monitors.script,
monitors.schedule, monitors.schedule,
monitors.created_at, monitors.created_at,
monitors.updated_at, monitors.updated_at,
monitors.deleted_at,
worker_groups.name as worker_group_name worker_groups.name as worker_group_name
FROM monitors FROM monitors
LEFT OUTER JOIN monitor_worker_groups ON monitors.slug = monitor_worker_groups.monitor_slug LEFT OUTER JOIN monitor_worker_groups ON monitors.id = monitor_worker_groups.monitor_id
LEFT OUTER JOIN worker_groups ON monitor_worker_groups.worker_group_slug = worker_groups.slug LEFT OUTER JOIN worker_groups ON monitor_worker_groups.worker_group_id = worker_groups.id
WHERE monitors.slug=$1 AND monitors.deleted_at IS NULL WHERE monitors.id=$1
`, `,
slug, id,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -143,13 +148,12 @@ WHERE monitors.slug=$1 AND monitors.deleted_at IS NULL
for rows.Next() { for rows.Next() {
var workerGroupName *string var workerGroupName *string
err = rows.Scan( err = rows.Scan(
&monitor.Slug, &monitor.Id,
&monitor.Name, &monitor.Name,
&monitor.Script, &monitor.Script,
&monitor.Schedule, &monitor.Schedule,
&monitor.CreatedAt, &monitor.CreatedAt,
&monitor.UpdatedAt, &monitor.UpdatedAt,
&monitor.DeletedAt,
&workerGroupName, &workerGroupName,
) )
if err != nil { if err != nil {
@ -166,7 +170,7 @@ WHERE monitors.slug=$1 AND monitors.deleted_at IS NULL
func GetMonitors(ctx context.Context, db *sqlx.DB) ([]*models.Monitor, error) { func GetMonitors(ctx context.Context, db *sqlx.DB) ([]*models.Monitor, error) {
monitors := []*models.Monitor{} monitors := []*models.Monitor{}
err := db.SelectContext(ctx, &monitors, err := db.SelectContext(ctx, &monitors,
"SELECT * FROM monitors WHERE deleted_at IS NULL ORDER BY name", "SELECT * FROM monitors ORDER BY name",
) )
return monitors, err return monitors, err
} }
@ -175,18 +179,16 @@ func GetMonitorsWithWorkerGroups(ctx context.Context, db *sqlx.DB) ([]*models.Mo
rows, err := db.QueryContext(ctx, rows, err := db.QueryContext(ctx,
` `
SELECT SELECT
monitors.slug, monitors.id,
monitors.name, monitors.name,
monitors.script, monitors.script,
monitors.schedule, monitors.schedule,
monitors.created_at, monitors.created_at,
monitors.updated_at, monitors.updated_at,
monitors.deleted_at,
worker_groups.name as worker_group_name worker_groups.name as worker_group_name
FROM monitors FROM monitors
LEFT OUTER JOIN monitor_worker_groups ON monitors.slug = monitor_worker_groups.monitor_slug LEFT OUTER JOIN monitor_worker_groups ON monitors.id = monitor_worker_groups.monitor_id
LEFT OUTER JOIN worker_groups ON monitor_worker_groups.worker_group_slug = worker_groups.slug LEFT OUTER JOIN worker_groups ON monitor_worker_groups.worker_group_id = worker_groups.id
WHERE monitors.deleted_at IS NULL
ORDER BY monitors.name ORDER BY monitors.name
`) `)
if err != nil { if err != nil {
@ -201,13 +203,12 @@ ORDER BY monitors.name
var workerGroupName *string var workerGroupName *string
err = rows.Scan( err = rows.Scan(
&monitor.Slug, &monitor.Id,
&monitor.Name, &monitor.Name,
&monitor.Script, &monitor.Script,
&monitor.Schedule, &monitor.Schedule,
&monitor.CreatedAt, &monitor.CreatedAt,
&monitor.UpdatedAt, &monitor.UpdatedAt,
&monitor.DeletedAt,
&workerGroupName, &workerGroupName,
) )
if err != nil { if err != nil {
@ -215,19 +216,19 @@ ORDER BY monitors.name
} }
if workerGroupName != nil { if workerGroupName != nil {
workerGroups := []string{} workerGroups := []string{}
if monitors[monitor.Slug] != nil { if monitors[monitor.Id] != nil {
workerGroups = monitors[monitor.Slug].WorkerGroups workerGroups = monitors[monitor.Id].WorkerGroups
} }
monitor.WorkerGroups = append(workerGroups, *workerGroupName) monitor.WorkerGroups = append(workerGroups, *workerGroupName)
} }
monitors[monitor.Slug] = monitor monitors[monitor.Id] = monitor
} }
return maps.Values(monitors), err return maps.Values(monitors), err
} }
func DeleteMonitorSchedule(ctx context.Context, t client.Client, slug string) error { func DeleteMonitorSchedule(ctx context.Context, t client.Client, id string) error {
schedule := t.ScheduleClient().GetHandle(ctx, getScheduleId(slug)) schedule := t.ScheduleClient().GetHandle(ctx, getScheduleId(id))
return schedule.Delete(ctx) return schedule.Delete(ctx)
} }
@ -241,24 +242,24 @@ func CreateOrUpdateMonitorSchedule(
workerGroupStrings := make([]string, len(workerGroups)) workerGroupStrings := make([]string, len(workerGroups))
for i, group := range workerGroups { for i, group := range workerGroups {
workerGroupStrings[i] = group.Slug workerGroupStrings[i] = group.Id
} }
args := make([]interface{}, 1) args := make([]interface{}, 1)
args[0] = workflows.MonitorWorkflowParam{ args[0] = workflows.MonitorWorkflowParam{
Script: monitor.Script, Script: monitor.Script,
Slug: monitor.Slug, MonitorId: monitor.Id,
WorkerGroups: workerGroupStrings, WorkerGroupIds: workerGroupStrings,
} }
options := client.ScheduleOptions{ options := client.ScheduleOptions{
ID: getScheduleId(monitor.Slug), ID: getScheduleId(monitor.Id),
Spec: client.ScheduleSpec{ Spec: client.ScheduleSpec{
CronExpressions: []string{monitor.Schedule}, CronExpressions: []string{monitor.Schedule},
Jitter: time.Second * 10, Jitter: time.Second * 10,
}, },
Action: &client.ScheduleWorkflowAction{ Action: &client.ScheduleWorkflowAction{
ID: getScheduleId(monitor.Slug), ID: getScheduleId(monitor.Id),
Workflow: workflows.NewWorkflows(nil).MonitorWorkflowDefinition, Workflow: workflows.NewWorkflows(nil).MonitorWorkflowDefinition,
Args: args, Args: args,
TaskQueue: "default", TaskQueue: "default",
@ -268,7 +269,7 @@ func CreateOrUpdateMonitorSchedule(
}, },
} }
schedule := t.ScheduleClient().GetHandle(ctx, getScheduleId(monitor.Slug)) schedule := t.ScheduleClient().GetHandle(ctx, getScheduleId(monitor.Id))
// If exists, we update // If exists, we update
_, err := schedule.Describe(ctx) _, err := schedule.Describe(ctx)

View file

@ -7,18 +7,60 @@ import (
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
func GetMonitorHistoryForMonitor(ctx context.Context, db *sqlx.DB, monitorSlug string) ([]*models.MonitorHistory, error) { type MonitorHistoryWithMonitor struct {
*models.MonitorHistory
MonitorName string `db:"monitor_name"`
MonitorId string `db:"monitor_id"`
}
func GetLastNMonitorHistory(ctx context.Context, db *sqlx.DB, n int) ([]*MonitorHistoryWithMonitor, error) {
var monitorHistory []*MonitorHistoryWithMonitor
err := db.SelectContext(ctx, &monitorHistory, `
SELECT
mh.*,
wg.name AS worker_group_name,
m.name AS monitor_name,
m.id AS monitor_id
FROM monitor_histories mh
LEFT JOIN worker_groups wg ON mh.worker_group_id = wg.id
LEFT JOIN monitor_worker_groups mwg ON mh.monitor_id = mwg.monitor_id
LEFT JOIN monitors m ON mwg.monitor_id = m.id
ORDER BY mh.created_at DESC
LIMIT $1
`, n)
return monitorHistory, err
}
func GetMonitorHistoryForMonitor(ctx context.Context, db *sqlx.DB, monitorId string) ([]*models.MonitorHistory, error) {
var monitorHistory []*models.MonitorHistory var monitorHistory []*models.MonitorHistory
err := db.SelectContext(ctx, &monitorHistory, err := db.SelectContext(ctx, &monitorHistory, `
"SELECT * FROM monitor_histories WHERE monitor_slug = $1 ORDER BY created_at DESC", SELECT
monitorSlug, mh.*,
) wg.name AS worker_group_name,
wg.id AS worker_group_id
FROM monitor_histories as mh
LEFT JOIN worker_groups wg ON mh.worker_group_id = wg.id
LEFT JOIN monitor_worker_groups mwg ON mh.monitor_id = mwg.monitor_id
WHERE mh.monitor_id = $1
ORDER BY mh.created_at DESC
`, monitorId)
return monitorHistory, err return monitorHistory, err
} }
func AddHistoryForMonitor(ctx context.Context, db *sqlx.DB, history *models.MonitorHistory) error { func AddHistoryForMonitor(ctx context.Context, db *sqlx.DB, history *models.MonitorHistory) error {
_, err := db.NamedExecContext(ctx, _, err := db.NamedExecContext(ctx,
"INSERT INTO monitor_histories (monitor_slug, status, note) VALUES (:monitor_slug, :status, :note)", `
INSERT INTO monitor_histories (
monitor_id,
worker_group_id,
status,
note
) VALUES (
:monitor_id,
:worker_group_id,
:status,
:note
)`,
history, history,
) )
return err return err

View file

@ -10,8 +10,14 @@ import (
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
) )
func GetActiveWorkers(ctx context.Context, workerGroupSlug string, temporal client.Client) ([]string, error) { func CountWorkerGroups(ctx context.Context, db *sqlx.DB) (int, error) {
response, err := temporal.DescribeTaskQueue(ctx, workerGroupSlug, enums.TASK_QUEUE_TYPE_ACTIVITY) var count int
err := db.GetContext(ctx, &count, "SELECT COUNT(*) FROM worker_groups")
return count, err
}
func GetActiveWorkers(ctx context.Context, workerGroupId string, temporal client.Client) ([]string, error) {
response, err := temporal.DescribeTaskQueue(ctx, workerGroupId, enums.TASK_QUEUE_TYPE_ACTIVITY)
if err != nil { if err != nil {
return make([]string, 0), err return make([]string, 0), err
} }
@ -26,16 +32,16 @@ func GetActiveWorkers(ctx context.Context, workerGroupSlug string, temporal clie
func CreateWorkerGroup(ctx context.Context, db *sqlx.DB, workerGroup *models.WorkerGroup) error { func CreateWorkerGroup(ctx context.Context, db *sqlx.DB, workerGroup *models.WorkerGroup) error {
_, err := db.NamedExecContext(ctx, _, err := db.NamedExecContext(ctx,
"INSERT INTO worker_groups (slug, name) VALUES (:slug, :name)", "INSERT INTO worker_groups (id, name) VALUES (:id, :name)",
workerGroup, workerGroup,
) )
return err return err
} }
func DeleteWorkerGroup(ctx context.Context, db *sqlx.DB, slug string) error { func DeleteWorkerGroup(ctx context.Context, db *sqlx.DB, id string) error {
_, err := db.ExecContext(ctx, _, err := db.ExecContext(ctx,
"UPDATE worker_groups SET deleted_at = datetime('now') WHERE slug = $1", "DELETE FROM worker_groups WHERE id = $1",
slug, id,
) )
return err return err
} }
@ -43,7 +49,7 @@ func DeleteWorkerGroup(ctx context.Context, db *sqlx.DB, slug string) error {
func GetWorkerGroups(ctx context.Context, db *sqlx.DB) ([]*models.WorkerGroup, error) { func GetWorkerGroups(ctx context.Context, db *sqlx.DB) ([]*models.WorkerGroup, error) {
var workerGroups []*models.WorkerGroup var workerGroups []*models.WorkerGroup
err := db.SelectContext(ctx, &workerGroups, err := db.SelectContext(ctx, &workerGroups,
"SELECT * FROM worker_groups WHERE deleted_at IS NULL ORDER BY name", "SELECT * FROM worker_groups ORDER BY name",
) )
return workerGroups, err return workerGroups, err
} }
@ -52,16 +58,14 @@ func GetWorkerGroupsWithMonitors(ctx context.Context, db *sqlx.DB) ([]*models.Wo
rows, err := db.QueryContext(ctx, rows, err := db.QueryContext(ctx,
` `
SELECT SELECT
worker_groups.slug, worker_groups.id,
worker_groups.name, worker_groups.name,
worker_groups.created_at, worker_groups.created_at,
worker_groups.updated_at, worker_groups.updated_at,
worker_groups.deleted_at,
monitors.name as monitor_name monitors.name as monitor_name
FROM worker_groups FROM worker_groups
LEFT OUTER JOIN monitor_worker_groups ON worker_groups.slug = monitor_worker_groups.worker_group_slug LEFT OUTER JOIN monitor_worker_groups ON worker_groups.id = monitor_worker_groups.worker_group_id
LEFT OUTER JOIN monitors ON monitor_worker_groups.monitor_slug = monitors.slug LEFT OUTER JOIN monitors ON monitor_worker_groups.monitor_id = monitors.id
WHERE worker_groups.deleted_at IS NULL AND monitors.deleted_at IS NULL
ORDER BY worker_groups.name ORDER BY worker_groups.name
`) `)
if err != nil { if err != nil {
@ -76,11 +80,10 @@ ORDER BY worker_groups.name
var monitorName *string var monitorName *string
err = rows.Scan( err = rows.Scan(
&workerGroup.Slug, &workerGroup.Id,
&workerGroup.Name, &workerGroup.Name,
&workerGroup.CreatedAt, &workerGroup.CreatedAt,
&workerGroup.UpdatedAt, &workerGroup.UpdatedAt,
&workerGroup.DeletedAt,
&monitorName, &monitorName,
) )
if err != nil { if err != nil {
@ -89,52 +92,51 @@ ORDER BY worker_groups.name
if monitorName != nil { if monitorName != nil {
monitors := []string{} monitors := []string{}
if workerGroups[workerGroup.Slug] != nil { if workerGroups[workerGroup.Id] != nil {
monitors = workerGroups[workerGroup.Slug].Monitors monitors = workerGroups[workerGroup.Id].Monitors
} }
workerGroup.Monitors = append(monitors, *monitorName) workerGroup.Monitors = append(monitors, *monitorName)
} }
workerGroups[workerGroup.Slug] = workerGroup workerGroups[workerGroup.Id] = workerGroup
} }
return maps.Values(workerGroups), err return maps.Values(workerGroups), err
} }
func GetWorkerGroupsBySlug(ctx context.Context, db *sqlx.DB, slugs []string) ([]*models.WorkerGroup, error) { func GetWorkerGroupsById(ctx context.Context, db *sqlx.DB, ids []string) ([]*models.WorkerGroup, error) {
var workerGroups []*models.WorkerGroup var workerGroups []*models.WorkerGroup
err := db.SelectContext(ctx, &workerGroups, err := db.SelectContext(ctx, &workerGroups,
"SELECT * FROM worker_groups WHERE slug = ANY($1) AND deleted_at IS NULL", "SELECT * FROM worker_groups WHERE id = ANY($1)",
slugs, ids,
) )
return workerGroups, err return workerGroups, err
} }
func GetWorkerGroup(ctx context.Context, db *sqlx.DB, slug string) (*models.WorkerGroup, error) { func GetWorkerGroup(ctx context.Context, db *sqlx.DB, id string) (*models.WorkerGroup, error) {
var workerGroup models.WorkerGroup var workerGroup models.WorkerGroup
err := db.GetContext(ctx, &workerGroup, err := db.GetContext(ctx, &workerGroup,
"SELECT * FROM worker_groups WHERE slug = $1 AND deleted_at IS NULL", "SELECT * FROM worker_groups WHERE id = $1",
slug, id,
) )
return &workerGroup, err return &workerGroup, err
} }
func GetWorkerGroupWithMonitors(ctx context.Context, db *sqlx.DB, slug string) (*models.WorkerGroupWithMonitors, error) { func GetWorkerGroupWithMonitors(ctx context.Context, db *sqlx.DB, id string) (*models.WorkerGroupWithMonitors, error) {
rows, err := db.QueryContext(ctx, rows, err := db.QueryContext(ctx,
` `
SELECT SELECT
worker_groups.slug, worker_groups.id,
worker_groups.name, worker_groups.name,
worker_groups.created_at, worker_groups.created_at,
worker_groups.updated_at, worker_groups.updated_at,
worker_groups.deleted_at,
monitors.name as monitor_name monitors.name as monitor_name
FROM worker_groups FROM worker_groups
LEFT OUTER JOIN monitor_worker_groups ON worker_groups.slug = monitor_worker_groups.worker_group_slug LEFT OUTER JOIN monitor_worker_groups ON worker_groups.id = monitor_worker_groups.worker_group_id
LEFT OUTER JOIN monitors ON monitor_worker_groups.monitor_slug = monitors.slug LEFT OUTER JOIN monitors ON monitor_worker_groups.monitor_id = monitors.id
WHERE worker_groups.slug=$1 AND worker_groups.deleted_at IS NULL AND monitors.deleted_at IS NULL WHERE worker_groups.id=$1
`, `,
slug, id,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -146,11 +148,10 @@ WHERE worker_groups.slug=$1 AND worker_groups.deleted_at IS NULL AND monitors.de
for rows.Next() { for rows.Next() {
var monitorName *string var monitorName *string
err = rows.Scan( err = rows.Scan(
&workerGroup.Slug, &workerGroup.Id,
&workerGroup.Name, &workerGroup.Name,
&workerGroup.CreatedAt, &workerGroup.CreatedAt,
&workerGroup.UpdatedAt, &workerGroup.UpdatedAt,
&workerGroup.DeletedAt,
&monitorName, &monitorName,
) )
if err != nil { if err != nil {

View file

@ -11,18 +11,18 @@ import (
type MonitorWorkflowParam struct { type MonitorWorkflowParam struct {
Script string Script string
Slug string MonitorId string
WorkerGroups []string WorkerGroupIds []string
} }
func (w *Workflows) MonitorWorkflowDefinition(ctx workflow.Context, param MonitorWorkflowParam) error { func (w *Workflows) MonitorWorkflowDefinition(ctx workflow.Context, param MonitorWorkflowParam) (models.MonitorStatus, error) {
workerGroups := param.WorkerGroups workerGroupIds := param.WorkerGroupIds
sort.Strings(workerGroups) sort.Strings(workerGroupIds)
for _, workerGroup := range workerGroups { for _, workerGroupId := range workerGroupIds {
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 60 * time.Second, StartToCloseTimeout: 60 * time.Second,
TaskQueue: workerGroup, TaskQueue: workerGroupId,
}) })
heatlcheckParam := activities.HealtcheckParam{ heatlcheckParam := activities.HealtcheckParam{
@ -32,7 +32,7 @@ func (w *Workflows) MonitorWorkflowDefinition(ctx workflow.Context, param Monito
var monitorResult *activities.MonitorResult var monitorResult *activities.MonitorResult
err := workflow.ExecuteActivity(ctx, w.activities.Monitor, heatlcheckParam).Get(ctx, &monitorResult) err := workflow.ExecuteActivity(ctx, w.activities.Monitor, heatlcheckParam).Get(ctx, &monitorResult)
if err != nil { if err != nil {
return err return models.MonitorUnknown, err
} }
status := models.MonitorFailure status := models.MonitorFailure
@ -41,18 +41,18 @@ func (w *Workflows) MonitorWorkflowDefinition(ctx workflow.Context, param Monito
} }
historyParam := activities.HealtcheckAddToHistoryParam{ historyParam := activities.HealtcheckAddToHistoryParam{
Slug: param.Slug, MonitorId: param.MonitorId,
Status: status, Status: status,
Note: monitorResult.Note, Note: monitorResult.Note,
WorkerGroup: workerGroup, WorkerGroupId: workerGroupId,
} }
var historyResult *activities.MonitorAddToHistoryResult var historyResult *activities.MonitorAddToHistoryResult
err = workflow.ExecuteActivity(ctx, w.activities.MonitorAddToHistory, historyParam).Get(ctx, &historyResult) err = workflow.ExecuteActivity(ctx, w.activities.MonitorAddToHistory, historyParam).Get(ctx, &historyResult)
if err != nil { if err != nil {
return err return models.MonitorUnknown, err
} }
} }
return nil return models.MonitorSuccess, nil
} }

View file

@ -1,7 +1,9 @@
package api package api
import "code.tjo.space/mentos1386/zdravko/database/models"
type ApiV1MonitorsHistoryPOSTBody struct { type ApiV1MonitorsHistoryPOSTBody struct {
Status string `json:"status"` Status models.MonitorStatus `json:"status"`
Note string `json:"note"` Note string `json:"note"`
WorkerGroup string `json:"worker_group"` WorkerGroupId string `json:"worker_group"`
} }

View file

@ -48,16 +48,16 @@ func Routes(
settings.GET("/monitors", h.SettingsMonitorsGET) settings.GET("/monitors", h.SettingsMonitorsGET)
settings.GET("/monitors/create", h.SettingsMonitorsCreateGET) settings.GET("/monitors/create", h.SettingsMonitorsCreateGET)
settings.POST("/monitors/create", h.SettingsMonitorsCreatePOST) settings.POST("/monitors/create", h.SettingsMonitorsCreatePOST)
settings.GET("/monitors/:slug", h.SettingsMonitorsDescribeGET) settings.GET("/monitors/:id", h.SettingsMonitorsDescribeGET)
settings.POST("/monitors/:slug", h.SettingsMonitorsDescribePOST) settings.POST("/monitors/:id", h.SettingsMonitorsDescribePOST)
settings.GET("/monitors/:slug/delete", h.SettingsMonitorsDescribeDELETE) settings.GET("/monitors/:id/delete", h.SettingsMonitorsDescribeDELETE)
settings.GET("/monitors/:slug/disable", h.SettingsMonitorsDisableGET) settings.GET("/monitors/:id/disable", h.SettingsMonitorsDisableGET)
settings.GET("/monitors/:slug/enable", h.SettingsMonitorsEnableGET) settings.GET("/monitors/:id/enable", h.SettingsMonitorsEnableGET)
settings.GET("/worker-groups", h.SettingsWorkerGroupsGET) settings.GET("/worker-groups", h.SettingsWorkerGroupsGET)
settings.GET("/worker-groups/create", h.SettingsWorkerGroupsCreateGET) settings.GET("/worker-groups/create", h.SettingsWorkerGroupsCreateGET)
settings.POST("/worker-groups/create", h.SettingsWorkerGroupsCreatePOST) settings.POST("/worker-groups/create", h.SettingsWorkerGroupsCreatePOST)
settings.GET("/worker-groups/:slug", h.SettingsWorkerGroupsDescribeGET) settings.GET("/worker-groups/:id", h.SettingsWorkerGroupsDescribeGET)
settings.GET("/worker-groups/:slug/delete", h.SettingsWorkerGroupsDescribeDELETE) settings.GET("/worker-groups/:id/delete", h.SettingsWorkerGroupsDescribeDELETE)
settings.Match([]string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"}, "/temporal*", h.Temporal) settings.Match([]string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"}, "/temporal*", h.Temporal)
@ -71,7 +71,7 @@ func Routes(
apiv1 := e.Group("/api/v1") apiv1 := e.Group("/api/v1")
apiv1.Use(h.Authenticated) apiv1.Use(h.Authenticated)
apiv1.GET("/workers/connect", h.ApiV1WorkersConnectGET) apiv1.GET("/workers/connect", h.ApiV1WorkersConnectGET)
apiv1.POST("/monitors/:slug/history", h.ApiV1MonitorsHistoryPOST) apiv1.POST("/monitors/:id/history", h.ApiV1MonitorsHistoryPOST)
// Error handler // Error handler
e.HTTPErrorHandler = func(err error, c echo.Context) { e.HTTPErrorHandler = func(err error, c echo.Context) {

View file

@ -1677,14 +1677,6 @@ code {
} }
@media (min-width: 640px) { @media (min-width: 640px) {
.sm\:ml-2 {
margin-left: 0.5rem;
}
.sm\:mt-0 {
margin-top: 0px;
}
.sm\:w-auto { .sm\:w-auto {
width: auto; width: auto;
} }

View file

@ -1,39 +1,3 @@
{{ 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" }} {{ define "main" }}
<div class="container max-w-screen-md flex flex-col mt-20"> <div class="container max-w-screen-md flex flex-col mt-20">
{{ if eq .MonitorsLength 0 }} {{ if eq .MonitorsLength 0 }}
@ -136,11 +100,32 @@
{{ end }} {{ end }}
<p>{{ .Name }}</p> <p>{{ .Name }}</p>
</div> </div>
{{ if eq $.TimeRange "90days" }} <div class="justify-self-end text-sm">
{{ template "daily" . }} {{ .History.Uptime }}% uptime
</div>
<div
class="grid gap-px col-span-2 grid-flow-col h-8 rounded overflow-hidden"
>
{{ range .History.List }}
{{ 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 }} {{ else }}
{{ template "hourly" . }} <div class="bg-gray-400 hover:bg-gray-500 flex-auto"></div>
{{ end }} {{ end }}
{{ end }}
</div>
<div class="text-slate-500 justify-self-start text-sm">
{{ if eq $.TimeRange "90days" }}
90 days ago
{{ else if eq $.TimeRange "48hours" }}
48 hours ago
{{ else if eq $.TimeRange "90minutes" }}
90 minutes ago
{{ end }}
</div>
<div class="text-slate-500 justify-self-end text-sm">Now</div>
</div> </div>
{{ end }} {{ end }}
</div> </div>

View file

@ -96,9 +96,7 @@
{{ .Schedule }} {{ .Schedule }}
</td> </td>
<td> <td>
<a href="/settings/monitors/{{ .Slug }}" class="link" <a href="/settings/monitors/{{ .Id }}" class="link">Details</a>
>Details</a
>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View file

@ -1,6 +1,6 @@
{{ define "settings" }} {{ define "settings" }}
<section class="p-5"> <section class="p-5">
<form action="/settings/monitors/{{ .Monitor.Slug }}" method="post"> <form action="/settings/monitors/{{ .Monitor.Id }}" method="post">
<h2>Configuration</h2> <h2>Configuration</h2>
<label for="workergroups">Worker Groups</label> <label for="workergroups">Worker Groups</label>
<input <input
@ -70,13 +70,13 @@
{{ if eq .Monitor.Status "ACTIVE" }} {{ if eq .Monitor.Status "ACTIVE" }}
<a <a
class="block text-center py-2.5 px-5 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100" class="block text-center py-2.5 px-5 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100"
href="/settings/monitors/{{ .Monitor.Slug }}/disable" href="/settings/monitors/{{ .Monitor.Id }}/disable"
>Pause</a >Pause</a
> >
{{ else if eq .Monitor.Status "PAUSED" }} {{ else if eq .Monitor.Status "PAUSED" }}
<a <a
class="block text-center py-2.5 px-5 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100" class="block text-center py-2.5 px-5 me-2 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100"
href="/settings/monitors/{{ .Monitor.Slug }}/enable" href="/settings/monitors/{{ .Monitor.Id }}/enable"
>Resume</a >Resume</a
> >
{{ end }} {{ end }}
@ -87,7 +87,7 @@
<p class="text-sm mb-2">Permanently delete this monitor.</p> <p class="text-sm mb-2">Permanently delete this monitor.</p>
<a <a
class="block text-center focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2" class="block text-center focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2"
href="/settings/monitors/{{ .Monitor.Slug }}/delete" href="/settings/monitors/{{ .Monitor.Id }}/delete"
>Delete</a >Delete</a
> >
</section> </section>
@ -102,6 +102,7 @@
<thead> <thead>
<tr> <tr>
<th>Status</th> <th>Status</th>
<th>Worker Group</th>
<th>Created At</th> <th>Created At</th>
<th>Duration</th> <th>Duration</th>
<th>Note</th> <th>Note</th>
@ -122,7 +123,14 @@
</span> </span>
</td> </td>
<td> <td>
{{ .CreatedAt.Format "2006-01-02 15:04:05" }} <span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
{{ .WorkerGroupName }}
</span>
</td>
<td>
{{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }}
</td> </td>
<td>{ .Duration }</td> <td>{ .Duration }</td>
<td class="whitespace-normal"> <td class="whitespace-normal">

View file

@ -6,33 +6,90 @@
Hi there, {{ .User.Email }}. Hi there, {{ .User.Email }}.
</h1> </h1>
<p class="mb-8 text-l font-normal text-gray-500 lg:text-l sm:px-8 md:px-40"> <p class="mb-8 text-l font-normal text-gray-500 lg:text-l sm:px-8 md:px-40">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod Welcome to the settings page. Here you can manage your worker groups,
tempor incididunt ut labore et dolore magna aliqua. monitors, and notifications.
</p> </p>
</div> </div>
<div class="mx-auto max-w-screen-xl flex flex-col sm:flex-row gap-4"> <div class="mx-auto max-w-screen-xl flex flex-col sm:flex-row gap-4">
<div <div
class="inline-block bg-white rounded-lg shadow p-5 text-center sm:mt-0 sm:ml-2 sm:text-left" class="inline-block bg-white rounded-lg shadow p-5 text-center sm:text-left"
> >
<h3 class="text-sm leading-6 font-medium text-gray-400">Total Workers</h3> <h3 class="text-sm leading-6 font-medium text-gray-400">
<p class="text-3xl font-bold text-black">42</p> Total Worker Groups
</h3>
<p class="text-3xl font-bold text-black">{{ .WorkerGroupsCount }}</p>
</div> </div>
<div <div
class="inline-block bg-white rounded-lg shadow p-5 text-center sm:mt-0 sm:ml-2 sm:text-left" class="inline-block bg-white rounded-lg shadow p-5 text-center sm:text-left"
> >
<h3 class="text-sm leading-6 font-medium text-gray-400"> <h3 class="text-sm leading-6 font-medium text-gray-400">
Total Monitors Total Monitors
</h3> </h3>
<p class="text-3xl font-bold text-black">42</p> <p class="text-3xl font-bold text-black">{{ .MonitorsCount }}</p>
</div> </div>
<div <div
class="inline-block bg-white rounded-lg shadow p-5 text-center sm:mt-0 sm:ml-2 sm:text-left" class="inline-block bg-white rounded-lg shadow p-5 text-center sm:text-left"
> >
<h3 class="text-sm leading-6 font-medium text-gray-400"> <h3 class="text-sm leading-6 font-medium text-gray-400">
Total Notifications Total Notifications
</h3> </h3>
<p class="text-3xl font-bold text-black">42</p> <p class="text-3xl font-bold text-black">{{ .NotificationsCount }}</p>
</div> </div>
</div> </div>
<section class="mt-4">
<table>
<caption>
Execution History
<p>Last 10 executions for all monitors and worker groups.</p>
</caption>
<thead>
<tr>
<th>Monitor</th>
<th>Worker Group</th>
<th>Status</th>
<th>Executed At</th>
<th>Note</th>
</tr>
</thead>
<tbody>
{{ range .History }}
<tr>
<th>
<a
class="underline hover:text-blue-600"
href="/settings/monitors/{{ .MonitorId }}"
>{{ .MonitorName }}</a
>
</th>
<td>
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
>
{{ .WorkerGroupName }}
</span>
</td>
<td>
<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 }}
bg-red-100 text-red-800
{{ end }}"
>
{{ .Status }}
</span>
</td>
<td>
{{ .CreatedAt.Time.Format "2006-01-02 15:04:05" }}
</td>
<td class="whitespace-normal">
{{ .Note }}
</td>
</tr>
{{ end }}
</tbody>
</table>
</section>
{{ end }} {{ end }}

View file

@ -80,7 +80,7 @@
{{ len .Monitors }} {{ len .Monitors }}
</td> </td>
<td> <td>
<a href="/settings/worker-groups/{{ .Slug }}" class="link" <a href="/settings/worker-groups/{{ .Id }}" class="link"
>Details</a >Details</a
> >
</td> </td>

View file

@ -82,7 +82,7 @@
</p> </p>
<a <a
class="block text-center focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2" class="block text-center focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2"
href="/settings/worker-groups/{{ .Worker.Slug }}/delete" href="/settings/worker-groups/{{ .Worker.Id }}/delete"
>Delete</a >Delete</a
> >
</section> </section>