mirror of
https://github.com/mentos1386/zdravko.git
synced 2024-11-25 00:58:07 +00:00
feat: initial hooks boilerplate and updated screenshots
This commit is contained in:
parent
c51bed6cb5
commit
1ef49c444d
22 changed files with 1101 additions and 77 deletions
11
README.md
11
README.md
|
@ -15,17 +15,20 @@ Golang selfhosted Status/Healthcheck monitoring app.
|
||||||
- Kinda working atm. ~But look if all the data could be stored/fetched from temporal.~
|
- Kinda working atm. ~But look if all the data could be stored/fetched from temporal.~
|
||||||
- [x] Edit/Delete operations for healthchecks and workers.
|
- [x] Edit/Delete operations for healthchecks and workers.
|
||||||
- [ ] CronJob Healthchecks (via webhooks).
|
- [ ] CronJob Healthchecks (via webhooks).
|
||||||
|
- Allow CronJob monitoring by checking that there was an event at expected time.
|
||||||
|
- Allow Heartbeat monitoring to alert when events stop coming.
|
||||||
|
- Allow integration with other services by alerting/transforming when events come.
|
||||||
- [ ] Notifications (webhooks, slack, etc).
|
- [ ] Notifications (webhooks, slack, etc).
|
||||||
- [ ] Incidents (based on script that is triggered by monitors/crobjobs).
|
- [ ] Incidents (based on script that is triggered by monitors/crobjobs).
|
||||||
- [ ] Prepare i18n.
|
- [ ] Prepare i18n.
|
||||||
- [ ] Alpha Version (1H 2024)
|
- [ ] Alpha Version (3Q 2024)
|
||||||
- [ ] ??
|
- [ ] ??
|
||||||
- [ ] Beta Version (2H 2024)
|
- [ ] Beta Version (4Q 2024)
|
||||||
- [ ] ??
|
- [ ] ??
|
||||||
- [ ] Stable Release (2025)
|
- [ ] Stable Release (1H 2025)
|
||||||
|
|
||||||
![Screenshot](docs/screenshot.png)
|
![Screenshot](docs/screenshot.png)
|
||||||
Demo is available at https://zdravko.mnts.dev.
|
Demo is available at https://zdravko.mnts.dev. More screenshots in the [docs folder](docs/).
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
|
|
27
database/models/checks.go
Normal file
27
database/models/checks.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type CheckState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CheckStateActive CheckState = "ACTIVE"
|
||||||
|
CheckStatePaused CheckState = "PAUSED"
|
||||||
|
CheckStateUnknown CheckState = "UNKNOWN"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Check struct {
|
||||||
|
CreatedAt *Time `db:"created_at"`
|
||||||
|
UpdatedAt *Time `db:"updated_at"`
|
||||||
|
|
||||||
|
Id string `db:"id"`
|
||||||
|
Name string `db:"name"`
|
||||||
|
|
||||||
|
Schedule string `db:"schedule"`
|
||||||
|
Script string `db:"script"`
|
||||||
|
Filter string `db:"filter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckWithWorkerGroups struct {
|
||||||
|
Check
|
||||||
|
// List of worker group names
|
||||||
|
WorkerGroups []string
|
||||||
|
}
|
20
database/models/hooks.go
Normal file
20
database/models/hooks.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type HookState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
HookStateActive HookState = "ACTIVE"
|
||||||
|
HookStatePaused HookState = "PAUSED"
|
||||||
|
HookStateUnknown HookState = "UNKNOWN"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Hook struct {
|
||||||
|
CreatedAt *Time `db:"created_at"`
|
||||||
|
UpdatedAt *Time `db:"updated_at"`
|
||||||
|
|
||||||
|
Id string `db:"id"`
|
||||||
|
Name string `db:"name"`
|
||||||
|
|
||||||
|
Schedule string `db:"schedule"`
|
||||||
|
Script string `db:"script"`
|
||||||
|
}
|
|
@ -45,32 +45,6 @@ type OAuth2State struct {
|
||||||
ExpiresAt *Time `db:"expires_at"`
|
ExpiresAt *Time `db:"expires_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CheckState string
|
|
||||||
|
|
||||||
const (
|
|
||||||
CheckStateActive CheckState = "ACTIVE"
|
|
||||||
CheckStatePaused CheckState = "PAUSED"
|
|
||||||
CheckStateUnknown CheckState = "UNKNOWN"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Check struct {
|
|
||||||
CreatedAt *Time `db:"created_at"`
|
|
||||||
UpdatedAt *Time `db:"updated_at"`
|
|
||||||
|
|
||||||
Id string `db:"id"`
|
|
||||||
Name string `db:"name"`
|
|
||||||
|
|
||||||
Schedule string `db:"schedule"`
|
|
||||||
Script string `db:"script"`
|
|
||||||
Filter string `db:"filter"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CheckWithWorkerGroups struct {
|
|
||||||
Check
|
|
||||||
// List of worker group names
|
|
||||||
WorkerGroups []string
|
|
||||||
}
|
|
||||||
|
|
||||||
type WorkerGroup struct {
|
type WorkerGroup struct {
|
||||||
CreatedAt *Time `db:"created_at"`
|
CreatedAt *Time `db:"created_at"`
|
||||||
UpdatedAt *Time `db:"updated_at"`
|
UpdatedAt *Time `db:"updated_at"`
|
||||||
|
@ -102,50 +76,3 @@ type Trigger struct {
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
Script string `db:"script"`
|
Script string `db:"script"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TargetVisibility string
|
|
||||||
|
|
||||||
const (
|
|
||||||
TargetVisibilityPublic TargetVisibility = "PUBLIC"
|
|
||||||
TargetVisibilityPrivate TargetVisibility = "PRIVATE"
|
|
||||||
TargetVisibilityUnknown TargetVisibility = "UNKNOWN"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TargetState string
|
|
||||||
|
|
||||||
const (
|
|
||||||
TargetStateActive TargetState = "ACTIVE"
|
|
||||||
TargetStatePaused TargetState = "PAUSED"
|
|
||||||
TargetStateUnknown TargetState = "UNKNOWN"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Target struct {
|
|
||||||
CreatedAt *Time `db:"created_at"`
|
|
||||||
UpdatedAt *Time `db:"updated_at"`
|
|
||||||
|
|
||||||
Id string `db:"id"`
|
|
||||||
Name string `db:"name"`
|
|
||||||
Group string `db:"group"`
|
|
||||||
Visibility TargetVisibility `db:"visibility"`
|
|
||||||
State TargetState `db:"state"`
|
|
||||||
Metadata string `db:"metadata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TargetStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
TargetStatusSuccess TargetStatus = "SUCCESS"
|
|
||||||
TargetStatusFailure TargetStatus = "FAILURE"
|
|
||||||
TargetStatusUnknown TargetStatus = "UNKNOWN"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TargetHistory struct {
|
|
||||||
CreatedAt *Time `db:"created_at"`
|
|
||||||
|
|
||||||
TargetId string `db:"target_id"`
|
|
||||||
WorkerGroupId string `db:"worker_group_id"`
|
|
||||||
CheckId string `db:"check_id"`
|
|
||||||
|
|
||||||
Status TargetStatus `db:"status"`
|
|
||||||
Note string `db:"note"`
|
|
||||||
}
|
|
||||||
|
|
48
database/models/target.go
Normal file
48
database/models/target.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type TargetVisibility string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TargetVisibilityPublic TargetVisibility = "PUBLIC"
|
||||||
|
TargetVisibilityPrivate TargetVisibility = "PRIVATE"
|
||||||
|
TargetVisibilityUnknown TargetVisibility = "UNKNOWN"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TargetState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TargetStateActive TargetState = "ACTIVE"
|
||||||
|
TargetStatePaused TargetState = "PAUSED"
|
||||||
|
TargetStateUnknown TargetState = "UNKNOWN"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Target struct {
|
||||||
|
CreatedAt *Time `db:"created_at"`
|
||||||
|
UpdatedAt *Time `db:"updated_at"`
|
||||||
|
|
||||||
|
Id string `db:"id"`
|
||||||
|
Name string `db:"name"`
|
||||||
|
Group string `db:"group"`
|
||||||
|
Visibility TargetVisibility `db:"visibility"`
|
||||||
|
State TargetState `db:"state"`
|
||||||
|
Metadata string `db:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TargetStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TargetStatusSuccess TargetStatus = "SUCCESS"
|
||||||
|
TargetStatusFailure TargetStatus = "FAILURE"
|
||||||
|
TargetStatusUnknown TargetStatus = "UNKNOWN"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TargetHistory struct {
|
||||||
|
CreatedAt *Time `db:"created_at"`
|
||||||
|
|
||||||
|
TargetId string `db:"target_id"`
|
||||||
|
WorkerGroupId string `db:"worker_group_id"`
|
||||||
|
CheckId string `db:"check_id"`
|
||||||
|
|
||||||
|
Status TargetStatus `db:"status"`
|
||||||
|
Note string `db:"note"`
|
||||||
|
}
|
30
database/sqlite/migrations/2024-05-29_hooks.sql
Normal file
30
database/sqlite/migrations/2024-05-29_hooks.sql
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
-- +migrate Up
|
||||||
|
CREATE TABLE hooks (
|
||||||
|
id TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
schedule TEXT NOT NULL,
|
||||||
|
script TEXT NOT NULL,
|
||||||
|
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
|
||||||
|
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')),
|
||||||
|
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
CONSTRAINT unique_hooks_name UNIQUE (name)
|
||||||
|
) STRICT;
|
||||||
|
-- +migrate StatementBegin
|
||||||
|
CREATE TRIGGER hooks_updated_timestamp AFTER UPDATE ON hooks BEGIN
|
||||||
|
UPDATE hooks SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') WHERE id = NEW.id;
|
||||||
|
END;
|
||||||
|
-- +migrate StatementEnd
|
||||||
|
|
||||||
|
CREATE TABLE hook_worker_groups (
|
||||||
|
worker_group_id TEXT NOT NULL,
|
||||||
|
hook_id TEXT NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (worker_group_id,hook_id),
|
||||||
|
CONSTRAINT fk_hook_worker_groups_worker_group FOREIGN KEY (worker_group_id) REFERENCES worker_groups(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_hook_worker_groups_hook FOREIGN KEY (hook_id) REFERENCES hooks(id) ON DELETE CASCADE
|
||||||
|
) STRICT;
|
||||||
|
-- +migrate Down
|
||||||
|
DROP TABLE hook_worker_groups;
|
||||||
|
DROP TABLE hooks;
|
BIN
docs/screenshot-checks-describe.png
Normal file
BIN
docs/screenshot-checks-describe.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 988 KiB |
BIN
docs/screenshot-checks.png
Normal file
BIN
docs/screenshot-checks.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 302 KiB |
BIN
docs/screenshot-index.png
Normal file
BIN
docs/screenshot-index.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 326 KiB |
BIN
docs/screenshot-settings.png
Normal file
BIN
docs/screenshot-settings.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 581 KiB |
BIN
docs/screenshot-targets.png
Normal file
BIN
docs/screenshot-targets.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 354 KiB |
Binary file not shown.
Before Width: | Height: | Size: 202 KiB |
|
@ -80,6 +80,9 @@ check: |
|
||||||
http.get(metadata.spec.url);
|
http.get(metadata.spec.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hook: |
|
||||||
|
// TODO: Implement hook example
|
||||||
|
|
||||||
filter: |
|
filter: |
|
||||||
target.metadata.kind == "Http" && target.metadata.spec.url != ""
|
target.metadata.kind == "Http" && target.metadata.spec.url != ""
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ type examples struct {
|
||||||
Filter string `yaml:"filter"`
|
Filter string `yaml:"filter"`
|
||||||
Trigger string `yaml:"trigger"`
|
Trigger string `yaml:"trigger"`
|
||||||
Target string `yaml:"target"`
|
Target string `yaml:"target"`
|
||||||
|
Hook string `yaml:"hook"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var Pages = []*components.Page{
|
var Pages = []*components.Page{
|
||||||
|
@ -69,6 +70,7 @@ func NewBaseHandler(db *sqlx.DB, kvStore database.KeyValueStore, temporal client
|
||||||
examples.Filter = script.EscapeString(examples.Filter)
|
examples.Filter = script.EscapeString(examples.Filter)
|
||||||
examples.Trigger = script.EscapeString(examples.Trigger)
|
examples.Trigger = script.EscapeString(examples.Trigger)
|
||||||
examples.Target = script.EscapeString(examples.Target)
|
examples.Target = script.EscapeString(examples.Target)
|
||||||
|
examples.Hook = script.EscapeString(examples.Hook)
|
||||||
|
|
||||||
return &BaseHandler{
|
return &BaseHandler{
|
||||||
db: db,
|
db: db,
|
||||||
|
|
271
internal/server/handlers/settings_hooks.go
Normal file
271
internal/server/handlers/settings_hooks.go
Normal file
|
@ -0,0 +1,271 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gosimple/slug"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/mentos1386/zdravko/database/models"
|
||||||
|
"github.com/mentos1386/zdravko/internal/server/services"
|
||||||
|
"github.com/mentos1386/zdravko/pkg/script"
|
||||||
|
"github.com/mentos1386/zdravko/web/templates/components"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateHook struct {
|
||||||
|
Name string `validate:"required"`
|
||||||
|
Schedule string `validate:"required,cron"`
|
||||||
|
Script string `validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateHook struct {
|
||||||
|
Schedule string `validate:"required,cron"`
|
||||||
|
Script string `validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HookWithState struct {
|
||||||
|
*models.Hook
|
||||||
|
State models.HookState
|
||||||
|
}
|
||||||
|
|
||||||
|
type SettingsHooks struct {
|
||||||
|
*Settings
|
||||||
|
Hooks []*HookWithState
|
||||||
|
History []struct {
|
||||||
|
CreatedAt time.Time
|
||||||
|
Status string
|
||||||
|
Note string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SettingsHook struct {
|
||||||
|
*Settings
|
||||||
|
Hook *HookWithState
|
||||||
|
History []*services.HookHistory
|
||||||
|
}
|
||||||
|
|
||||||
|
type SettingsHookCreate struct {
|
||||||
|
*Settings
|
||||||
|
ExampleScript string
|
||||||
|
ExampleFilter string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BaseHandler) SettingsHooksGET(c echo.Context) error {
|
||||||
|
cc := c.(AuthenticatedContext)
|
||||||
|
|
||||||
|
hooks, err := services.GetHooks(context.Background(), h.db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hooksWithState := make([]*HookWithState, len(hooks))
|
||||||
|
for i, hook := range hooks {
|
||||||
|
state, err := services.GetHookState(context.Background(), h.temporal, hook.Id)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("Failed to get hook state", "error", err)
|
||||||
|
state = models.HookStateUnknown
|
||||||
|
}
|
||||||
|
hooksWithState[i] = &HookWithState{
|
||||||
|
Hook: hook,
|
||||||
|
State: state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Render(http.StatusOK, "settings_hooks.tmpl", &SettingsHooks{
|
||||||
|
Settings: NewSettings(
|
||||||
|
cc.Principal.User,
|
||||||
|
GetPageByTitle(SettingsPages, "Hooks"),
|
||||||
|
[]*components.Page{GetPageByTitle(SettingsPages, "Hooks")},
|
||||||
|
),
|
||||||
|
Hooks: hooksWithState,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BaseHandler) SettingsHooksDescribeGET(c echo.Context) error {
|
||||||
|
cc := c.(AuthenticatedContext)
|
||||||
|
|
||||||
|
slug := c.Param("id")
|
||||||
|
|
||||||
|
hook, err := services.GetHook(context.Background(), h.db, slug)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := services.GetHookState(context.Background(), h.temporal, hook.Id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hookWithStatus := &HookWithState{
|
||||||
|
Hook: hook,
|
||||||
|
State: status,
|
||||||
|
}
|
||||||
|
|
||||||
|
history, err := services.GetHookHistoryForHook(context.Background(), h.temporal, slug)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
maxElements := 10
|
||||||
|
if len(history) < maxElements {
|
||||||
|
maxElements = len(history)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Render(http.StatusOK, "settings_hooks_describe.tmpl", &SettingsHook{
|
||||||
|
Settings: NewSettings(
|
||||||
|
cc.Principal.User,
|
||||||
|
GetPageByTitle(SettingsPages, "Hooks"),
|
||||||
|
[]*components.Page{
|
||||||
|
GetPageByTitle(SettingsPages, "Hooks"),
|
||||||
|
{
|
||||||
|
Path: fmt.Sprintf("/settings/hooks/%s", slug),
|
||||||
|
Title: "Describe",
|
||||||
|
Breadcrumb: hook.Name,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Hook: hookWithStatus,
|
||||||
|
History: history[:maxElements],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BaseHandler) SettingsHooksDescribeDELETE(c echo.Context) error {
|
||||||
|
slug := c.Param("id")
|
||||||
|
|
||||||
|
err := services.DeleteHook(context.Background(), h.db, slug)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = services.DeleteHookSchedule(context.Background(), h.temporal, slug)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Redirect(http.StatusSeeOther, "/settings/hooks")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BaseHandler) SettingsHooksDisableGET(c echo.Context) error {
|
||||||
|
slug := c.Param("id")
|
||||||
|
|
||||||
|
hook, err := services.GetHook(context.Background(), h.db, slug)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = services.SetHookState(context.Background(), h.temporal, hook.Id, models.HookStatePaused)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/hooks/%s", slug))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BaseHandler) SettingsHooksEnableGET(c echo.Context) error {
|
||||||
|
slug := c.Param("id")
|
||||||
|
|
||||||
|
hook, err := services.GetHook(context.Background(), h.db, slug)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = services.SetHookState(context.Background(), h.temporal, hook.Id, models.HookStateActive)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/hooks/%s", slug))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BaseHandler) SettingsHooksDescribePOST(c echo.Context) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
hookId := c.Param("id")
|
||||||
|
|
||||||
|
update := UpdateHook{
|
||||||
|
Schedule: c.FormValue("schedule"),
|
||||||
|
Script: script.EscapeString(c.FormValue("script")),
|
||||||
|
}
|
||||||
|
err := validator.New(validator.WithRequiredStructEnabled()).Struct(update)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hook, err := services.GetHook(ctx, h.db, hookId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hook.Schedule = update.Schedule
|
||||||
|
hook.Script = update.Script
|
||||||
|
|
||||||
|
err = services.UpdateHook(
|
||||||
|
ctx,
|
||||||
|
h.db,
|
||||||
|
hook,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = services.CreateOrUpdateHookSchedule(ctx, h.temporal, hook)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/hooks/%s", hookId))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BaseHandler) SettingsHooksCreateGET(c echo.Context) error {
|
||||||
|
cc := c.(AuthenticatedContext)
|
||||||
|
|
||||||
|
return c.Render(http.StatusOK, "settings_hooks_create.tmpl", &SettingsHookCreate{
|
||||||
|
Settings: NewSettings(
|
||||||
|
cc.Principal.User,
|
||||||
|
GetPageByTitle(SettingsPages, "Hooks"),
|
||||||
|
[]*components.Page{
|
||||||
|
GetPageByTitle(SettingsPages, "Hooks"),
|
||||||
|
GetPageByTitle(SettingsPages, "Hooks Create"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ExampleScript: h.examples.Hook,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *BaseHandler) SettingsHooksCreatePOST(c echo.Context) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
hookId := slug.Make(c.FormValue("name"))
|
||||||
|
|
||||||
|
create := CreateHook{
|
||||||
|
Name: c.FormValue("name"),
|
||||||
|
Schedule: c.FormValue("schedule"),
|
||||||
|
Script: script.EscapeString(c.FormValue("script")),
|
||||||
|
}
|
||||||
|
err := validator.New(validator.WithRequiredStructEnabled()).Struct(create)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hook := &models.Hook{
|
||||||
|
Name: create.Name,
|
||||||
|
Id: hookId,
|
||||||
|
Schedule: create.Schedule,
|
||||||
|
Script: create.Script,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = services.CreateHook(
|
||||||
|
ctx,
|
||||||
|
h.db,
|
||||||
|
hook,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = services.CreateOrUpdateHookSchedule(ctx, h.temporal, hook)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Redirect(http.StatusSeeOther, "/settings/hooks")
|
||||||
|
}
|
164
internal/server/services/hook.go
Normal file
164
internal/server/services/hook.go
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/mentos1386/zdravko/database/models"
|
||||||
|
internaltemporal "github.com/mentos1386/zdravko/internal/temporal"
|
||||||
|
"go.temporal.io/sdk/client"
|
||||||
|
"go.temporal.io/sdk/temporal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getHookScheduleId(id string) string {
|
||||||
|
return "hook-" + id
|
||||||
|
}
|
||||||
|
|
||||||
|
func CountHooks(ctx context.Context, db *sqlx.DB) (int, error) {
|
||||||
|
var count int
|
||||||
|
err := db.GetContext(ctx, &count, "SELECT COUNT(*) FROM hooks")
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetHookState(ctx context.Context, temporal client.Client, id string) (models.HookState, error) {
|
||||||
|
schedule := temporal.ScheduleClient().GetHandle(ctx, getHookScheduleId(id))
|
||||||
|
|
||||||
|
description, err := schedule.Describe(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return models.HookStateUnknown, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if description.Schedule.State.Paused {
|
||||||
|
return models.HookStatePaused, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return models.HookStateActive, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetHookState(ctx context.Context, temporal client.Client, id string, state models.HookState) error {
|
||||||
|
schedule := temporal.ScheduleClient().GetHandle(ctx, getHookScheduleId(id))
|
||||||
|
|
||||||
|
if state == models.HookStateActive {
|
||||||
|
return schedule.Unpause(ctx, client.ScheduleUnpauseOptions{Note: "Unpaused by user"})
|
||||||
|
}
|
||||||
|
|
||||||
|
if state == models.HookStatePaused {
|
||||||
|
return schedule.Pause(ctx, client.SchedulePauseOptions{Note: "Paused by user"})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateHook(ctx context.Context, db *sqlx.DB, hook *models.Hook) error {
|
||||||
|
_, err := db.NamedExecContext(ctx,
|
||||||
|
`INSERT INTO hooks (id, name, script, schedule)
|
||||||
|
VALUES (:id, :name, :script, :schedule)`,
|
||||||
|
hook,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateHook(ctx context.Context, db *sqlx.DB, hook *models.Hook) error {
|
||||||
|
_, err := db.NamedExecContext(ctx,
|
||||||
|
`UPDATE hooks SET script=:script, schedule=:schedule WHERE id=:id`,
|
||||||
|
hook,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteHook(ctx context.Context, db *sqlx.DB, id string) error {
|
||||||
|
_, err := db.ExecContext(ctx,
|
||||||
|
"DELETE FROM hooks WHERE id=$1",
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetHook(ctx context.Context, db *sqlx.DB, id string) (*models.Hook, error) {
|
||||||
|
hook := &models.Hook{}
|
||||||
|
err := db.GetContext(ctx, hook,
|
||||||
|
"SELECT * FROM hooks WHERE id=$1",
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
return hook, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetHooks(ctx context.Context, db *sqlx.DB) ([]*models.Hook, error) {
|
||||||
|
hooks := []*models.Hook{}
|
||||||
|
err := db.SelectContext(ctx, &hooks,
|
||||||
|
"SELECT * FROM hooks ORDER BY name",
|
||||||
|
)
|
||||||
|
return hooks, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteHookSchedule(ctx context.Context, t client.Client, id string) error {
|
||||||
|
schedule := t.ScheduleClient().GetHandle(ctx, getHookScheduleId(id))
|
||||||
|
return schedule.Delete(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateOrUpdateHookSchedule(
|
||||||
|
ctx context.Context,
|
||||||
|
t client.Client,
|
||||||
|
hook *models.Hook,
|
||||||
|
) error {
|
||||||
|
log.Println("Creating or Updating Hook Schedule")
|
||||||
|
|
||||||
|
args := make([]interface{}, 1)
|
||||||
|
args[0] = internaltemporal.WorkflowHookParam{
|
||||||
|
Script: hook.Script,
|
||||||
|
HookId: hook.Id,
|
||||||
|
}
|
||||||
|
|
||||||
|
options := client.ScheduleOptions{
|
||||||
|
ID: getHookScheduleId(hook.Id),
|
||||||
|
Spec: client.ScheduleSpec{
|
||||||
|
CronExpressions: []string{hook.Schedule},
|
||||||
|
Jitter: time.Second * 10,
|
||||||
|
},
|
||||||
|
Action: &client.ScheduleWorkflowAction{
|
||||||
|
ID: getHookScheduleId(hook.Id),
|
||||||
|
Workflow: internaltemporal.WorkflowHookName,
|
||||||
|
Args: args,
|
||||||
|
TaskQueue: internaltemporal.TEMPORAL_SERVER_QUEUE,
|
||||||
|
RetryPolicy: &temporal.RetryPolicy{
|
||||||
|
MaximumAttempts: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule := t.ScheduleClient().GetHandle(ctx, getHookScheduleId(hook.Id))
|
||||||
|
|
||||||
|
// If exists, we update
|
||||||
|
_, err := schedule.Describe(ctx)
|
||||||
|
if err == nil {
|
||||||
|
err = schedule.Update(ctx, client.ScheduleUpdateOptions{
|
||||||
|
DoUpdate: func(input client.ScheduleUpdateInput) (*client.ScheduleUpdate, error) {
|
||||||
|
return &client.ScheduleUpdate{
|
||||||
|
Schedule: &client.Schedule{
|
||||||
|
Spec: &options.Spec,
|
||||||
|
Action: options.Action,
|
||||||
|
Policy: input.Description.Schedule.Policy,
|
||||||
|
State: input.Description.Schedule.State,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
schedule, err = t.ScheduleClient().Create(ctx, options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = schedule.Trigger(ctx, client.ScheduleTriggerOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
105
internal/server/services/hook_history.go
Normal file
105
internal/server/services/hook_history.go
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/mentos1386/zdravko/internal/temporal"
|
||||||
|
"go.temporal.io/api/enums/v1"
|
||||||
|
"go.temporal.io/api/workflowservice/v1"
|
||||||
|
"go.temporal.io/sdk/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HookHistory struct {
|
||||||
|
HookId string
|
||||||
|
Status string
|
||||||
|
Duration time.Duration
|
||||||
|
StartTime time.Time
|
||||||
|
EndTime time.Time
|
||||||
|
WorkerGroupName string
|
||||||
|
Note string
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLastNHookHistory(ctx context.Context, t client.Client, n int32) ([]*HookHistory, error) {
|
||||||
|
var hookHistory []*HookHistory
|
||||||
|
|
||||||
|
response, err := t.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
|
||||||
|
PageSize: n,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return hookHistory, err
|
||||||
|
}
|
||||||
|
|
||||||
|
executions := response.GetExecutions()
|
||||||
|
|
||||||
|
for _, execution := range executions {
|
||||||
|
scheduleId := string(execution.GetSearchAttributes().GetIndexedFields()["TemporalScheduledById"].Data)
|
||||||
|
|
||||||
|
// Remove the quotes around the hookId and the prefix.
|
||||||
|
hookId := scheduleId[len("\"hook-") : len(scheduleId)-1]
|
||||||
|
|
||||||
|
var result temporal.WorkflowHookResult
|
||||||
|
if execution.Status != enums.WORKFLOW_EXECUTION_STATUS_RUNNING {
|
||||||
|
workflowRun := t.GetWorkflow(ctx, execution.GetExecution().GetWorkflowId(), execution.GetExecution().GetRunId())
|
||||||
|
err := workflowRun.Get(ctx, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hookHistory = append(hookHistory, &HookHistory{
|
||||||
|
HookId: hookId,
|
||||||
|
Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()),
|
||||||
|
StartTime: execution.StartTime.AsTime(),
|
||||||
|
EndTime: execution.CloseTime.AsTime(),
|
||||||
|
Status: execution.Status.String(),
|
||||||
|
WorkerGroupName: execution.GetTaskQueue(),
|
||||||
|
Note: result.Note,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return hookHistory, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetHookHistoryForHook(ctx context.Context, t client.Client, hookId string) ([]*HookHistory, error) {
|
||||||
|
var hookHistory []*HookHistory
|
||||||
|
|
||||||
|
response, err := t.ListWorkflow(ctx, &workflowservice.ListWorkflowExecutionsRequest{
|
||||||
|
PageSize: 10,
|
||||||
|
Query: fmt.Sprintf(`TemporalScheduledById = "%s"`, getScheduleId(hookId)),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return hookHistory, err
|
||||||
|
}
|
||||||
|
|
||||||
|
executions := response.GetExecutions()
|
||||||
|
|
||||||
|
for _, execution := range executions {
|
||||||
|
scheduleId := string(execution.GetSearchAttributes().GetIndexedFields()["TemporalScheduledById"].Data)
|
||||||
|
|
||||||
|
// Remove the quotes around the hookId and the prefix.
|
||||||
|
hookId := scheduleId[len("\"hook-") : len(scheduleId)-1]
|
||||||
|
|
||||||
|
var result temporal.WorkflowHookResult
|
||||||
|
if execution.Status != enums.WORKFLOW_EXECUTION_STATUS_RUNNING {
|
||||||
|
workflowRun := t.GetWorkflow(ctx, execution.GetExecution().GetWorkflowId(), execution.GetExecution().GetRunId())
|
||||||
|
err := workflowRun.Get(ctx, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hookHistory = append(hookHistory, &HookHistory{
|
||||||
|
HookId: hookId,
|
||||||
|
Duration: execution.CloseTime.AsTime().Sub(execution.StartTime.AsTime()),
|
||||||
|
StartTime: execution.StartTime.AsTime(),
|
||||||
|
EndTime: execution.CloseTime.AsTime(),
|
||||||
|
Status: execution.Status.String(),
|
||||||
|
WorkerGroupName: execution.GetTaskQueue(),
|
||||||
|
Note: result.Note,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return hookHistory, nil
|
||||||
|
}
|
13
internal/temporal/workflow_hook.go
Normal file
13
internal/temporal/workflow_hook.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package temporal
|
||||||
|
|
||||||
|
type WorkflowHookParam struct {
|
||||||
|
Script string
|
||||||
|
Filter string
|
||||||
|
HookId string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkflowHookResult struct {
|
||||||
|
Note string
|
||||||
|
}
|
||||||
|
|
||||||
|
const WorkflowHookName = "HOOK_WORKFLOW"
|
|
@ -83,6 +83,15 @@ func Routes(
|
||||||
settings.GET("/checks/:id/disable", h.SettingsChecksDisableGET)
|
settings.GET("/checks/:id/disable", h.SettingsChecksDisableGET)
|
||||||
settings.GET("/checks/:id/enable", h.SettingsChecksEnableGET)
|
settings.GET("/checks/:id/enable", h.SettingsChecksEnableGET)
|
||||||
|
|
||||||
|
settings.GET("/hooks", h.SettingsHooksGET)
|
||||||
|
settings.GET("/hooks/create", h.SettingsHooksCreateGET)
|
||||||
|
settings.POST("/hooks/create", h.SettingsHooksCreatePOST)
|
||||||
|
settings.GET("/hooks/:id", h.SettingsHooksDescribeGET)
|
||||||
|
settings.POST("/hooks/:id", h.SettingsHooksDescribePOST)
|
||||||
|
settings.GET("/hooks/:id/delete", h.SettingsHooksDescribeDELETE)
|
||||||
|
settings.GET("/hooks/:id/disable", h.SettingsHooksDisableGET)
|
||||||
|
settings.GET("/hooks/:id/enable", h.SettingsHooksEnableGET)
|
||||||
|
|
||||||
settings.GET("/notifications", h.SettingsNotificationsGET)
|
settings.GET("/notifications", h.SettingsNotificationsGET)
|
||||||
|
|
||||||
settings.GET("/worker-groups", h.SettingsWorkerGroupsGET)
|
settings.GET("/worker-groups", h.SettingsWorkerGroupsGET)
|
||||||
|
|
103
web/templates/pages/settings_hooks.tmpl
Normal file
103
web/templates/pages/settings_hooks.tmpl
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
{{ define "settings" }}
|
||||||
|
{{ $description := "Hooks can be used to monitor <strong>CronJobs</strong> by making sure they execute at specified intervals, for <strong>alert integrations</strong> and <strong>heartbeat monitoring</strong> to notify when requests stop coming for certain time periods." }}
|
||||||
|
|
||||||
|
{{ $length := len .Hooks }}
|
||||||
|
{{ if eq $length 0 }}
|
||||||
|
<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 hooks yet.
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
class="mb-8 text-l font-normal text-gray-700 lg:text-l sm:px-8 lg:px-40"
|
||||||
|
>
|
||||||
|
{{ $description }}
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
||||||
|
<a
|
||||||
|
href="/settings/hooks/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 Hook
|
||||||
|
<svg class="feather ml-1 h-5 w-5 overflow-visible">
|
||||||
|
<use href="/static/icons/feather-sprite.svg#plus" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ else }}
|
||||||
|
<section>
|
||||||
|
<table>
|
||||||
|
<caption>
|
||||||
|
List of Hooks
|
||||||
|
<div class="mt-1 gap-4 grid grid-cols-1 md:grid-cols-[1fr,20%]">
|
||||||
|
<p>
|
||||||
|
{{ $description }}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="/settings/hooks/create"
|
||||||
|
class="h-min inline-flex justify-center items-center py-2 px-4 text-sm font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300"
|
||||||
|
>
|
||||||
|
Create New
|
||||||
|
<svg class="feather h-5 w-5 overflow-visible">
|
||||||
|
<use href="/static/icons/feather-sprite.svg#plus" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</caption>
|
||||||
|
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Name</th>
|
||||||
|
<th scope="col">Targets</th>
|
||||||
|
<th scope="col">State</th>
|
||||||
|
<th scope="col">Schedule</th>
|
||||||
|
<th scope="col">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{ range $hooks := .Hooks }}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
{{ .Name }}
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
|
||||||
|
>3</span
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ if eq .State "ACTIVE" }}
|
||||||
|
<span
|
||||||
|
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
|
||||||
|
>
|
||||||
|
ACTIVE
|
||||||
|
</span>
|
||||||
|
{{ else if eq .State "PAUSED" }}
|
||||||
|
<span
|
||||||
|
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800"
|
||||||
|
>
|
||||||
|
PAUSED
|
||||||
|
</span>
|
||||||
|
{{ else }}
|
||||||
|
<span
|
||||||
|
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
|
||||||
|
>
|
||||||
|
UNKNOWN
|
||||||
|
</span>
|
||||||
|
{{ end }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ .Schedule }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/settings/hooks/{{ .Id }}" class="link">Details</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
95
web/templates/pages/settings_hooks_create.tmpl
Normal file
95
web/templates/pages/settings_hooks_create.tmpl
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
{{ define "settings" }}
|
||||||
|
<section class="p-5">
|
||||||
|
<form action="/settings/hooks/create" method="post">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input type="text" name="name" id="name" placeholder="HTTP GET Request" />
|
||||||
|
<p>Name of the hook can be anything.</p>
|
||||||
|
<label for="workergroups">Worker Groups</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="workergroups"
|
||||||
|
id="workergroups"
|
||||||
|
placeholder="europe asia"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
Worker groups are used to distribute the hook to specific workers. Space
|
||||||
|
is a separator between groups.
|
||||||
|
</p>
|
||||||
|
<label for="schedule">Schedule</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="schedule"
|
||||||
|
id="schedule"
|
||||||
|
placeholder="@every 1m"
|
||||||
|
value="@every 1m"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
Schedule is a cron expression that defines when the hook should be
|
||||||
|
executed.
|
||||||
|
<br />
|
||||||
|
You can also use <code>@every [interval]</code> where interval is a
|
||||||
|
duration like 5m, 1h, 60s. Or use <code>@hourly</code>,
|
||||||
|
<code>@daily</code>, <code>@weekly</code>, <code>@monthly</code>,
|
||||||
|
<code>@yearly</code>.
|
||||||
|
</p>
|
||||||
|
<label for="script">Script</label>
|
||||||
|
<textarea required id="script" name="script" class="sm:col-span-2 h-96">
|
||||||
|
{{ ScriptUnescapeString .ExampleScript }}</textarea
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="editor-script"
|
||||||
|
class="hidden sm:col-span-2 block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
|
||||||
|
></div>
|
||||||
|
<p class="sm:col-span-2">
|
||||||
|
Script is what determines the status of a service. You can read more
|
||||||
|
about it on
|
||||||
|
<a target="_blank" href="https://k6.io/docs/using-k6/http-requests/"
|
||||||
|
>k6 documentation</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
<button type="submit" onclick="save()">Create</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="/static/monaco/vs/loader.js"></script>
|
||||||
|
<script>
|
||||||
|
const items = [{ name: "script", language: "javascript" }];
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
for (const { name } of items) {
|
||||||
|
const elem = window.editors[name].getValue();
|
||||||
|
document.getElementById(name).value = elem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.editors = {};
|
||||||
|
for (const { name, language, options = {} } of items) {
|
||||||
|
const textarea = document.getElementById(name);
|
||||||
|
const editor = document.getElementById("editor-" + name);
|
||||||
|
|
||||||
|
editor.classList.remove("hidden");
|
||||||
|
textarea.hidden = true;
|
||||||
|
|
||||||
|
require.config({ paths: { vs: "/static/monaco/vs" } });
|
||||||
|
require(["vs/editor/editor.main"], function () {
|
||||||
|
window.editors[name] = monaco.editor.create(editor, {
|
||||||
|
value: textarea.value,
|
||||||
|
language: language,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
codeLens: false,
|
||||||
|
contextmenu: false,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
wordWrap: "on",
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
window.editors[name].layout();
|
||||||
|
});
|
||||||
|
resizeObserver.observe(editor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{{ end }}
|
204
web/templates/pages/settings_hooks_describe.tmpl
Normal file
204
web/templates/pages/settings_hooks_describe.tmpl
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
{{ define "settings" }}
|
||||||
|
<section class="p-5">
|
||||||
|
<form action="/settings/hooks/{{ .Hook.Id }}" method="post">
|
||||||
|
<h2>Configuration</h2>
|
||||||
|
<label for="workergroups">Worker Groups</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="workergroups"
|
||||||
|
id="workergroups"
|
||||||
|
value="{{ range .Hook.WorkerGroups }}{{ . }}{{ end }}"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p>Worker groups are used to distribute the hook to specific workers.</p>
|
||||||
|
<label for="schedule">Schedule</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="schedule"
|
||||||
|
id="schedule"
|
||||||
|
value="{{ .Hook.Schedule }}"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
Schedule is a cron expression that defines when the hook should be
|
||||||
|
executed.
|
||||||
|
<br />
|
||||||
|
You can also use <code>@every [interval]</code> where interval is a
|
||||||
|
duration like 5m, 1h, 60s. Or use <code>@hourly</code>,
|
||||||
|
<code>@daily</code>, <code>@weekly</code>, <code>@monthly</code>,
|
||||||
|
<code>@yearly</code>.
|
||||||
|
</p>
|
||||||
|
<label for="script">Script</label>
|
||||||
|
<textarea required id="script" name="script" class="sm:col-span-2 h-96">
|
||||||
|
{{ ScriptUnescapeString .Hook.Script }}</textarea
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="editor-script"
|
||||||
|
class="hidden sm:col-span-2 block w-full h-96 rounded-lg border border-gray-300 overflow-hidden"
|
||||||
|
></div>
|
||||||
|
<p class="sm:col-span-2">
|
||||||
|
Script is what determines the status of a service. You can read more
|
||||||
|
about it on
|
||||||
|
<a target="_blank" href="https://k6.io/docs/using-k6/http-requests/"
|
||||||
|
>k6 documentation</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
<button type="submit" onclick="save()">Save</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="flex md:flex-row flex-col gap-4 h-min">
|
||||||
|
<section class="p-5 flex-1">
|
||||||
|
<h2 class="mb-2 flex flex-row gap-2">
|
||||||
|
State
|
||||||
|
{{ if eq .Hook.State "ACTIVE" }}
|
||||||
|
<span
|
||||||
|
class="self-center h-fit w-fit px-2 text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
|
||||||
|
>
|
||||||
|
ACTIVE
|
||||||
|
</span>
|
||||||
|
{{ else if eq .Hook.State "PAUSED" }}
|
||||||
|
<span
|
||||||
|
class="self-center h-fit w-fit px-2 text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800"
|
||||||
|
>
|
||||||
|
PAUSED
|
||||||
|
</span>
|
||||||
|
{{ end }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm mb-2">
|
||||||
|
Pausing the hook will stop it from executing. This can be useful in
|
||||||
|
cases of expected downtime. Or when the hook is not needed anymore.
|
||||||
|
</p>
|
||||||
|
{{ if eq .Hook.State "ACTIVE" }}
|
||||||
|
<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"
|
||||||
|
href="/settings/hooks/{{ .Hook.Id }}/disable"
|
||||||
|
>Pause</a
|
||||||
|
>
|
||||||
|
{{ else if eq .Hook.State "PAUSED" }}
|
||||||
|
<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"
|
||||||
|
href="/settings/hooks/{{ .Hook.Id }}/enable"
|
||||||
|
>Resume</a
|
||||||
|
>
|
||||||
|
{{ end }}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="p-2 flex-1 border-4 border-red-300">
|
||||||
|
<h2 class="mb-2">Danger Zone</h2>
|
||||||
|
<p class="text-sm mb-2">Permanently delete this hook.</p>
|
||||||
|
<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"
|
||||||
|
href="/settings/hooks/{{ .Hook.Id }}/delete"
|
||||||
|
>Delete</a
|
||||||
|
>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<table>
|
||||||
|
<caption>
|
||||||
|
History
|
||||||
|
<p>Last 10 executions of hook script.</p>
|
||||||
|
</caption>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Hook ID</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Started At</th>
|
||||||
|
<th>Ended At</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
<th>Note</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{ range .History }}
|
||||||
|
{{ if eq .Status "Running" }}
|
||||||
|
<tr>
|
||||||
|
<td>{{ .HookId }}</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800"
|
||||||
|
>
|
||||||
|
{{ .Status }}...
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ .StartTime.Format "2006-01-02 15:04:05" }}</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td class="whitespace-normal"></td>
|
||||||
|
</tr>
|
||||||
|
{{ else }}
|
||||||
|
<tr>
|
||||||
|
<td>{{ .HookId }}</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
|
||||||
|
{{ if eq .Status "Completed" }}
|
||||||
|
bg-purple-100 text-purple-800
|
||||||
|
{{ else }}
|
||||||
|
bg-red-100 text-red-800
|
||||||
|
{{ end }}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ .Status }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<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>{{ .StartTime.Format "2006-01-02 15:04:05" }}</td>
|
||||||
|
<td>{{ .EndTime.Format "2006-01-02 15:04:05" }}</td>
|
||||||
|
<td>{{ DurationRoundMillisecond .Duration }}</td>
|
||||||
|
<td class="whitespace-normal">{{ .Note }}</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="/static/monaco/vs/loader.js"></script>
|
||||||
|
<script>
|
||||||
|
const items = [{ name: "script", language: "javascript" }];
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
for (const { name } of items) {
|
||||||
|
const elem = window.editors[name].getValue();
|
||||||
|
document.getElementById(name).value = elem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.editors = {};
|
||||||
|
for (const { name, language, options = {} } of items) {
|
||||||
|
const textarea = document.getElementById(name);
|
||||||
|
const editor = document.getElementById("editor-" + name);
|
||||||
|
|
||||||
|
editor.classList.remove("hidden");
|
||||||
|
textarea.hidden = true;
|
||||||
|
|
||||||
|
require.config({ paths: { vs: "/static/monaco/vs" } });
|
||||||
|
require(["vs/editor/editor.main"], function () {
|
||||||
|
window.editors[name] = monaco.editor.create(editor, {
|
||||||
|
value: textarea.value,
|
||||||
|
language: language,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
codeLens: false,
|
||||||
|
contextmenu: false,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
wordWrap: "on",
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver((entries) => {
|
||||||
|
window.editors[name].layout();
|
||||||
|
});
|
||||||
|
resizeObserver.observe(editor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{{ end }}
|
Loading…
Reference in a new issue