Added backlite UI to the admin panel.
This commit is contained in:
parent
52f87580a0
commit
3cfcb43031
8 changed files with 99 additions and 13 deletions
18
README.md
18
README.md
|
|
@ -88,6 +88,7 @@
|
||||||
* [Tasks](#tasks)
|
* [Tasks](#tasks)
|
||||||
* [Queues](#queues)
|
* [Queues](#queues)
|
||||||
* [Dispatcher](#dispatcher)
|
* [Dispatcher](#dispatcher)
|
||||||
|
* [Monitoring tasks and queues](#monitoring-tasks-and-queues)
|
||||||
* [Cron](#cron)
|
* [Cron](#cron)
|
||||||
* [Files](#files)
|
* [Files](#files)
|
||||||
* [Static files](#static-files)
|
* [Static files](#static-files)
|
||||||
|
|
@ -154,6 +155,10 @@ Originally, Postgres and Redis were chosen as defaults but since the aim of this
|
||||||
|
|
||||||
<img src="https://raw.githubusercontent.com/mikestefanello/readmeimages/main/pagoda/admin-user_edit.png" alt="User entity edit"/>
|
<img src="https://raw.githubusercontent.com/mikestefanello/readmeimages/main/pagoda/admin-user_edit.png" alt="User entity edit"/>
|
||||||
|
|
||||||
|
#### Monitor task queues (provided by Backlite via the admin panel)
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/mikestefanello/readmeimages/main/backlite/failed.png" alt="Manage task queues"/>
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
### Dependencies
|
### 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.
|
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
|
### Code generation
|
||||||
|
|
||||||
|
|
@ -431,7 +440,6 @@ Since the generated code is completely dynamic, all entity functionality related
|
||||||
* Exposed filters.
|
* Exposed filters.
|
||||||
* Support all field types (types such as _JSON_ as currently not supported).
|
* Support all field types (types such as _JSON_ as currently not supported).
|
||||||
* Control which fields appear in the entity list table.
|
* 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
|
## 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`.
|
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
|
## 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).
|
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).
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -12,7 +12,7 @@ require (
|
||||||
github.com/labstack/echo/v4 v4.13.3
|
github.com/labstack/echo/v4 v4.13.3
|
||||||
github.com/mattn/go-sqlite3 v1.14.28
|
github.com/mattn/go-sqlite3 v1.14.28
|
||||||
github.com/maypok86/otter v1.2.4
|
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/afero v1.14.0
|
||||||
github.com/spf13/viper v1.20.1
|
github.com/spf13/viper v1.20.1
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
|
|
|
||||||
4
go.sum
4
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/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 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc=
|
||||||
github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4=
|
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.5.0 h1:6lKZdEYgutdmwV4Id8/t9E/NY14U3fS4/RhiOkFDD4c=
|
||||||
github.com/mikestefanello/backlite v0.4.0/go.mod h1:yvGKIFQxscmVYW8dtvlmT3UzfmM0NX2OigwNGsbR46o=
|
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 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
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=
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"entgo.io/ent/entc/gen"
|
"entgo.io/ent/entc/gen"
|
||||||
"entgo.io/ent/entc/load"
|
"entgo.io/ent/entc/load"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/mikestefanello/backlite/ui"
|
||||||
"github.com/mikestefanello/pagoda/ent"
|
"github.com/mikestefanello/pagoda/ent"
|
||||||
"github.com/mikestefanello/pagoda/ent/admin"
|
"github.com/mikestefanello/pagoda/ent/admin"
|
||||||
"github.com/mikestefanello/pagoda/pkg/context"
|
"github.com/mikestefanello/pagoda/pkg/context"
|
||||||
|
|
@ -26,6 +27,7 @@ type Admin struct {
|
||||||
orm *ent.Client
|
orm *ent.Client
|
||||||
graph *gen.Graph
|
graph *gen.Graph
|
||||||
admin *admin.Handler
|
admin *admin.Handler
|
||||||
|
backlite *ui.Handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
@ -33,6 +35,7 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Admin) Init(c *services.Container) error {
|
func (h *Admin) Init(c *services.Container) error {
|
||||||
|
var err error
|
||||||
h.graph = c.Graph
|
h.graph = c.Graph
|
||||||
h.orm = c.ORM
|
h.orm = c.ORM
|
||||||
h.admin = admin.NewHandler(h.orm, admin.HandlerConfig{
|
h.admin = admin.NewHandler(h.orm, admin.HandlerConfig{
|
||||||
|
|
@ -40,12 +43,19 @@ func (h *Admin) Init(c *services.Container) error {
|
||||||
PageQueryKey: pager.QueryKey,
|
PageQueryKey: pager.QueryKey,
|
||||||
TimeFormat: time.DateTime,
|
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) {
|
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 {
|
for _, n := range h.graph.Nodes {
|
||||||
ng := entities.Group(fmt.Sprintf("/%s", strings.ToLower(n.Name)))
|
ng := entities.Group(fmt.Sprintf("/%s", strings.ToLower(n.Name)))
|
||||||
ng.GET("", h.EntityList(n)).
|
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)).
|
ng.POST("/:id/delete", h.EntityDeleteSubmit(n), h.middlewareEntityLoad(n)).
|
||||||
Name = routenames.AdminEntityDeleteSubmit(n.Name)
|
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.
|
// 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
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ const (
|
||||||
CacheSubmit = "cache.submit"
|
CacheSubmit = "cache.submit"
|
||||||
Files = "files"
|
Files = "files"
|
||||||
FilesSubmit = "files.submit"
|
FilesSubmit = "files.submit"
|
||||||
|
AdminTasks = "admin:tasks"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AdminEntityList(entityTypeName string) string {
|
func AdminEntityList(entityTypeName string) string {
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,20 @@ func sidebarMenu(r *ui.Request) Node {
|
||||||
Class("menu-list"),
|
Class("menu-list"),
|
||||||
entityTypeLinks,
|
entityTypeLinks,
|
||||||
),
|
),
|
||||||
|
P(
|
||||||
|
Class("menu-label"),
|
||||||
|
Text("Monitoring"),
|
||||||
|
),
|
||||||
|
Ul(
|
||||||
|
Class("menu-list"),
|
||||||
|
Li(
|
||||||
|
A(
|
||||||
|
Href(r.Path(routenames.AdminTasks)),
|
||||||
|
Text("Tasks"),
|
||||||
|
Target("_blank"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
|
"github.com/mikestefanello/pagoda/pkg/ui/layouts"
|
||||||
"github.com/mikestefanello/pagoda/pkg/ui/models"
|
"github.com/mikestefanello/pagoda/pkg/ui/models"
|
||||||
. "maragu.dev/gomponents"
|
. "maragu.dev/gomponents"
|
||||||
|
. "maragu.dev/gomponents/components"
|
||||||
. "maragu.dev/gomponents/html"
|
. "maragu.dev/gomponents/html"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -38,7 +39,7 @@ func Home(ctx echo.Context, posts *models.Posts) error {
|
||||||
headerMsg := func() Node {
|
headerMsg := func() Node {
|
||||||
return Group{
|
return Group{
|
||||||
Section(
|
Section(
|
||||||
Class("hero is-info welcome is-small mb-5"),
|
Class("hero is-info welcome is-small mb-3"),
|
||||||
Div(
|
Div(
|
||||||
Class("hero-body"),
|
Class("hero-body"),
|
||||||
Div(
|
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(`(<a href="https://github.com/mikestefanello/pagoda#create-an-admin-account">click here</a> for instructions to make an admin account)`),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
H2(Class("title"), Text("Recent posts")),
|
H2(Class("title"), Text("Recent posts")),
|
||||||
H3(Class("subtitle"), Text("Below is an example of both paging and AJAX fetching using HTMX")),
|
H3(Class("subtitle"), Text("Below is an example of both paging and AJAX fetching using HTMX")),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,19 @@ func AddTask(ctx echo.Context, form *forms.Task) error {
|
||||||
"",
|
"",
|
||||||
Group{
|
Group{
|
||||||
P(Raw("Submitting this form will create an <i>ExampleTask</i> in the task queue. After the specified delay, the message will be logged by the queue processor.")),
|
P(Raw("Submitting this form will create an <i>ExampleTask</i> 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 <i>pkg/tasks</i> and the README for more information.")),
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
form.Render(r),
|
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)
|
return r.Render(layouts.Primary, g)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue