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

@ -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)

View file

@ -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,
},
})
}

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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"`
}

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 (
"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)
}

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;
}
.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;
}

View file

@ -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;
}

View file

@ -19,11 +19,11 @@
</div>
{{ else }}
<section>
<table class="w-full text-sm text-left rtl:text-right text-gray-500">
<caption class="p-5 text-lg font-semibold text-left rtl:text-right text-gray-900 bg-white">
<table>
<caption>
List of Monitors
<div class="mt-1 gap-4 flex justify-between">
<p class="mt-1 text-sm font-normal text-gray-500">
<p>
{{ $description }}
</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">

View file

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

View file

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

View file

@ -16,6 +16,31 @@
</div>
</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>
const copyTokenButton = document.getElementById('copy-token');