From b7aac5d59dcf6d88508294f6c54b4d5c8fc4a1ab Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Thu, 26 Feb 2026 10:07:21 +0100 Subject: [PATCH] feat: add forms --- .gitignore | 2 + cmd/server/main.go | 3 + go.mod | 4 ++ go.sum | 8 +++ pkg/types/host.go | 51 ++++++++++++++++ pkg/web/host.go | 128 ++++++++++++++++++++++++++++++++++++++- pkg/web/hosts.go | 10 +++ pkg/web/partial/pager.go | 2 +- 8 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 pkg/types/host.go diff --git a/.gitignore b/.gitignore index b3fe14d..c8ebb8a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /borgmatic-monitor /env.sh /example.json +/build + diff --git a/cmd/server/main.go b/cmd/server/main.go index bab49e7..20768d2 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -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" diff --git a/go.mod b/go.mod index ab88385..9566f1f 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 6b74a68..96970cf 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/types/host.go b/pkg/types/host.go new file mode 100644 index 0000000..621b4ed --- /dev/null +++ b/pkg/types/host.go @@ -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 +} diff --git a/pkg/web/host.go b/pkg/web/host.go index 040af15..ab12515 100644 --- a/pkg/web/host.go +++ b/pkg/web/host.go @@ -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, "/") +} diff --git a/pkg/web/hosts.go b/pkg/web/hosts.go index 2f40c12..b50f934 100644 --- a/pkg/web/hosts.go +++ b/pkg/web/hosts.go @@ -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), diff --git a/pkg/web/partial/pager.go b/pkg/web/partial/pager.go index e39f6ba..0c2aa3e 100644 --- a/pkg/web/partial/pager.go +++ b/pkg/web/partial/pager.go @@ -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(