diff --git a/internal/handlers/index.go b/internal/handlers/index.go index 0352010..12180e2 100644 --- a/internal/handlers/index.go +++ b/internal/handlers/index.go @@ -13,7 +13,7 @@ import ( type IndexData struct { *components.Base - Monitors map[string][]*Monitor + Monitors map[string]MonitorsAndStatus MonitorsLength int TimeRange string Status models.MonitorStatus @@ -26,12 +26,22 @@ type Monitor struct { History *History } +type HistoryItem struct { + Status models.MonitorStatus + Date time.Time +} + type History struct { - List []models.MonitorStatus + List []HistoryItem Uptime int } -func getHour(date time.Time) string { +type MonitorsAndStatus struct { + Status models.MonitorStatus + Monitors []*Monitor +} + +func getDateString(date time.Time) string { return date.UTC().Format("2006-01-02T15:04:05") } @@ -41,15 +51,15 @@ func getHistory(history []*models.MonitorHistory, period time.Duration, buckets numTotal := 0 for i := 0; i < buckets; i++ { - datetime := getHour(time.Now().Add(period * time.Duration(-i)).Truncate(period)) - historyMap[datetime] = models.MonitorUnknown + dateString := getDateString(time.Now().Add(period * time.Duration(-i)).Truncate(period)) + historyMap[dateString] = models.MonitorUnknown } for _, _history := range history { - hour := getHour(_history.CreatedAt.Time.Truncate(period)) + dateString := getDateString(_history.CreatedAt.Time.Truncate(period)) // Skip if not part of the "buckets" - if _, ok := historyMap[hour]; !ok { + if _, ok := historyMap[dateString]; !ok { continue } @@ -59,17 +69,23 @@ func getHistory(history []*models.MonitorHistory, period time.Duration, buckets } // skip if it is already set to failure - if historyMap[hour] == models.MonitorFailure { + if historyMap[dateString] == models.MonitorFailure { continue } - historyMap[hour] = _history.Status + // FIXME: This is wrong! As we can have multiple checks in dateString. + // We should look at only the newest one. + historyMap[dateString] = _history.Status } - historyHourly := make([]models.MonitorStatus, buckets) + historyItems := make([]HistoryItem, buckets) for i := 0; i < buckets; i++ { - datetime := getHour(time.Now().Add(period * time.Duration(-buckets+i+1)).Truncate(period)) - historyHourly[i] = historyMap[datetime] + date := time.Now().Add(period * time.Duration(-buckets+i+1)).Truncate(period) + datestring := getDateString(date) + historyItems[i] = HistoryItem{ + Status: historyMap[datestring], + Date: date, + } } uptime := 0 @@ -78,7 +94,7 @@ func getHistory(history []*models.MonitorHistory, period time.Duration, buckets } return &History{ - List: historyHourly, + List: historyItems, Uptime: uptime, } } @@ -95,7 +111,8 @@ func (h *BaseHandler) Index(c echo.Context) error { timeRange = "90days" } - overallStatus := models.MonitorSuccess + overallStatus := models.MonitorUnknown + statusByGroup := make(map[string]models.MonitorStatus) monitorsWithHistory := make([]*Monitor, len(monitors)) for i, monitor := range monitors { @@ -114,22 +131,38 @@ func (h *BaseHandler) Index(c echo.Context) error { historyResult = getHistory(history, time.Minute, 90) } + if statusByGroup[monitor.Group] == "" { + statusByGroup[monitor.Group] = models.MonitorUnknown + } + status := historyResult.List[len(historyResult.List)-1] - if status != models.MonitorSuccess { - overallStatus = status + if status.Status == models.MonitorSuccess { + if overallStatus == models.MonitorUnknown { + overallStatus = status.Status + } + if statusByGroup[monitor.Group] == models.MonitorUnknown { + statusByGroup[monitor.Group] = status.Status + } + } + if status.Status != models.MonitorSuccess && status.Status != models.MonitorUnknown { + overallStatus = status.Status + statusByGroup[monitor.Group] = status.Status } monitorsWithHistory[i] = &Monitor{ Name: monitor.Name, Group: monitor.Group, - Status: status, + Status: status.Status, History: historyResult, } } - monitorsByGroup := map[string][]*Monitor{} + monitorsByGroup := map[string]MonitorsAndStatus{} for _, monitor := range monitorsWithHistory { - monitorsByGroup[monitor.Group] = append(monitorsByGroup[monitor.Group], monitor) + monitorsByGroup[monitor.Group] = MonitorsAndStatus{ + Status: statusByGroup[monitor.Group], + Monitors: append(monitorsByGroup[monitor.Group].Monitors, monitor), + } } return c.Render(http.StatusOK, "index.tmpl", &IndexData{ diff --git a/internal/handlers/settingsmonitors.go b/internal/handlers/settingsmonitors.go index 1977690..ffcc06f 100644 --- a/internal/handlers/settingsmonitors.go +++ b/internal/handlers/settingsmonitors.go @@ -16,6 +16,21 @@ import ( "github.com/labstack/echo/v4" ) +const example = ` +import http from 'k6/http'; + +export const options = { + thresholds: { + // http errors should be less than 1% + http_req_failed: ['rate<0.01'], + }, +}; + +export default function () { + http.get('https://example.com'); +} +` + type CreateMonitor struct { Name string `validate:"required"` Group string `validate:"required"` @@ -48,6 +63,11 @@ type SettingsMonitor struct { History []*models.MonitorHistory } +type SettingsMonitorCreate struct { + *Settings + Example string +} + func (h *BaseHandler) SettingsMonitorsGET(c echo.Context) error { cc := c.(AuthenticatedContext) @@ -188,8 +208,8 @@ func (h *BaseHandler) SettingsMonitorsDescribePOST(c echo.Context) error { monitorId := c.Param("id") update := UpdateMonitor{ - Group: c.FormValue("group"), - WorkerGroups: strings.TrimSpace(c.FormValue("workergroups")), + Group: strings.ToLower(c.FormValue("group")), + WorkerGroups: strings.ToLower(strings.TrimSpace(c.FormValue("workergroups"))), Schedule: c.FormValue("schedule"), Script: c.FormValue("script"), } @@ -251,14 +271,17 @@ func (h *BaseHandler) SettingsMonitorsDescribePOST(c echo.Context) error { func (h *BaseHandler) SettingsMonitorsCreateGET(c echo.Context) error { cc := c.(AuthenticatedContext) - return c.Render(http.StatusOK, "settings_monitors_create.tmpl", NewSettings( - cc.Principal.User, - GetPageByTitle(SettingsPages, "Monitors"), - []*components.Page{ + return c.Render(http.StatusOK, "settings_monitors_create.tmpl", &SettingsMonitorCreate{ + Settings: NewSettings( + cc.Principal.User, GetPageByTitle(SettingsPages, "Monitors"), - GetPageByTitle(SettingsPages, "Monitors Create"), - }, - )) + []*components.Page{ + GetPageByTitle(SettingsPages, "Monitors"), + GetPageByTitle(SettingsPages, "Monitors Create"), + }, + ), + Example: example, + }) } func (h *BaseHandler) SettingsMonitorsCreatePOST(c echo.Context) error { @@ -267,9 +290,9 @@ func (h *BaseHandler) SettingsMonitorsCreatePOST(c echo.Context) error { create := CreateMonitor{ Name: c.FormValue("name"), - Group: c.FormValue("group"), + Group: strings.ToLower(c.FormValue("group")), + WorkerGroups: strings.ToLower(strings.TrimSpace(c.FormValue("workergroups"))), Schedule: c.FormValue("schedule"), - WorkerGroups: c.FormValue("workergroups"), Script: c.FormValue("script"), } err := validator.New(validator.WithRequiredStructEnabled()).Struct(create) diff --git a/internal/handlers/settingsworkergroups.go b/internal/handlers/settingsworkergroups.go index c7659f9..7d16984 100644 --- a/internal/handlers/settingsworkergroups.go +++ b/internal/handlers/settingsworkergroups.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "strings" "code.tjo.space/mentos1386/zdravko/database/models" "code.tjo.space/mentos1386/zdravko/internal/jwt" @@ -134,7 +135,7 @@ func (h *BaseHandler) SettingsWorkerGroupsCreatePOST(c echo.Context) error { id := slug.Make(c.FormValue("name")) workerGroup := &models.WorkerGroup{ - Name: c.FormValue("name"), + Name: strings.ToLower(c.FormValue("name")), Id: id, } diff --git a/web/static/css/main.css b/web/static/css/main.css index e117c05..dbcbc78 100644 --- a/web/static/css/main.css +++ b/web/static/css/main.css @@ -56,9 +56,6 @@ code { @apply bg-blue-700 text-white; } -.monitors .monitors-list { - @apply grid justify-items-stretch justify-stretch items-center bg-white shadow-md rounded-lg; -} .monitors .time-range > a { @apply font-medium text-sm px-2.5 py-1 rounded-lg; @apply text-black bg-gray-100 hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-400; diff --git a/web/static/css/tailwind.css b/web/static/css/tailwind.css index e8a8d33..daebbfa 100644 --- a/web/static/css/tailwind.css +++ b/web/static/css/tailwind.css @@ -590,6 +590,14 @@ video { } } +.absolute { + position: absolute; +} + +.z-50 { + z-index: 50; +} + .col-span-1 { grid-column: span 1 / span 1; } @@ -598,6 +606,11 @@ video { grid-column: span 2 / span 2; } +.-mx-3 { + margin-left: -0.75rem; + margin-right: -0.75rem; +} + .mx-1 { margin-left: 0.25rem; margin-right: 0.25rem; @@ -613,6 +626,10 @@ video { margin-right: auto; } +.-ml-4 { + margin-left: -1rem; +} + .mb-2 { margin-bottom: 0.5rem; } @@ -641,6 +658,14 @@ video { margin-top: 0.25rem; } +.mt-10 { + margin-top: 2.5rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + .mt-20 { margin-top: 5rem; } @@ -768,6 +793,10 @@ video { flex: 1 1 auto; } +.cursor-pointer { + cursor: pointer; +} + .grid-flow-col { grid-auto-flow: column; } @@ -838,10 +867,6 @@ video { justify-self: end; } -.justify-self-center { - justify-self: center; -} - .overflow-hidden { overflow: hidden; } @@ -882,6 +907,11 @@ video { border-bottom-width: 1px; } +.border-gray-100 { + --tw-border-opacity: 1; + border-color: rgb(243 244 246 / var(--tw-border-opacity)); +} + .border-gray-200 { --tw-border-opacity: 1; border-color: rgb(229 231 235 / var(--tw-border-opacity)); @@ -975,6 +1005,10 @@ video { padding: 0.625rem; } +.p-3 { + padding: 0.75rem; +} + .p-4 { padding: 1rem; } @@ -983,6 +1017,10 @@ video { padding: 1.25rem; } +.p-6 { + padding: 1.5rem; +} + .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; @@ -1023,6 +1061,10 @@ video { padding-bottom: 2rem; } +.pb-2 { + padding-bottom: 0.5rem; +} + .pt-8 { padding-top: 2rem; } @@ -1051,11 +1093,6 @@ video { line-height: 1.25rem; } -.text-xl { - font-size: 1.25rem; - line-height: 1.75rem; -} - .text-xs { font-size: 0.75rem; line-height: 1rem; @@ -1085,6 +1122,10 @@ video { text-transform: uppercase; } +.capitalize { + text-transform: capitalize; +} + .leading-5 { line-height: 1.25rem; } @@ -1185,11 +1226,33 @@ video { box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } +.shadow-lg { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-md { + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + .blur { --tw-blur: blur(8px); filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); } +.transition-all { + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.duration-300 { + transition-duration: 300ms; +} + .link, p > a { font-weight: 500; @@ -1332,19 +1395,6 @@ code { color: rgb(255 255 255 / var(--tw-text-opacity)); } -.monitors .monitors-list { - display: grid; - align-items: center; - justify-content: stretch; - justify-items: stretch; - border-radius: 0.5rem; - --tw-bg-opacity: 1; - background-color: rgb(255 255 255 / var(--tw-bg-opacity)); - --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - .monitors .time-range > a { border-radius: 0.5rem; padding-left: 0.625rem; @@ -1626,6 +1676,19 @@ code { letter-spacing: 0.05em; } +.last-of-type\:border-0:last-of-type { + border-width: 0px; +} + +.last-of-type\:pb-0:last-of-type { + padding-bottom: 0px; +} + +.hover\:bg-blue-50:hover { + --tw-bg-opacity: 1; + background-color: rgb(239 246 255 / var(--tw-bg-opacity)); +} + .hover\:bg-blue-800:hover { --tw-bg-opacity: 1; background-color: rgb(30 64 175 / var(--tw-bg-opacity)); @@ -1723,15 +1786,6 @@ code { margin-bottom: calc(0px * var(--tw-space-y-reverse)); } - .sm\:justify-self-end { - justify-self: end; - } - - .sm\:px-0 { - padding-left: 0px; - padding-right: 0px; - } - .sm\:px-8 { padding-left: 2rem; padding-right: 2rem; @@ -1751,6 +1805,10 @@ code { flex-direction: row; } + .md\:justify-end { + justify-content: flex-end; + } + .md\:px-40 { padding-left: 10rem; padding-right: 10rem; @@ -1799,3 +1857,16 @@ code { line-height: 2.5rem; } } + +.\[\&_\.tooltip\]\:hover\:visible:hover .tooltip { + visibility: visible; +} + +.\[\&_\.tooltip\]\:hover\:flex:hover .tooltip { + display: flex; +} + +.\[\&_svg\]\:open\:-rotate-180[open] svg { + --tw-rotate: -180deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} diff --git a/web/templates/pages/index.tmpl b/web/templates/pages/index.tmpl index 8bdebc7..3ec81ec 100644 --- a/web/templates/pages/index.tmpl +++ b/web/templates/pages/index.tmpl @@ -51,7 +51,7 @@ > -

Degraded performance

+

Degraded performance

Last updated on {{ Now.UTC.Format "Jan 02 at 15:04 MST" }} @@ -59,49 +59,62 @@ {{ end }}

-
-

+ 90 Days - Monitors -

-
- {{ range $group, $monitors := .Monitors }} -
-

- - {{ $group }} - + {{ range $group, $monitorsAndStatus := .Monitors }} +
+ + {{ if eq $monitorsAndStatus.Status "SUCCESS" }} +
+ {{ else if eq $monitorsAndStatus.Status "FAILURE" }} + + {{ else }} + + {{ end }} +

+ {{ $group }} +

+ - - {{ range $monitors }} -
+ + {{ range $monitorsAndStatus.Monitors }} +
{{ if eq .Status "SUCCESS" }} @@ -119,17 +132,47 @@ class="grid gap-px col-span-2 grid-flow-col h-8 rounded overflow-hidden" > {{ range .History.List }} - {{ if eq . "SUCCESS" }} +
+ {{ if eq .Status "SUCCESS" }} +
+ {{ else if eq .Status "FAILURE" }} +
+ {{ else }} +
+ {{ end }}
- {{ else if eq . "FAILURE" }} -
- {{ else }} -
- {{ end }} + 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" + > + {{ if eq .Status "SUCCESS" }} + + {{ else if eq .Status "FAILURE" }} + + {{ else }} + + {{ end }} + {{ 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 }} +
+
{{ end }}
{{ end }} -
+ {{ end }}
{{ end }} diff --git a/web/templates/pages/settings_monitors.tmpl b/web/templates/pages/settings_monitors.tmpl index 9639a1f..a3c059c 100644 --- a/web/templates/pages/settings_monitors.tmpl +++ b/web/templates/pages/settings_monitors.tmpl @@ -50,7 +50,7 @@ - Group + Monitor Group Name Worker Groups Status diff --git a/web/templates/pages/settings_monitors_create.tmpl b/web/templates/pages/settings_monitors_create.tmpl index 5153ae5..fcfc17f 100644 --- a/web/templates/pages/settings_monitors_create.tmpl +++ b/web/templates/pages/settings_monitors_create.tmpl @@ -4,14 +4,18 @@

Name of the monitor can be anything.

- + + + +

Group monitors together. This affects how they are presented on the homepage. @@ -22,6 +26,7 @@ name="workergroups" id="workergroups" placeholder="NA EU" + required />

Worker groups are used to distribute the monitor to specific workers. @@ -33,6 +38,7 @@ id="schedule" placeholder="@every 1m" value="@every 1m" + required />

Schedule is a cron expression that defines when the monitor should be @@ -44,10 +50,12 @@ @yearly.

- +

Script is what determines the status of a service. You can read more @@ -62,25 +70,16 @@