diff --git a/README.md b/README.md index c17a2b0..6b1b1fb 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ * [Tasks](#tasks) * [Queues](#queues) * [Dispatcher](#dispatcher) + * [Monitoring tasks and queues](#monitoring-tasks-and-queues) * [Cron](#cron) * [Files](#files) * [Static files](#static-files) @@ -154,6 +155,10 @@ Originally, Postgres and Redis were chosen as defaults but since the aim of this User entity edit +#### Monitor task queues (provided by Backlite via the admin panel) + +Manage task queues + ## Getting started ### Dependencies @@ -401,9 +406,13 @@ To generate a new verification token, the `AuthClient` has a method `GenerateEma The admin panel functionality is considered to be in _beta_ and remains under active development, though all features described here are expected to be fully-functional. Please use caution when using these features and be sure to report any issues you encounter. -What is currently included in the _admin panel_ is a completely dynamic UI to manage all entities defined by _Ent_ (see [screenshots](#screenshots)). There are no separate templates or interfaces for the admin section. +The _admin panel_ currently includes: +* A completely dynamic UI to manage all entities defined by _Ent_. +* A section to monitor all [background tasks and queues](#tasks). -Users with admin [access](#access) will see additional links on the default sidebar for each defined entity type. As with all default UI components, you can easily move these pages and links to a dedicated section, layout, etc. Clicking on the link for any given entity type will provide a pageable table of entities and the ability to add/edit/delete. +There are no separate templates or interfaces for the admin section (see [screenshots](#screenshots)). + +Users with admin [access](#access) will see additional links on the default sidebar at the bottom. As with all default UI components, you can easily move these pages and links to a dedicated section, layout, etc. Clicking on the link for any given entity type will provide a pageable table of entities and the ability to add/edit/delete. ### Code generation @@ -431,7 +440,6 @@ Since the generated code is completely dynamic, all entity functionality related * Exposed filters. * Support all field types (types such as _JSON_ as currently not supported). * Control which fields appear in the entity list table. -* More features than just entity management (ie, including the [Backlite](https://github.com/mikestefanello/backlite#screenshots) UI). ## Routes @@ -989,6 +997,10 @@ The app [configuration](#configuration) contains values to configure the client When the app is shutdown, the dispatcher is given 10 seconds to wait for any in-progress tasks to finish execution. This can be changed in `cmd/web/main.go`. +### Monitoring tasks and queues + +The [admin panel](#admin-panel) contains the UI provided by [Backlite](https://github.com/mikestefanello/backlite) in order to fully monitor all tasks and queues from within your browser. + ## Cron By default, no cron solution is provided because it's very easy to add yourself if you need this. You can either use a [ticker](https://pkg.go.dev/time#Ticker) or a [library](https://github.com/robfig/cron). diff --git a/go.mod b/go.mod index 4135c6d..92e7a72 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/labstack/echo/v4 v4.13.3 github.com/mattn/go-sqlite3 v1.14.28 github.com/maypok86/otter v1.2.4 - github.com/mikestefanello/backlite v0.4.0 + github.com/mikestefanello/backlite v0.5.0 github.com/spf13/afero v1.14.0 github.com/spf13/viper v1.20.1 github.com/stretchr/testify v1.10.0 diff --git a/go.sum b/go.sum index e870643..5286966 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,8 @@ github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEu github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc= github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4= -github.com/mikestefanello/backlite v0.4.0 h1:+OTiErTfwydXzeDS8H8iAdbmrQYJ7XuwxFApFORVy+k= -github.com/mikestefanello/backlite v0.4.0/go.mod h1:yvGKIFQxscmVYW8dtvlmT3UzfmM0NX2OigwNGsbR46o= +github.com/mikestefanello/backlite v0.5.0 h1:6lKZdEYgutdmwV4Id8/t9E/NY14U3fS4/RhiOkFDD4c= +github.com/mikestefanello/backlite v0.5.0/go.mod h1:gx6UKLUQY5OVXQkIm3AzNkyPn9OzoKHKuwM4JGrY4tQ= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= diff --git a/pkg/handlers/admin.go b/pkg/handlers/admin.go index c999a8c..e87e883 100644 --- a/pkg/handlers/admin.go +++ b/pkg/handlers/admin.go @@ -10,6 +10,7 @@ import ( "entgo.io/ent/entc/gen" "entgo.io/ent/entc/load" "github.com/labstack/echo/v4" + "github.com/mikestefanello/backlite/ui" "github.com/mikestefanello/pagoda/ent" "github.com/mikestefanello/pagoda/ent/admin" "github.com/mikestefanello/pagoda/pkg/context" @@ -23,9 +24,10 @@ import ( ) type Admin struct { - orm *ent.Client - graph *gen.Graph - admin *admin.Handler + orm *ent.Client + graph *gen.Graph + admin *admin.Handler + backlite *ui.Handler } func init() { @@ -33,6 +35,7 @@ func init() { } func (h *Admin) Init(c *services.Container) error { + var err error h.graph = c.Graph h.orm = c.ORM h.admin = admin.NewHandler(h.orm, admin.HandlerConfig{ @@ -40,12 +43,19 @@ func (h *Admin) Init(c *services.Container) error { PageQueryKey: pager.QueryKey, TimeFormat: time.DateTime, }) - return nil + h.backlite, err = ui.NewHandler(ui.Config{ + DB: c.Database, + BasePath: "/admin/tasks", + ItemsPerPage: 25, + ReleaseAfter: c.Config.Tasks.ReleaseAfter, + }) + return err } func (h *Admin) Routes(g *echo.Group) { - entities := g.Group("/admin/entity", middleware.RequireAdmin) + ag := g.Group("/admin", middleware.RequireAdmin) + entities := ag.Group("/entity") for _, n := range h.graph.Nodes { ng := entities.Group(fmt.Sprintf("/%s", strings.ToLower(n.Name))) ng.GET("", h.EntityList(n)). @@ -63,6 +73,14 @@ func (h *Admin) Routes(g *echo.Group) { ng.POST("/:id/delete", h.EntityDeleteSubmit(n), h.middlewareEntityLoad(n)). Name = routenames.AdminEntityDeleteSubmit(n.Name) } + + tasks := ag.Group("/tasks") + tasks.GET("", h.Backlite(h.backlite.Running)).Name = routenames.AdminTasks + tasks.GET("/succeeded", h.Backlite(h.backlite.Succeeded)) + tasks.GET("/failed", h.Backlite(h.backlite.Failed)) + tasks.GET("/upcoming", h.Backlite(h.backlite.Upcoming)) + tasks.GET("/task/:id", h.Backlite(h.backlite.Task)) + tasks.GET("/completed/:id", h.Backlite(h.backlite.TaskCompleted)) } // middlewareEntityLoad is middleware to extract the entity ID and attempt to load the given entity. @@ -182,3 +200,12 @@ func (h *Admin) getEntitySchema(n *gen.Type) *load.Schema { } return nil } + +func (h *Admin) Backlite(handler func(http.ResponseWriter, *http.Request) error) echo.HandlerFunc { + return func(c echo.Context) error { + if id := c.Param("id"); id != "" { + c.Request().SetPathValue("task", id) + } + return handler(c.Response().Writer, c.Request()) + } +} diff --git a/pkg/routenames/names.go b/pkg/routenames/names.go index 007c6e2..ee6c9a7 100644 --- a/pkg/routenames/names.go +++ b/pkg/routenames/names.go @@ -26,6 +26,7 @@ const ( CacheSubmit = "cache.submit" Files = "files" FilesSubmit = "files.submit" + AdminTasks = "admin:tasks" ) func AdminEntityList(entityTypeName string) string { diff --git a/pkg/ui/layouts/primary.go b/pkg/ui/layouts/primary.go index a2b6256..a4d78a2 100644 --- a/pkg/ui/layouts/primary.go +++ b/pkg/ui/layouts/primary.go @@ -149,6 +149,20 @@ func sidebarMenu(r *ui.Request) Node { Class("menu-list"), entityTypeLinks, ), + P( + Class("menu-label"), + Text("Monitoring"), + ), + Ul( + Class("menu-list"), + Li( + A( + Href(r.Path(routenames.AdminTasks)), + Text("Tasks"), + Target("_blank"), + ), + ), + ), } } diff --git a/pkg/ui/pages/home.go b/pkg/ui/pages/home.go index 9ae14f0..aea8433 100644 --- a/pkg/ui/pages/home.go +++ b/pkg/ui/pages/home.go @@ -10,6 +10,7 @@ import ( "github.com/mikestefanello/pagoda/pkg/ui/layouts" "github.com/mikestefanello/pagoda/pkg/ui/models" . "maragu.dev/gomponents" + . "maragu.dev/gomponents/components" . "maragu.dev/gomponents/html" ) @@ -38,7 +39,7 @@ func Home(ctx echo.Context, posts *models.Posts) error { headerMsg := func() Node { return Group{ Section( - Class("hero is-info welcome is-small mb-5"), + Class("hero is-info welcome is-small mb-3"), Div( Class("hero-body"), Div( @@ -58,6 +59,28 @@ func Home(ctx echo.Context, posts *models.Posts) error { ), ), ), + Section( + Class("hero is-light is-small mb-5"), + Div( + Class("hero-body"), + Div( + Class("container"), + B(Text("Admin status: ")), + Span( + Classes{ + "tag": true, + "is-success": r.IsAdmin, + "is-danger": !r.IsAdmin, + }, + Text(fmt.Sprint(r.IsAdmin)), + ), + If(!r.IsAdmin, Span( + Class("is-size-7 ml-3"), + Raw(`(click here for instructions to make an admin account)`), + )), + ), + ), + ), H2(Class("title"), Text("Recent posts")), H3(Class("subtitle"), Text("Below is an example of both paging and AJAX fetching using HTMX")), } diff --git a/pkg/ui/pages/task.go b/pkg/ui/pages/task.go index 52c4d5e..94bcb95 100644 --- a/pkg/ui/pages/task.go +++ b/pkg/ui/pages/task.go @@ -22,10 +22,19 @@ func AddTask(ctx echo.Context, form *forms.Task) error { "", Group{ P(Raw("Submitting this form will create an ExampleTask in the task queue. After the specified delay, the message will be logged by the queue processor.")), - P(Text("See pkg/tasks and the README for more information.")), + P(Raw("See pkg/tasks and the README for more information.")), }) }), form.Render(r), + Iff(r.Htmx.Target != "task", func() Node { + return components.Message( + "is-warning", + "", + Group{ + If(!r.IsAdmin, P(Text("Log in as an admin in order to access the task and queue monitoring UI."))), + If(r.IsAdmin, P(Text("View all queued tasks by clicking on the Tasks link in the sidebar."))), + }) + }), } return r.Render(layouts.Primary, g)