diff --git a/internal/activities/monitor.go b/internal/activities/monitor.go index 4c64769..502fbeb 100644 --- a/internal/activities/monitor.go +++ b/internal/activities/monitor.go @@ -33,9 +33,10 @@ func (a *Activities) Monitor(ctx context.Context, param HealtcheckParam) (*Monit } type HealtcheckAddToHistoryParam struct { - Slug string - Status string - Note string + Slug string + Status string + Note string + WorkerGroup string } type MonitorAddToHistoryResult struct { @@ -45,8 +46,9 @@ func (a *Activities) MonitorAddToHistory(ctx context.Context, param HealtcheckAd url := fmt.Sprintf("%s/api/v1/monitors/%s/history", a.config.ApiUrl, param.Slug) body := api.ApiV1MonitorsHistoryPOSTBody{ - Status: param.Status, - Note: param.Note, + Status: param.Status, + Note: param.Note, + WorkerGroup: param.WorkerGroup, } jsonBody, err := json.Marshal(body) diff --git a/internal/handlers/settingsworkergroups.go b/internal/handlers/settingsworkergroups.go index 1b4419f..f026fe7 100644 --- a/internal/handlers/settingsworkergroups.go +++ b/internal/handlers/settingsworkergroups.go @@ -14,20 +14,26 @@ import ( "github.com/labstack/echo/v4" ) -type WorkerWithToken struct { +type WorkerWithTokenAndActiveWorkers struct { *models.WorkerGroup - Token string + Token string + ActiveWorkers []string +} + +type WorkerGroupWithActiveWorkers struct { + *models.WorkerGroupWithMonitors + ActiveWorkers []string } type SettingsWorkerGroups struct { *Settings - WorkerGroups []*models.WorkerGroupWithMonitors + WorkerGroups []*WorkerGroupWithActiveWorkers WorkerGroupsLength int } type SettingsWorker struct { *Settings - Worker *WorkerWithToken + Worker *WorkerWithTokenAndActiveWorkers } func (h *BaseHandler) SettingsWorkerGroupsGET(c echo.Context) error { @@ -38,14 +44,26 @@ func (h *BaseHandler) SettingsWorkerGroupsGET(c echo.Context) error { 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{ Settings: NewSettings( cc.Principal.User, GetPageByTitle(SettingsPages, "Worker Groups"), []*components.Page{GetPageByTitle(SettingsPages, "Worker Groups")}, ), - WorkerGroups: workerGroups, - WorkerGroupsLength: len(workerGroups), + WorkerGroups: workerGroupsWithActiveWorkers, + WorkerGroupsLength: len(workerGroupsWithActiveWorkers), }) } @@ -65,6 +83,11 @@ func (h *BaseHandler) SettingsWorkerGroupsDescribeGET(c echo.Context) error { 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{ Settings: NewSettings( cc.Principal.User, @@ -77,9 +100,10 @@ func (h *BaseHandler) SettingsWorkerGroupsDescribeGET(c echo.Context) error { Breadcrumb: worker.Name, }, }), - Worker: &WorkerWithToken{ - WorkerGroup: worker, - Token: token, + Worker: &WorkerWithTokenAndActiveWorkers{ + WorkerGroup: worker, + Token: token, + ActiveWorkers: activeWorkers, }, }) } diff --git a/internal/services/monitor.go b/internal/services/monitor.go index 8e3534e..4407aba 100644 --- a/internal/services/monitor.go +++ b/internal/services/monitor.go @@ -13,8 +13,8 @@ import ( "golang.org/x/exp/maps" ) -func getScheduleId(monitor *models.Monitor, group string) string { - return "monitor-" + monitor.Slug + "-" + group +func getScheduleId(monitor *models.Monitor) string { + return "monitor-" + monitor.Slug } 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) { monitors := []*models.Monitor{} 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 } @@ -142,6 +142,7 @@ FROM monitors 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 WHERE monitors.deleted_at IS NULL +ORDER BY monitors.name `) if err != nil { return nil, err @@ -188,59 +189,66 @@ func CreateOrUpdateMonitorSchedule( ) error { log.Println("Creating or Updating Monitor Schedule") - args := make([]interface{}, 0) - args = append(args, workflows.MonitorWorkflowParam{Script: monitor.Script, Slug: monitor.Slug}) + workerGroupStrings := make([]string, len(workerGroups)) + for i, group := range workerGroups { + workerGroupStrings[i] = group.Slug + } - for _, group := range workerGroups { - options := client.ScheduleOptions{ - ID: getScheduleId(monitor, group.Slug), - Spec: client.ScheduleSpec{ - CronExpressions: []string{monitor.Schedule}, - Jitter: time.Second * 10, + args := make([]interface{}, 1) + args[0] = workflows.MonitorWorkflowParam{ + Script: monitor.Script, + Slug: monitor.Slug, + WorkerGroups: workerGroupStrings, + } + + options := client.ScheduleOptions{ + ID: getScheduleId(monitor), + Spec: client.ScheduleSpec{ + CronExpressions: []string{monitor.Schedule}, + Jitter: time.Second * 10, + }, + Action: &client.ScheduleWorkflowAction{ + ID: getScheduleId(monitor), + Workflow: workflows.NewWorkflows(nil).MonitorWorkflowDefinition, + Args: args, + TaskQueue: "default", + RetryPolicy: &temporal.RetryPolicy{ + MaximumAttempts: 3, }, - Action: &client.ScheduleWorkflowAction{ - ID: getScheduleId(monitor, group.Slug), - Workflow: workflows.NewWorkflows(nil).MonitorWorkflowDefinition, - Args: args, - TaskQueue: group.Slug, - RetryPolicy: &temporal.RetryPolicy{ - MaximumAttempts: 3, - }, + }, + } + + schedule := t.ScheduleClient().GetHandle(ctx, getScheduleId(monitor)) + + // 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 } - - schedule := t.ScheduleClient().GetHandle(ctx, getScheduleId(monitor, group.Slug)) - - // 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{}) + } 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 } diff --git a/internal/services/worker_group.go b/internal/services/worker_group.go index da16b27..38fd1cb 100644 --- a/internal/services/worker_group.go +++ b/internal/services/worker_group.go @@ -5,9 +5,25 @@ import ( "code.tjo.space/mentos1386/zdravko/database/models" "github.com/jmoiron/sqlx" + "go.temporal.io/api/enums/v1" + "go.temporal.io/sdk/client" "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 { _, err := db.NamedExecContext(ctx, "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) { var workerGroups []*models.WorkerGroup 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 } @@ -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 monitors ON monitor_worker_groups.monitor_slug = monitors.slug WHERE worker_groups.deleted_at IS NULL AND monitors.deleted_at IS NULL +ORDER BY worker_groups.name `) if err != nil { return nil, err diff --git a/internal/workflows/monitor.go b/internal/workflows/monitor.go index e22b24a..5424ea1 100644 --- a/internal/workflows/monitor.go +++ b/internal/workflows/monitor.go @@ -1,6 +1,7 @@ package workflows import ( + "sort" "time" "code.tjo.space/mentos1386/zdravko/database/models" @@ -9,41 +10,48 @@ import ( ) type MonitorWorkflowParam struct { - Script string - Slug string + Script string + Slug string + WorkerGroups []string } func (w *Workflows) MonitorWorkflowDefinition(ctx workflow.Context, param MonitorWorkflowParam) error { - options := workflow.ActivityOptions{ - StartToCloseTimeout: 10 * time.Second, - } - ctx = workflow.WithActivityOptions(ctx, options) + workerGroups := param.WorkerGroups + sort.Strings(workerGroups) - heatlcheckParam := activities.HealtcheckParam{ - Script: param.Script, - } + for _, workerGroup := range workerGroups { + ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ + StartToCloseTimeout: 60 * time.Second, + TaskQueue: workerGroup, + }) - var monitorResult *activities.MonitorResult - err := workflow.ExecuteActivity(ctx, w.activities.Monitor, heatlcheckParam).Get(ctx, &monitorResult) - if err != nil { - return err - } + heatlcheckParam := activities.HealtcheckParam{ + Script: param.Script, + } - status := models.MonitorFailure - if monitorResult.Success { - status = models.MonitorSuccess - } + var monitorResult *activities.MonitorResult + err := workflow.ExecuteActivity(ctx, w.activities.Monitor, heatlcheckParam).Get(ctx, &monitorResult) + if err != nil { + return err + } - historyParam := activities.HealtcheckAddToHistoryParam{ - Slug: param.Slug, - Status: status, - Note: monitorResult.Note, - } + status := models.MonitorFailure + if monitorResult.Success { + status = models.MonitorSuccess + } - var historyResult *activities.MonitorAddToHistoryResult - err = workflow.ExecuteActivity(ctx, w.activities.MonitorAddToHistory, historyParam).Get(ctx, &historyResult) - if err != nil { - return err + historyParam := activities.HealtcheckAddToHistoryParam{ + Slug: param.Slug, + Status: status, + Note: monitorResult.Note, + WorkerGroup: workerGroup, + } + + var historyResult *activities.MonitorAddToHistoryResult + err = workflow.ExecuteActivity(ctx, w.activities.MonitorAddToHistory, historyParam).Get(ctx, &historyResult) + if err != nil { + return err + } } return nil diff --git a/pkg/api/monitors.go b/pkg/api/monitors.go index 5696d6b..a2689f2 100644 --- a/pkg/api/monitors.go +++ b/pkg/api/monitors.go @@ -1,6 +1,7 @@ package api type ApiV1MonitorsHistoryPOSTBody struct { - Status string `json:"status"` - Note string `json:"note"` + Status string `json:"status"` + Note string `json:"note"` + WorkerGroup string `json:"worker_group"` } diff --git a/pkg/server/routes.go b/pkg/server/routes.go new file mode 100644 index 0000000..55ba3dc --- /dev/null +++ b/pkg/server/routes.go @@ -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()) + } +} diff --git a/pkg/server/server.go b/pkg/server/server.go index 4268ba5..28c8ed4 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -3,13 +3,10 @@ package server import ( "context" "log/slog" - "net/http" "code.tjo.space/mentos1386/zdravko/database" "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/web/static" "code.tjo.space/mentos1386/zdravko/web/templates" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" @@ -19,6 +16,8 @@ type Server struct { echo *echo.Echo cfg *config.ServerConfig logger *slog.Logger + + worker *Worker } func NewServer(cfg *config.ServerConfig) (*Server, error) { @@ -34,10 +33,6 @@ func (s *Server) Name() string { } 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) if err != nil { return err @@ -48,72 +43,25 @@ func (s *Server) Start() error { return err } - h := handlers.NewBaseHandler(db, temporalClient, s.cfg, s.logger) + s.worker = NewWorker(temporalClient, s.cfg) - // Health - s.echo.GET("/health", func(c echo.Context) error { - err = db.Ping() - if err != nil { - return err + s.echo.Renderer = templates.NewTemplates() + //s.echo.Use(middleware.Logger()) + s.echo.Use(middleware.Recover()) + Routes(s.echo, db, temporalClient, s.cfg, s.logger) + + go func() { + if err := s.worker.Start(); err != nil { + panic(err) } - return c.JSON(http.StatusOK, map[string]string{"status": "ok"}) - }) - - // 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) } func (s *Server) Stop() error { + s.worker.Stop() + ctx := context.Background() return s.echo.Shutdown(ctx) } diff --git a/pkg/server/worker.go b/pkg/server/worker.go new file mode 100644 index 0000000..e505905 --- /dev/null +++ b/pkg/server/worker.go @@ -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() +} diff --git a/web/static/css/main.css b/web/static/css/main.css index 463c3c5..5481903 100644 --- a/web/static/css/main.css +++ b/web/static/css/main.css @@ -107,15 +107,24 @@ code { @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 { @apply text-xs text-gray-700 uppercase bg-gray-50; } .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; } -.settings section table tbody tr { - @apply px-6 py-4 text-center; +.settings section table tbody tr td { + @apply px-6 py-4 text-center whitespace-nowrap; } diff --git a/web/static/css/tailwind.css b/web/static/css/tailwind.css index 3d85bd6..b9c5dad 100644 --- a/web/static/css/tailwind.css +++ b/web/static/css/tailwind.css @@ -725,10 +725,6 @@ video { width: 100%; } -.min-w-full { - min-width: 100%; -} - .max-w-screen-lg { max-width: 1024px; } @@ -819,8 +815,8 @@ video { overflow-x: auto; } -.whitespace-nowrap { - white-space: nowrap; +.whitespace-normal { + white-space: normal; } .rounded { @@ -843,10 +839,6 @@ video { border-width: 1px; } -.border-b-2 { - border-bottom-width: 2px; -} - .border-gray-300 { --tw-border-opacity: 1; border-color: rgb(209 213 219 / var(--tw-border-opacity)); @@ -948,11 +940,6 @@ video { padding-right: 1.25rem; } -.px-6 { - padding-left: 1.5rem; - padding-right: 1.5rem; -} - .py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; @@ -963,11 +950,6 @@ video { padding-bottom: 0.75rem; } -.py-4 { - padding-top: 1rem; - padding-bottom: 1rem; -} - .py-8 { padding-top: 2rem; padding-bottom: 2rem; @@ -977,10 +959,6 @@ video { padding-top: 2rem; } -.text-left { - text-align: left; -} - .text-center { text-align: center; } @@ -1000,11 +978,6 @@ video { line-height: 1.5rem; } -.text-lg { - font-size: 1.125rem; - line-height: 1.75rem; -} - .text-sm { font-size: 0.875rem; line-height: 1.25rem; @@ -1055,10 +1028,6 @@ video { letter-spacing: -0.025em; } -.tracking-wider { - letter-spacing: 0.05em; -} - .text-black { --tw-text-opacity: 1; color: rgb(0 0 0 / var(--tw-text-opacity)); @@ -1084,11 +1053,6 @@ video { 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 { --tw-text-opacity: 1; 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 { --tw-bg-opacity: 1; background-color: rgb(249 250 251 / var(--tw-bg-opacity)); @@ -1522,12 +1524,19 @@ code { .settings section table thead th { padding-left: 1.5rem; padding-right: 1.5rem; - padding-top: 0.75rem; - padding-bottom: 0.75rem; + padding-top: 1rem; + padding-bottom: 1rem; 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; padding-left: 1.5rem; padding-right: 1.5rem; @@ -1539,7 +1548,8 @@ code { 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-right: 1.5rem; padding-top: 1rem; @@ -1686,7 +1696,3 @@ code { line-height: 2.5rem; } } - -.rtl\:text-right:where([dir="rtl"], [dir="rtl"] *) { - text-align: right; -} diff --git a/web/templates/pages/settings_monitors.tmpl b/web/templates/pages/settings_monitors.tmpl index 1e10c2e..50d836b 100644 --- a/web/templates/pages/settings_monitors.tmpl +++ b/web/templates/pages/settings_monitors.tmpl @@ -19,11 +19,11 @@ {{ else }}
- -
+ +
List of Monitors
-

+

{{ $description }}

diff --git a/web/templates/pages/settings_monitors_describe.tmpl b/web/templates/pages/settings_monitors_describe.tmpl index 301d61c..93eeb27 100644 --- a/web/templates/pages/settings_monitors_describe.tmpl +++ b/web/templates/pages/settings_monitors_describe.tmpl @@ -27,36 +27,36 @@
- -
+ + - - - - + + + + {{range .History}} - - - - diff --git a/web/templates/pages/settings_worker_groups.tmpl b/web/templates/pages/settings_worker_groups.tmpl index f2ae54e..14109e7 100644 --- a/web/templates/pages/settings_worker_groups.tmpl +++ b/web/templates/pages/settings_worker_groups.tmpl @@ -19,11 +19,11 @@ {{ else }}
-
History -

+

Last 10 executions of monitor script.

StatusCreated AtDurationNoteStatusCreated AtDurationNote
+ {{ .Status }} + {{ .CreatedAt.Format "2006-01-02 15:04:05" }} + { .Duration } + {{ .Note }}
-
+ + - +
List of Worker Groups
-

+

{{ $description }}

@@ -32,7 +32,7 @@
Name @@ -55,12 +55,15 @@ {{.Name}} - - 10 ONLINE - + {{ if eq ( len .ActiveWorkers) 0 }} NONE + {{ else }} + + {{ len .ActiveWorkers }} ONLINE + + {{ end }} {{ len .Monitors }} diff --git a/web/templates/pages/settings_worker_groups_describe.tmpl b/web/templates/pages/settings_worker_groups_describe.tmpl index a9dfe65..d7e833e 100644 --- a/web/templates/pages/settings_worker_groups_describe.tmpl +++ b/web/templates/pages/settings_worker_groups_describe.tmpl @@ -16,6 +16,31 @@ +
+ + + + + + + + + {{range .Worker.ActiveWorkers }} + + + + {{end}} + +
+ Active Workers +

+ Current workers that were online in last minutes. +

+
Identity
+ {{ . }} +
+
+