Added handler examples for caching and tasks.
This commit is contained in:
parent
5707343d57
commit
2004d6b139
8 changed files with 264 additions and 16 deletions
|
|
@ -103,6 +103,8 @@ func (f *Submission) setErrorMessages(err error) {
|
||||||
message = "Enter a valid email address."
|
message = "Enter a valid email address."
|
||||||
case "eqfield":
|
case "eqfield":
|
||||||
message = "Does not match."
|
message = "Does not match."
|
||||||
|
case "gte":
|
||||||
|
message = fmt.Sprintf("Must be greater than or equal to %v.", ve.Param())
|
||||||
default:
|
default:
|
||||||
message = "Invalid value."
|
message = "Invalid value."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
90
pkg/handlers/cache.go
Normal file
90
pkg/handlers/cache.go
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/form"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/page"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/services"
|
||||||
|
"github.com/mikestefanello/pagoda/templates"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
routeNameCache = "cache"
|
||||||
|
routeNameCacheSubmit = "cache.submit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Cache struct {
|
||||||
|
cache *services.CacheClient
|
||||||
|
*services.TemplateRenderer
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheForm struct {
|
||||||
|
Value string `form:"value"`
|
||||||
|
form.Submission
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(new(Cache))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Cache) Init(c *services.Container) error {
|
||||||
|
h.TemplateRenderer = c.TemplateRenderer
|
||||||
|
h.cache = c.Cache
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Cache) Routes(g *echo.Group) {
|
||||||
|
g.GET("/cache", h.Page).Name = routeNameCache
|
||||||
|
g.POST("/cache", h.Submit).Name = routeNameCacheSubmit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Cache) Page(ctx echo.Context) error {
|
||||||
|
p := page.New(ctx)
|
||||||
|
p.Layout = templates.LayoutMain
|
||||||
|
p.Name = templates.PageCache
|
||||||
|
p.Title = "Set a cache entry"
|
||||||
|
p.Form = form.Get[cacheForm](ctx)
|
||||||
|
|
||||||
|
// Fetch the value from the cache
|
||||||
|
value, err := h.cache.Get().
|
||||||
|
Key("page_cache_example").
|
||||||
|
Fetch(ctx.Request().Context())
|
||||||
|
|
||||||
|
// Store the value in the page so it can be rendered, if found
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
p.Data = value.(string)
|
||||||
|
case errors.Is(err, services.ErrCacheMiss):
|
||||||
|
default:
|
||||||
|
return fail(err, "failed to fetch from cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.RenderPage(ctx, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Cache) Submit(ctx echo.Context) error {
|
||||||
|
var input cacheForm
|
||||||
|
|
||||||
|
if err := form.Submit(ctx, &input); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the cache
|
||||||
|
err := h.cache.Set().
|
||||||
|
Key("page_cache_example").
|
||||||
|
Data(input.Value).
|
||||||
|
Expiration(10 * time.Minute).
|
||||||
|
Save(ctx.Request().Context())
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fail(err, "unable to set cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
form.Clear(ctx)
|
||||||
|
|
||||||
|
return h.Page(ctx)
|
||||||
|
}
|
||||||
|
|
@ -2,14 +2,11 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/mikestefanello/pagoda/pkg/form"
|
"github.com/mikestefanello/pagoda/pkg/form"
|
||||||
"github.com/mikestefanello/pagoda/pkg/page"
|
"github.com/mikestefanello/pagoda/pkg/page"
|
||||||
"github.com/mikestefanello/pagoda/pkg/services"
|
"github.com/mikestefanello/pagoda/pkg/services"
|
||||||
"github.com/mikestefanello/pagoda/pkg/tasks"
|
|
||||||
"github.com/mikestefanello/pagoda/templates"
|
"github.com/mikestefanello/pagoda/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -20,8 +17,7 @@ const (
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Contact struct {
|
Contact struct {
|
||||||
mail *services.MailClient
|
mail *services.MailClient
|
||||||
tasks *services.TaskClient
|
|
||||||
*services.TemplateRenderer
|
*services.TemplateRenderer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -40,7 +36,6 @@ func init() {
|
||||||
func (h *Contact) Init(c *services.Container) error {
|
func (h *Contact) Init(c *services.Container) error {
|
||||||
h.TemplateRenderer = c.TemplateRenderer
|
h.TemplateRenderer = c.TemplateRenderer
|
||||||
h.mail = c.Mail
|
h.mail = c.Mail
|
||||||
h.tasks = c.Tasks
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -72,16 +67,6 @@ func (h *Contact) Submit(ctx echo.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO create a new page for this
|
|
||||||
err = h.tasks.New(tasks.ExampleTask{
|
|
||||||
Message: input.Message,
|
|
||||||
}).
|
|
||||||
Wait(10 * time.Second).
|
|
||||||
Save()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.mail.
|
err = h.mail.
|
||||||
Compose().
|
Compose().
|
||||||
To(input.Email).
|
To(input.Email).
|
||||||
|
|
|
||||||
88
pkg/handlers/task.go
Normal file
88
pkg/handlers/task.go
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/msg"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/form"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/page"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/services"
|
||||||
|
"github.com/mikestefanello/pagoda/pkg/tasks"
|
||||||
|
"github.com/mikestefanello/pagoda/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
routeNameTask = "task"
|
||||||
|
routeNameTaskSubmit = "task.submit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Task struct {
|
||||||
|
tasks *services.TaskClient
|
||||||
|
*services.TemplateRenderer
|
||||||
|
}
|
||||||
|
|
||||||
|
taskForm struct {
|
||||||
|
Delay int `form:"delay" validate:"gte=0"`
|
||||||
|
Message string `form:"message" validate:"required"`
|
||||||
|
form.Submission
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(new(Task))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Task) Init(c *services.Container) error {
|
||||||
|
h.TemplateRenderer = c.TemplateRenderer
|
||||||
|
h.tasks = c.Tasks
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Task) Routes(g *echo.Group) {
|
||||||
|
g.GET("/task", h.Page).Name = routeNameTask
|
||||||
|
g.POST("/task", h.Submit).Name = routeNameTaskSubmit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Task) Page(ctx echo.Context) error {
|
||||||
|
p := page.New(ctx)
|
||||||
|
p.Layout = templates.LayoutMain
|
||||||
|
p.Name = templates.PageTask
|
||||||
|
p.Title = "Create a task"
|
||||||
|
p.Form = form.Get[taskForm](ctx)
|
||||||
|
|
||||||
|
return h.RenderPage(ctx, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Task) Submit(ctx echo.Context) error {
|
||||||
|
var input taskForm
|
||||||
|
|
||||||
|
err := form.Submit(ctx, &input)
|
||||||
|
|
||||||
|
switch err.(type) {
|
||||||
|
case nil:
|
||||||
|
case validator.ValidationErrors:
|
||||||
|
return h.Page(ctx)
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the task
|
||||||
|
err = h.tasks.New(tasks.ExampleTask{
|
||||||
|
Message: input.Message,
|
||||||
|
}).
|
||||||
|
Wait(time.Duration(input.Delay) * time.Second).
|
||||||
|
Save()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fail(err, "unable to create a task")
|
||||||
|
}
|
||||||
|
|
||||||
|
msg.Success(ctx, fmt.Sprintf("The task has been created. Check the logs in %d seconds.", input.Delay))
|
||||||
|
form.Clear(ctx)
|
||||||
|
|
||||||
|
return h.Page(ctx)
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,8 @@
|
||||||
<li>{{link (url "home") "Dashboard" .Path}}</li>
|
<li>{{link (url "home") "Dashboard" .Path}}</li>
|
||||||
<li>{{link (url "about") "About" .Path}}</li>
|
<li>{{link (url "about") "About" .Path}}</li>
|
||||||
<li>{{link (url "contact") "Contact" .Path}}</li>
|
<li>{{link (url "contact") "Contact" .Path}}</li>
|
||||||
|
<li>{{link (url "cache") "Cache" .Path}}</li>
|
||||||
|
<li>{{link (url "task") "Task" .Path}}</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p class="menu-label">Account</p>
|
<p class="menu-label">Account</p>
|
||||||
|
|
|
||||||
36
templates/pages/cache.gohtml
Normal file
36
templates/pages/cache.gohtml
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{{define "content"}}
|
||||||
|
<form id="task" method="post" hx-post="{{url "cache.submit"}}">
|
||||||
|
<article class="message">
|
||||||
|
<div class="message-header">
|
||||||
|
<p>Test the cache</p>
|
||||||
|
</div>
|
||||||
|
<div class="message-body">
|
||||||
|
This route handler shows how the default in-memory cache works. Try updating the value using the form below and see how it persists after you reload the page.
|
||||||
|
HTMX makes it easy to re-render the cached value after the form is submitted.
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<label for="value" class="label">Value in cache: </label>
|
||||||
|
{{if .Data}}
|
||||||
|
<span class="tag is-success">{{.Data}}</span>
|
||||||
|
{{- else}}
|
||||||
|
<i>(empty)</i>
|
||||||
|
{{- end}}
|
||||||
|
<br/><br/>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="value" class="label">Value</label>
|
||||||
|
<div class="control">
|
||||||
|
<input id="value" name="value" class="input" value="{{.Form.Value}}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field is-grouped">
|
||||||
|
<div class="control">
|
||||||
|
<button class="button is-link">Update cache</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{template "csrf" .}}
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
43
templates/pages/task.gohtml
Normal file
43
templates/pages/task.gohtml
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
{{define "content"}}
|
||||||
|
{{- if not (eq .HTMX.Request.Target "task")}}
|
||||||
|
<article class="message is-link">
|
||||||
|
<div class="message-body">
|
||||||
|
<p>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>
|
||||||
|
<p>See pkg/tasks and the README for more information.</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
{{template "form" .}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "form"}}
|
||||||
|
<form id="task" method="post" hx-post="{{url "task.submit"}}">
|
||||||
|
{{template "messages" .}}
|
||||||
|
<div class="field">
|
||||||
|
<label for="delay" class="label">Delay (in seconds)</label>
|
||||||
|
<div class="control">
|
||||||
|
<input type="number" id="delay" name="delay" class="input {{.Form.GetFieldStatusClass "Delay"}}" value="{{.Form.Delay}}"/>
|
||||||
|
</div>
|
||||||
|
<p class="help">How long to wait until the task is executed</p>
|
||||||
|
{{template "field-errors" (.Form.GetFieldErrors "Delay")}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="message" class="label">Message</label>
|
||||||
|
<div class="control">
|
||||||
|
<textarea id="message" name="message" class="textarea {{.Form.GetFieldStatusClass "Message"}}">{{.Form.Message}}</textarea>
|
||||||
|
</div>
|
||||||
|
<p class="help">The message the task will output to the log</p>
|
||||||
|
{{template "field-errors" (.Form.GetFieldErrors "Message")}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field is-grouped">
|
||||||
|
<div class="control">
|
||||||
|
<button class="button is-link">Add task to queue</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{template "csrf" .}}
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
|
@ -22,6 +22,7 @@ const (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PageAbout Page = "about"
|
PageAbout Page = "about"
|
||||||
|
PageCache Page = "cache"
|
||||||
PageContact Page = "contact"
|
PageContact Page = "contact"
|
||||||
PageError Page = "error"
|
PageError Page = "error"
|
||||||
PageForgotPassword Page = "forgot-password"
|
PageForgotPassword Page = "forgot-password"
|
||||||
|
|
@ -30,6 +31,7 @@ const (
|
||||||
PageRegister Page = "register"
|
PageRegister Page = "register"
|
||||||
PageResetPassword Page = "reset-password"
|
PageResetPassword Page = "reset-password"
|
||||||
PageSearch Page = "search"
|
PageSearch Page = "search"
|
||||||
|
PageTask Page = "task"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed *
|
//go:embed *
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue