feat: add forms

This commit is contained in:
Simon Vieille 2026-02-26 10:07:21 +01:00
commit b7aac5d59d
Signed by: deblan
GPG key ID: 579388D585F70417
8 changed files with 205 additions and 3 deletions

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
/borgmatic-monitor
/env.sh
/example.json
/build

View file

@ -23,6 +23,9 @@ func main() {
e.GET("/", web.Hosts)
e.GET("/host/:id", web.Host)
e.Any("/host/new", web.HostCreate)
e.Any("/host/:id/edit", web.HostUpdate)
e.POST("/host/:id/delete", web.HostDelete)
e.POST("/api/v1/borgmatic/infos", web.ApiBorgmaticInfos)
docs.SwaggerInfo.Title = "Borgmatic monitor"

4
go.mod
View file

@ -21,11 +21,13 @@ require (
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/labstack/echo/v5 v5.0.4 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
@ -37,6 +39,8 @@ require (
github.com/swaggo/swag/v2 v2.0.0-rc5 // indirect
github.com/urfave/cli/v2 v2.27.5 // indirect
github.com/urfave/cli/v3 v3.6.2 // indirect
github.com/yassinebenaid/godump v0.11.1 // indirect
gitnet.fr/deblan/go-form v1.5.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.33.0 // indirect

8
go.sum
View file

@ -50,6 +50,8 @@ github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtP
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@ -68,6 +70,8 @@ github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -98,6 +102,10 @@ github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/yassinebenaid/godump v0.11.1 h1:SPujx/XaYqGDfmNh7JI3dOyCUVrG0bG2duhO3Eh2EhI=
github.com/yassinebenaid/godump v0.11.1/go.mod h1:dc/0w8wmg6kVIvNGAzbKH1Oa54dXQx8SNKh4dPRyW44=
gitnet.fr/deblan/go-form v1.5.0 h1:DHRP8kXTMYVtBsTBej6f+oLfpOxp3Ga3PcNpEwm9eqc=
gitnet.fr/deblan/go-form v1.5.0/go.mod h1:jE0cvyDshY1XKmPbjm2ywRl245iSl+ghWj8ANUqU0Fg=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=

51
pkg/types/host.go Normal file
View file

@ -0,0 +1,51 @@
package types
import (
"fmt"
"net/http"
"gitnet.fr/deblan/borgmatic-monitor/pkg/database/model"
"gitnet.fr/deblan/go-form/form"
"gitnet.fr/deblan/go-form/validation"
)
func NewHostForm() *form.Form {
f := form.NewForm(
form.
NewFieldText("Name").
WithOptions(form.NewOption("label", "Name")).
WithConstraints(validation.NewNotBlank()),
form.
NewSubmit("submit").
WithOptions(form.NewOption("attr", form.Attrs{
"class": "mt-2 btn btn-primary",
})),
)
f.
WithName("form").
WithMethod(http.MethodPost).
End()
return f
}
func NewHostDeleteForm(host *model.Host) *form.Form {
btn := form.
NewSubmit("delete").
WithOptions(form.NewOption("attr", form.Attrs{
"class": "btn btn-danger",
"onclick": "return confirm('Are you sure?')",
}))
btn.Data = "Delete"
f := form.NewForm(btn)
f.
WithMethod(http.MethodPost).
WithAction(fmt.Sprintf("/host/%d/delete", host.ID)).
End()
return f
}

View file

@ -8,7 +8,11 @@ import (
"github.com/labstack/echo/v5"
"github.com/spf13/cast"
"gitnet.fr/deblan/borgmatic-monitor/pkg/database"
"gitnet.fr/deblan/borgmatic-monitor/pkg/database/model"
"gitnet.fr/deblan/borgmatic-monitor/pkg/types"
"gitnet.fr/deblan/borgmatic-monitor/pkg/web/partial"
"gitnet.fr/deblan/go-form/theme"
"gorm.io/gorm/clause"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
@ -69,8 +73,18 @@ func Host(c *echo.Context) error {
Div(
Class("container-fluid"),
Div(
Class("d-flex justify-content-end"),
partial.Paginate(fmt.Sprintf("/host/%d?page=%%page%%", host.ID), page, maxPages),
Class("d-flex justify-content-end mb-3"),
Div(
A(
Class("btn btn-primary"),
Href(fmt.Sprintf("/host/%d/edit", host.ID)),
Text("Edit"),
),
),
Div(
Class("ms-2"),
partial.Paginate(fmt.Sprintf("/host/%d?page=%%page%%", host.ID), page, maxPages),
),
),
Div(
Class("row"),
@ -81,3 +95,113 @@ func Host(c *echo.Context) error {
),
))
}
func HostCreate(c *echo.Context) error {
host := new(model.Host)
f := types.NewHostForm()
f.WithAction(c.Request().RequestURI)
f.Mount(host)
render := theme.NewRenderer(theme.Bootstrap5)
if c.Request().Method == f.Method {
f.HandleRequest(c.Request())
if f.IsSubmitted() && f.IsValid() {
f.Bind(host)
database.GetDb().Save(&host)
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/host/%d", host.ID))
}
}
return c.HTML(http.StatusOK, Page(
"home",
"New host",
Div(
Class("container-fluid"),
Div(
Class("d-flex justify-content-end"),
),
Div(
Class("row"),
Div(
Class("col-12"),
Raw(string(render.RenderForm(f))),
),
),
),
))
}
func HostUpdate(c *echo.Context) error {
id := cast.ToInt(c.Param("id"))
if id == 0 {
return c.HTML(http.StatusNotFound, "Not found.")
}
host := database.Host(id)
if host == nil {
return c.HTML(http.StatusNotFound, "Not found.")
}
f := types.NewHostForm()
f.WithAction(c.Request().RequestURI)
f.Mount(host)
render := theme.NewRenderer(theme.Bootstrap5)
if c.Request().Method == f.Method {
f.HandleRequest(c.Request())
if f.IsSubmitted() && f.IsValid() {
f.Bind(host)
database.GetDb().Save(&host)
return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/host/%d", host.ID))
}
}
return c.HTML(http.StatusOK, Page(
fmt.Sprintf("host_%d", host.ID),
host.Name,
Div(
Class("container-fluid"),
Div(
Class("d-flex justify-content-end mb-3"),
Raw(string(render.RenderForm(types.NewHostDeleteForm(host)))),
),
Div(
Class("row"),
Div(
Class("col-12"),
Raw(string(render.RenderForm(f))),
),
),
),
))
}
func HostDelete(c *echo.Context) error {
id := cast.ToInt(c.Param("id"))
if id == 0 {
return c.HTML(http.StatusNotFound, "Not found.")
}
host := database.Host(id)
if host == nil {
return c.HTML(http.StatusNotFound, "Not found.")
}
database.GetDb().
Unscoped().
Select(clause.Associations).
Delete(&host)
return c.Redirect(http.StatusSeeOther, "/")
}

View file

@ -22,6 +22,16 @@ func Hosts(c *echo.Context) error {
"Hosts",
Div(
Class("container-fluid"),
Div(
Class("d-flex justify-content-end mb-3"),
Div(
A(
Class("btn btn-primary"),
Href("/host/new"),
Text("Create"),
),
),
),
Div(
Class("row"),
Group(nodes),

View file

@ -12,7 +12,7 @@ func Paginate(path string, currentPage int, pages int) Node {
return Nav(
Attr("arial-label", "Pagination"),
Ul(
Class("pagination"),
Class("pagination mb-0"),
If(
currentPage > 1,
Li(