feat: single schedule for all worker groups, simplify tables, show active workers

This commit is contained in:
Tine 2024-02-27 23:47:20 +01:00
parent 7035afe008
commit 306f583418
Signed by: mentos1386
SSH key fingerprint: SHA256:MNtTsLbihYaWF8j1fkOHfkKNlnN1JQfxEU/rBU8nCGw
15 changed files with 400 additions and 229 deletions

View file

@ -36,6 +36,7 @@ type HealtcheckAddToHistoryParam struct {
Slug string Slug string
Status string Status string
Note string Note string
WorkerGroup string
} }
type MonitorAddToHistoryResult struct { type MonitorAddToHistoryResult struct {
@ -47,6 +48,7 @@ func (a *Activities) MonitorAddToHistory(ctx context.Context, param HealtcheckAd
body := api.ApiV1MonitorsHistoryPOSTBody{ body := api.ApiV1MonitorsHistoryPOSTBody{
Status: param.Status, Status: param.Status,
Note: param.Note, Note: param.Note,
WorkerGroup: param.WorkerGroup,
} }
jsonBody, err := json.Marshal(body) jsonBody, err := json.Marshal(body)

View file

@ -14,20 +14,26 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
type WorkerWithToken struct { type WorkerWithTokenAndActiveWorkers struct {
*models.WorkerGroup *models.WorkerGroup
Token string Token string
ActiveWorkers []string
}
type WorkerGroupWithActiveWorkers struct {
*models.WorkerGroupWithMonitors
ActiveWorkers []string
} }
type SettingsWorkerGroups struct { type SettingsWorkerGroups struct {
*Settings *Settings
WorkerGroups []*models.WorkerGroupWithMonitors WorkerGroups []*WorkerGroupWithActiveWorkers
WorkerGroupsLength int WorkerGroupsLength int
} }
type SettingsWorker struct { type SettingsWorker struct {
*Settings *Settings
Worker *WorkerWithToken Worker *WorkerWithTokenAndActiveWorkers
} }
func (h *BaseHandler) SettingsWorkerGroupsGET(c echo.Context) error { func (h *BaseHandler) SettingsWorkerGroupsGET(c echo.Context) error {
@ -38,14 +44,26 @@ func (h *BaseHandler) SettingsWorkerGroupsGET(c echo.Context) error {
return err return err
} }
workerGroupsWithActiveWorkers := make([]*WorkerGroupWithActiveWorkers, len(workerGroups))
for i, workerGroup := range workerGroups {
activeWorkers, err := services.GetActiveWorkers(context.Background(), workerGroup.Slug, h.temporal)
if err != nil {
return err
}
workerGroupsWithActiveWorkers[i] = &WorkerGroupWithActiveWorkers{
WorkerGroupWithMonitors: workerGroup,
ActiveWorkers: activeWorkers,
}
}
return c.Render(http.StatusOK, "settings_worker_groups.tmpl", &SettingsWorkerGroups{ return c.Render(http.StatusOK, "settings_worker_groups.tmpl", &SettingsWorkerGroups{
Settings: NewSettings( Settings: NewSettings(
cc.Principal.User, cc.Principal.User,
GetPageByTitle(SettingsPages, "Worker Groups"), GetPageByTitle(SettingsPages, "Worker Groups"),
[]*components.Page{GetPageByTitle(SettingsPages, "Worker Groups")}, []*components.Page{GetPageByTitle(SettingsPages, "Worker Groups")},
), ),
WorkerGroups: workerGroups, WorkerGroups: workerGroupsWithActiveWorkers,
WorkerGroupsLength: len(workerGroups), WorkerGroupsLength: len(workerGroupsWithActiveWorkers),
}) })
} }
@ -65,6 +83,11 @@ func (h *BaseHandler) SettingsWorkerGroupsDescribeGET(c echo.Context) error {
return err return err
} }
activeWorkers, err := services.GetActiveWorkers(context.Background(), worker.Slug, h.temporal)
if err != nil {
return err
}
return c.Render(http.StatusOK, "settings_worker_groups_describe.tmpl", &SettingsWorker{ return c.Render(http.StatusOK, "settings_worker_groups_describe.tmpl", &SettingsWorker{
Settings: NewSettings( Settings: NewSettings(
cc.Principal.User, cc.Principal.User,
@ -77,9 +100,10 @@ func (h *BaseHandler) SettingsWorkerGroupsDescribeGET(c echo.Context) error {
Breadcrumb: worker.Name, Breadcrumb: worker.Name,
}, },
}), }),
Worker: &WorkerWithToken{ Worker: &WorkerWithTokenAndActiveWorkers{
WorkerGroup: worker, WorkerGroup: worker,
Token: token, Token: token,
ActiveWorkers: activeWorkers,
}, },
}) })
} }

View file

@ -13,8 +13,8 @@ import (
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
) )
func getScheduleId(monitor *models.Monitor, group string) string { func getScheduleId(monitor *models.Monitor) string {
return "monitor-" + monitor.Slug + "-" + group return "monitor-" + monitor.Slug
} }
func CreateMonitor(ctx context.Context, db *sqlx.DB, monitor *models.Monitor) error { func CreateMonitor(ctx context.Context, db *sqlx.DB, monitor *models.Monitor) error {
@ -121,7 +121,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", "SELECT * FROM monitors WHERE deleted_at IS NULL ORDER BY name",
) )
return monitors, err return monitors, err
} }
@ -142,6 +142,7 @@ FROM monitors
LEFT OUTER JOIN monitor_worker_groups ON monitors.slug = monitor_worker_groups.monitor_slug LEFT OUTER JOIN monitor_worker_groups ON monitors.slug = monitor_worker_groups.monitor_slug
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_slug = worker_groups.slug
WHERE monitors.deleted_at IS NULL WHERE monitors.deleted_at IS NULL
ORDER BY monitors.name
`) `)
if err != nil { if err != nil {
return nil, err return nil, err
@ -188,28 +189,36 @@ func CreateOrUpdateMonitorSchedule(
) error { ) error {
log.Println("Creating or Updating Monitor Schedule") log.Println("Creating or Updating Monitor Schedule")
args := make([]interface{}, 0) workerGroupStrings := make([]string, len(workerGroups))
args = append(args, workflows.MonitorWorkflowParam{Script: monitor.Script, Slug: monitor.Slug}) for i, group := range workerGroups {
workerGroupStrings[i] = group.Slug
}
args := make([]interface{}, 1)
args[0] = workflows.MonitorWorkflowParam{
Script: monitor.Script,
Slug: monitor.Slug,
WorkerGroups: workerGroupStrings,
}
for _, group := range workerGroups {
options := client.ScheduleOptions{ options := client.ScheduleOptions{
ID: getScheduleId(monitor, group.Slug), ID: getScheduleId(monitor),
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, group.Slug), ID: getScheduleId(monitor),
Workflow: workflows.NewWorkflows(nil).MonitorWorkflowDefinition, Workflow: workflows.NewWorkflows(nil).MonitorWorkflowDefinition,
Args: args, Args: args,
TaskQueue: group.Slug, TaskQueue: "default",
RetryPolicy: &temporal.RetryPolicy{ RetryPolicy: &temporal.RetryPolicy{
MaximumAttempts: 3, MaximumAttempts: 3,
}, },
}, },
} }
schedule := t.ScheduleClient().GetHandle(ctx, getScheduleId(monitor, group.Slug)) schedule := t.ScheduleClient().GetHandle(ctx, getScheduleId(monitor))
// If exists, we update // If exists, we update
_, err := schedule.Describe(ctx) _, err := schedule.Describe(ctx)
@ -240,7 +249,6 @@ func CreateOrUpdateMonitorSchedule(
if err != nil { if err != nil {
return err return err
} }
}
return nil return nil
} }

View file

@ -5,9 +5,25 @@ import (
"code.tjo.space/mentos1386/zdravko/database/models" "code.tjo.space/mentos1386/zdravko/database/models"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"go.temporal.io/api/enums/v1"
"go.temporal.io/sdk/client"
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
) )
func GetActiveWorkers(ctx context.Context, workerGroupSlug string, temporal client.Client) ([]string, error) {
response, err := temporal.DescribeTaskQueue(ctx, workerGroupSlug, enums.TASK_QUEUE_TYPE_ACTIVITY)
if err != nil {
return make([]string, 0), err
}
workers := make([]string, len(response.Pollers))
for i, poller := range response.Pollers {
workers[i] = poller.Identity
}
return workers, nil
}
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 (slug, name) VALUES (:slug, :name)",
@ -19,7 +35,7 @@ func CreateWorkerGroup(ctx context.Context, db *sqlx.DB, workerGroup *models.Wor
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", "SELECT * FROM worker_groups WHERE deleted_at IS NULL ORDER BY name",
) )
return workerGroups, err return workerGroups, err
} }
@ -38,6 +54,7 @@ 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.slug = monitor_worker_groups.worker_group_slug
LEFT OUTER JOIN monitors ON monitor_worker_groups.monitor_slug = monitors.slug LEFT OUTER JOIN monitors ON monitor_worker_groups.monitor_slug = monitors.slug
WHERE worker_groups.deleted_at IS NULL AND monitors.deleted_at IS NULL WHERE worker_groups.deleted_at IS NULL AND monitors.deleted_at IS NULL
ORDER BY worker_groups.name
`) `)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -1,6 +1,7 @@
package workflows package workflows
import ( import (
"sort"
"time" "time"
"code.tjo.space/mentos1386/zdravko/database/models" "code.tjo.space/mentos1386/zdravko/database/models"
@ -11,13 +12,18 @@ import (
type MonitorWorkflowParam struct { type MonitorWorkflowParam struct {
Script string Script string
Slug string Slug string
WorkerGroups []string
} }
func (w *Workflows) MonitorWorkflowDefinition(ctx workflow.Context, param MonitorWorkflowParam) error { func (w *Workflows) MonitorWorkflowDefinition(ctx workflow.Context, param MonitorWorkflowParam) error {
options := workflow.ActivityOptions{ workerGroups := param.WorkerGroups
StartToCloseTimeout: 10 * time.Second, sort.Strings(workerGroups)
}
ctx = workflow.WithActivityOptions(ctx, options) for _, workerGroup := range workerGroups {
ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
StartToCloseTimeout: 60 * time.Second,
TaskQueue: workerGroup,
})
heatlcheckParam := activities.HealtcheckParam{ heatlcheckParam := activities.HealtcheckParam{
Script: param.Script, Script: param.Script,
@ -38,6 +44,7 @@ func (w *Workflows) MonitorWorkflowDefinition(ctx workflow.Context, param Monito
Slug: param.Slug, Slug: param.Slug,
Status: status, Status: status,
Note: monitorResult.Note, Note: monitorResult.Note,
WorkerGroup: workerGroup,
} }
var historyResult *activities.MonitorAddToHistoryResult var historyResult *activities.MonitorAddToHistoryResult
@ -45,6 +52,7 @@ func (w *Workflows) MonitorWorkflowDefinition(ctx workflow.Context, param Monito
if err != nil { if err != nil {
return err return err
} }
}
return nil return nil
} }

View file

@ -3,4 +3,5 @@ package api
type ApiV1MonitorsHistoryPOSTBody struct { type ApiV1MonitorsHistoryPOSTBody struct {
Status string `json:"status"` Status string `json:"status"`
Note string `json:"note"` Note string `json:"note"`
WorkerGroup string `json:"worker_group"`
} }

84
pkg/server/routes.go Normal file
View file

@ -0,0 +1,84 @@
package server
import (
"log/slog"
"net/http"
"code.tjo.space/mentos1386/zdravko/internal/config"
"code.tjo.space/mentos1386/zdravko/internal/handlers"
"code.tjo.space/mentos1386/zdravko/web/static"
"github.com/jmoiron/sqlx"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"go.temporal.io/sdk/client"
)
func Routes(
e *echo.Echo,
db *sqlx.DB,
temporalClient client.Client,
cfg *config.ServerConfig,
logger *slog.Logger,
) {
h := handlers.NewBaseHandler(db, temporalClient, cfg, logger)
// Health
e.GET("/health", func(c echo.Context) error {
err := db.Ping()
if err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
})
// Server static files
stat := e.Group("/static")
stat.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Filesystem: http.FS(static.Static),
}))
// Public
e.GET("", h.Index)
e.GET("/incidents", h.Incidents)
// Settings
settings := e.Group("/settings")
settings.Use(h.Authenticated)
settings.GET("", h.SettingsOverviewGET)
settings.GET("/monitors", h.SettingsMonitorsGET)
settings.GET("/monitors/create", h.SettingsMonitorsCreateGET)
settings.POST("/monitors/create", h.SettingsMonitorsCreatePOST)
settings.GET("/monitors/:slug", h.SettingsMonitorsDescribeGET)
settings.POST("/monitors/:slug", h.SettingsMonitorsDescribePOST)
settings.GET("/worker-groups", h.SettingsWorkerGroupsGET)
settings.GET("/worker-groups/create", h.SettingsWorkerGroupsCreateGET)
settings.POST("/worker-groups/create", h.SettingsWorkerGroupsCreatePOST)
settings.GET("/worker-groups/:slug", h.SettingsWorkerGroupsDescribeGET)
settings.Match([]string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"}, "/temporal*", h.Temporal)
// OAuth2
oauth2 := e.Group("/oauth2")
oauth2.GET("/login", h.OAuth2LoginGET)
oauth2.GET("/callback", h.OAuth2CallbackGET)
oauth2.GET("/logout", h.OAuth2LogoutGET, h.Authenticated)
// API
apiv1 := e.Group("/api/v1")
apiv1.Use(h.Authenticated)
apiv1.GET("/workers/connect", h.ApiV1WorkersConnectGET)
apiv1.POST("/monitors/:slug/history", h.ApiV1MonitorsHistoryPOST)
// Error handler
e.HTTPErrorHandler = func(err error, c echo.Context) {
code := http.StatusInternalServerError
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
}
if code == http.StatusNotFound {
_ = h.Error404(c)
return
}
_ = c.String(code, err.Error())
}
}

View file

@ -3,13 +3,10 @@ package server
import ( import (
"context" "context"
"log/slog" "log/slog"
"net/http"
"code.tjo.space/mentos1386/zdravko/database" "code.tjo.space/mentos1386/zdravko/database"
"code.tjo.space/mentos1386/zdravko/internal/config" "code.tjo.space/mentos1386/zdravko/internal/config"
"code.tjo.space/mentos1386/zdravko/internal/handlers"
"code.tjo.space/mentos1386/zdravko/internal/temporal" "code.tjo.space/mentos1386/zdravko/internal/temporal"
"code.tjo.space/mentos1386/zdravko/web/static"
"code.tjo.space/mentos1386/zdravko/web/templates" "code.tjo.space/mentos1386/zdravko/web/templates"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
@ -19,6 +16,8 @@ type Server struct {
echo *echo.Echo echo *echo.Echo
cfg *config.ServerConfig cfg *config.ServerConfig
logger *slog.Logger logger *slog.Logger
worker *Worker
} }
func NewServer(cfg *config.ServerConfig) (*Server, error) { func NewServer(cfg *config.ServerConfig) (*Server, error) {
@ -34,10 +33,6 @@ func (s *Server) Name() string {
} }
func (s *Server) Start() error { func (s *Server) Start() error {
s.echo.Renderer = templates.NewTemplates()
//s.echo.Use(middleware.Logger())
s.echo.Use(middleware.Recover())
db, err := database.ConnectToDatabase(s.logger, s.cfg.DatabasePath) db, err := database.ConnectToDatabase(s.logger, s.cfg.DatabasePath)
if err != nil { if err != nil {
return err return err
@ -48,72 +43,25 @@ func (s *Server) Start() error {
return err return err
} }
h := handlers.NewBaseHandler(db, temporalClient, s.cfg, s.logger) s.worker = NewWorker(temporalClient, s.cfg)
// Health s.echo.Renderer = templates.NewTemplates()
s.echo.GET("/health", func(c echo.Context) error { //s.echo.Use(middleware.Logger())
err = db.Ping() s.echo.Use(middleware.Recover())
if err != nil { Routes(s.echo, db, temporalClient, s.cfg, s.logger)
return err
} go func() {
return c.JSON(http.StatusOK, map[string]string{"status": "ok"}) if err := s.worker.Start(); err != nil {
}) panic(err)
// Server static files
stat := s.echo.Group("/static")
stat.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Filesystem: http.FS(static.Static),
}))
// Public
s.echo.GET("", h.Index)
s.echo.GET("/incidents", h.Incidents)
// Settings
settings := s.echo.Group("/settings")
settings.Use(h.Authenticated)
settings.GET("", h.SettingsOverviewGET)
settings.GET("/monitors", h.SettingsMonitorsGET)
settings.GET("/monitors/create", h.SettingsMonitorsCreateGET)
settings.POST("/monitors/create", h.SettingsMonitorsCreatePOST)
settings.GET("/monitors/:slug", h.SettingsMonitorsDescribeGET)
settings.POST("/monitors/:slug", h.SettingsMonitorsDescribePOST)
settings.GET("/worker-groups", h.SettingsWorkerGroupsGET)
settings.GET("/worker-groups/create", h.SettingsWorkerGroupsCreateGET)
settings.POST("/worker-groups/create", h.SettingsWorkerGroupsCreatePOST)
settings.GET("/worker-groups/:slug", h.SettingsWorkerGroupsDescribeGET)
settings.Match([]string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"}, "/temporal*", h.Temporal)
// OAuth2
oauth2 := s.echo.Group("/oauth2")
oauth2.GET("/login", h.OAuth2LoginGET)
oauth2.GET("/callback", h.OAuth2CallbackGET)
oauth2.GET("/logout", h.OAuth2LogoutGET, h.Authenticated)
// API
apiv1 := s.echo.Group("/api/v1")
apiv1.Use(h.Authenticated)
apiv1.GET("/workers/connect", h.ApiV1WorkersConnectGET)
apiv1.POST("/monitors/:slug/history", h.ApiV1MonitorsHistoryPOST)
// Error handler
s.echo.HTTPErrorHandler = func(err error, c echo.Context) {
code := http.StatusInternalServerError
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
}
if code == http.StatusNotFound {
_ = h.Error404(c)
return
}
_ = c.String(code, err.Error())
} }
}()
return s.echo.Start(":" + s.cfg.Port) return s.echo.Start(":" + s.cfg.Port)
} }
func (s *Server) Stop() error { func (s *Server) Stop() error {
s.worker.Stop()
ctx := context.Background() ctx := context.Background()
return s.echo.Shutdown(ctx) return s.echo.Shutdown(ctx)
} }

36
pkg/server/worker.go Normal file
View file

@ -0,0 +1,36 @@
package server
import (
"code.tjo.space/mentos1386/zdravko/internal/activities"
"code.tjo.space/mentos1386/zdravko/internal/config"
"code.tjo.space/mentos1386/zdravko/internal/workflows"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/worker"
)
type Worker struct {
worker worker.Worker
}
func NewWorker(temporalClient client.Client, cfg *config.ServerConfig) *Worker {
w := worker.New(temporalClient, "default", worker.Options{})
workerActivities := activities.NewActivities(&config.WorkerConfig{})
workerWorkflows := workflows.NewWorkflows(workerActivities)
// Register Workflows
w.RegisterWorkflow(workerWorkflows.MonitorWorkflowDefinition)
return &Worker{
worker: w,
}
}
func (w *Worker) Start() error {
return w.worker.Run(worker.InterruptCh())
}
func (w *Worker) Stop() {
w.worker.Stop()
}

View file

@ -107,15 +107,24 @@ code {
@apply sm:col-span-2; @apply sm:col-span-2;
} }
.settings section table {
@apply w-full text-sm text-left rtl:text-right text-gray-500;
}
.settings section table caption {
@apply p-5 text-lg font-semibold text-left rtl:text-right text-gray-900 bg-white;
}
.settings section table caption p {
@apply mt-1 text-sm font-normal text-gray-500;
}
.settings section table thead { .settings section table thead {
@apply text-xs text-gray-700 uppercase bg-gray-50; @apply text-xs text-gray-700 uppercase bg-gray-50;
} }
.settings section table thead th { .settings section table thead th {
@apply px-6 py-3 text-center; @apply px-6 py-4 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider;
} }
.settings section table tbody th { .settings section table tbody tr th {
@apply px-6 py-4 font-medium text-gray-900 whitespace-nowrap text-center; @apply px-6 py-4 font-medium text-gray-900 whitespace-nowrap text-center;
} }
.settings section table tbody tr { .settings section table tbody tr td {
@apply px-6 py-4 text-center; @apply px-6 py-4 text-center whitespace-nowrap;
} }

View file

@ -725,10 +725,6 @@ video {
width: 100%; width: 100%;
} }
.min-w-full {
min-width: 100%;
}
.max-w-screen-lg { .max-w-screen-lg {
max-width: 1024px; max-width: 1024px;
} }
@ -819,8 +815,8 @@ video {
overflow-x: auto; overflow-x: auto;
} }
.whitespace-nowrap { .whitespace-normal {
white-space: nowrap; white-space: normal;
} }
.rounded { .rounded {
@ -843,10 +839,6 @@ video {
border-width: 1px; border-width: 1px;
} }
.border-b-2 {
border-bottom-width: 2px;
}
.border-gray-300 { .border-gray-300 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity)); border-color: rgb(209 213 219 / var(--tw-border-opacity));
@ -948,11 +940,6 @@ video {
padding-right: 1.25rem; padding-right: 1.25rem;
} }
.px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.py-1 { .py-1 {
padding-top: 0.25rem; padding-top: 0.25rem;
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
@ -963,11 +950,6 @@ video {
padding-bottom: 0.75rem; padding-bottom: 0.75rem;
} }
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.py-8 { .py-8 {
padding-top: 2rem; padding-top: 2rem;
padding-bottom: 2rem; padding-bottom: 2rem;
@ -977,10 +959,6 @@ video {
padding-top: 2rem; padding-top: 2rem;
} }
.text-left {
text-align: left;
}
.text-center { .text-center {
text-align: center; text-align: center;
} }
@ -1000,11 +978,6 @@ video {
line-height: 1.5rem; line-height: 1.5rem;
} }
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-sm { .text-sm {
font-size: 0.875rem; font-size: 0.875rem;
line-height: 1.25rem; line-height: 1.25rem;
@ -1055,10 +1028,6 @@ video {
letter-spacing: -0.025em; letter-spacing: -0.025em;
} }
.tracking-wider {
letter-spacing: 0.05em;
}
.text-black { .text-black {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(0 0 0 / var(--tw-text-opacity)); color: rgb(0 0 0 / var(--tw-text-opacity));
@ -1084,11 +1053,6 @@ video {
color: rgb(107 114 128 / var(--tw-text-opacity)); color: rgb(107 114 128 / var(--tw-text-opacity));
} }
.text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity));
}
.text-gray-700 { .text-gray-700 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity)); color: rgb(55 65 81 / var(--tw-text-opacity));
@ -1509,6 +1473,44 @@ code {
} }
} }
.settings section table {
width: 100%;
text-align: left;
font-size: 0.875rem;
line-height: 1.25rem;
--tw-text-opacity: 1;
color: rgb(107 114 128 / var(--tw-text-opacity));
}
.settings section table:where([dir="rtl"], [dir="rtl"] *) {
text-align: right;
}
.settings section table caption {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
padding: 1.25rem;
text-align: left;
font-size: 1.125rem;
line-height: 1.75rem;
font-weight: 600;
--tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity));
}
.settings section table caption:where([dir="rtl"], [dir="rtl"] *) {
text-align: right;
}
.settings section table caption p {
margin-top: 0.25rem;
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: 400;
--tw-text-opacity: 1;
color: rgb(107 114 128 / var(--tw-text-opacity));
}
.settings section table thead { .settings section table thead {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity)); background-color: rgb(249 250 251 / var(--tw-bg-opacity));
@ -1522,12 +1524,19 @@ code {
.settings section table thead th { .settings section table thead th {
padding-left: 1.5rem; padding-left: 1.5rem;
padding-right: 1.5rem; padding-right: 1.5rem;
padding-top: 0.75rem; padding-top: 1rem;
padding-bottom: 0.75rem; padding-bottom: 1rem;
text-align: center; text-align: center;
font-size: 0.75rem;
line-height: 1rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity));
} }
.settings section table tbody th { .settings section table tbody tr th {
white-space: nowrap; white-space: nowrap;
padding-left: 1.5rem; padding-left: 1.5rem;
padding-right: 1.5rem; padding-right: 1.5rem;
@ -1539,7 +1548,8 @@ code {
color: rgb(17 24 39 / var(--tw-text-opacity)); color: rgb(17 24 39 / var(--tw-text-opacity));
} }
.settings section table tbody tr { .settings section table tbody tr td {
white-space: nowrap;
padding-left: 1.5rem; padding-left: 1.5rem;
padding-right: 1.5rem; padding-right: 1.5rem;
padding-top: 1rem; padding-top: 1rem;
@ -1686,7 +1696,3 @@ code {
line-height: 2.5rem; line-height: 2.5rem;
} }
} }
.rtl\:text-right:where([dir="rtl"], [dir="rtl"] *) {
text-align: right;
}

View file

@ -19,11 +19,11 @@
</div> </div>
{{ else }} {{ else }}
<section> <section>
<table class="w-full text-sm text-left rtl:text-right text-gray-500"> <table>
<caption class="p-5 text-lg font-semibold text-left rtl:text-right text-gray-900 bg-white"> <caption>
List of Monitors List of Monitors
<div class="mt-1 gap-4 flex justify-between"> <div class="mt-1 gap-4 flex justify-between">
<p class="mt-1 text-sm font-normal text-gray-500"> <p>
{{ $description }} {{ $description }}
</p> </p>
<a href="/settings/monitors/create" class="inline-flex justify-center items-center py-1 px-2 text-sm font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300"> <a href="/settings/monitors/create" class="inline-flex justify-center items-center py-1 px-2 text-sm font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300">

View file

@ -27,36 +27,36 @@
</section> </section>
<section> <section>
<table class="min-w-full"> <table>
<caption class="p-5 text-lg font-semibold text-left rtl:text-right text-gray-900 bg-white"> <caption>
History History
<p class="mt-1 text-sm font-normal text-gray-500"> <p>
Last 10 executions of monitor script. Last 10 executions of monitor script.
</p> </p>
</caption> </caption>
<thead> <thead>
<tr> <tr>
<th class="px-6 py-3 border-b-2 border-gray-300 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Status</th> <th>Status</th>
<th class="px-6 py-3 border-b-2 border-gray-300 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Created At</th> <th>Created At</th>
<th class="px-6 py-3 border-b-2 border-gray-300 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Duration</th> <th>Duration</th>
<th class="px-6 py-3 border-b-2 border-gray-300 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Note</th> <th>Note</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{range .History}} {{range .History}}
<tr> <tr>
<td class="px-6 py-4 whitespace-nowrap"> <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}}"> <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 }} {{ .Status }}
</span> </span>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td>
{{ .CreatedAt.Format "2006-01-02 15:04:05" }} {{ .CreatedAt.Format "2006-01-02 15:04:05" }}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td>
{ .Duration } { .Duration }
</td> </td>
<td class="px-6 py-4"> <td class="whitespace-normal">
{{ .Note }} {{ .Note }}
</td> </td>
</tr> </tr>

View file

@ -19,11 +19,11 @@
</div> </div>
{{ else }} {{ else }}
<section> <section>
<table class="w-full text-sm text-left rtl:text-right text-gray-500"> <table>
<caption class="p-5 text-lg font-semibold text-left rtl:text-right text-gray-900 bg-white"> <caption>
List of Worker Groups List of Worker Groups
<div class="mt-1 gap-4 flex justify-between"> <div class="mt-1 gap-4 flex justify-between">
<p class="mt-1 text-sm font-normal text-gray-500"> <p>
{{ $description }} {{ $description }}
</p> </p>
<a href="/settings/worker-groups/create" class="inline-flex justify-center items-center py-1 px-2 text-sm font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300"> <a href="/settings/worker-groups/create" class="inline-flex justify-center items-center py-1 px-2 text-sm font-medium text-center text-white rounded-lg bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300">
@ -32,7 +32,7 @@
</a> </a>
</div> </div>
</caption> </caption>
<thead class="text-xs text-gray-700 uppercase bg-gray-50"> <thead>
<tr> <tr>
<th> <th>
Name Name
@ -55,12 +55,15 @@
{{.Name}} {{.Name}}
</th> </th>
<td> <td>
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"> {{ if eq ( len .ActiveWorkers) 0 }}
10 ONLINE
</span>
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"> <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">
NONE NONE
</span> </span>
{{ else }}
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
{{ len .ActiveWorkers }} ONLINE
</span>
{{ end }}
</td> </td>
<td> <td>
{{ len .Monitors }} {{ len .Monitors }}

View file

@ -16,6 +16,31 @@
</div> </div>
</section> </section>
<section>
<table>
<caption>
Active Workers
<p >
Current workers that were online in last minutes.
</p>
</caption>
<thead>
<tr>
<th>Identity</th>
</tr>
</thead>
<tbody>
{{range .Worker.ActiveWorkers }}
<tr>
<td>
{{ . }}
</td>
</tr>
{{end}}
</tbody>
</table>
</section>
<script> <script>
const copyTokenButton = document.getElementById('copy-token'); const copyTokenButton = document.getElementById('copy-token');