diff --git a/internal/view/web/dashboard/backups/create_backup.go b/internal/view/web/dashboard/backups/create_backup.go new file mode 100644 index 0000000..af0bade --- /dev/null +++ b/internal/view/web/dashboard/backups/create_backup.go @@ -0,0 +1,298 @@ +package backups + +import ( + "net/http" + + lucide "github.com/eduardolat/gomponents-lucide" + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/staticdata" + "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/validate" + "github.com/eduardolat/pgbackweb/internal/view/web/component" + "github.com/eduardolat/pgbackweb/internal/view/web/htmx" + "github.com/google/uuid" + "github.com/labstack/echo/v4" + "github.com/maragudk/gomponents" + "github.com/maragudk/gomponents/html" +) + +func (h *handlers) createBackupHandler(c echo.Context) error { + ctx := c.Request().Context() + + var formData struct { + DatabaseID uuid.UUID `form:"database_id" validate:"required,uuid"` + DestinationID uuid.UUID `form:"destination_id" validate:"required,uuid"` + Name string `form:"name" validate:"required"` + CronExpression string `form:"cron_expression" validate:"required"` + TimeZone string `form:"time_zone" validate:"required"` + IsActive string `form:"is_active" validate:"required,oneof=true false"` + DestDir string `form:"dest_dir" validate:"required"` + RetentionDays int16 `form:"retention_days" validate:"required"` + OptDataOnly string `form:"opt_data_only" validate:"required,oneof=true false"` + OptSchemaOnly string `form:"opt_schema_only" validate:"required,oneof=true false"` + OptClean string `form:"opt_clean" validate:"required,oneof=true false"` + OptIfExists string `form:"opt_if_exists" validate:"required,oneof=true false"` + OptCreate string `form:"opt_create" validate:"required,oneof=true false"` + OptNoComments string `form:"opt_no_comments" validate:"required,oneof=true false"` + } + if err := c.Bind(&formData); err != nil { + return htmx.RespondToastError(c, err.Error()) + } + if err := validate.Struct(&formData); err != nil { + return htmx.RespondToastError(c, err.Error()) + } + + _, err := h.servs.BackupsService.CreateBackup( + ctx, dbgen.BackupsServiceCreateBackupParams{ + DatabaseID: formData.DatabaseID, + DestinationID: formData.DestinationID, + Name: formData.Name, + CronExpression: formData.CronExpression, + TimeZone: formData.TimeZone, + IsActive: formData.IsActive == "true", + DestDir: formData.DestDir, + RetentionDays: formData.RetentionDays, + OptDataOnly: formData.OptDataOnly == "true", + OptSchemaOnly: formData.OptSchemaOnly == "true", + OptClean: formData.OptClean == "true", + OptIfExists: formData.OptIfExists == "true", + OptCreate: formData.OptCreate == "true", + OptNoComments: formData.OptNoComments == "true", + }, + ) + if err != nil { + return htmx.RespondToastError(c, err.Error()) + } + + return htmx.RespondRedirect(c, "/dashboard/backups") +} + +func (h *handlers) createBackupFormHandler(c echo.Context) error { + ctx := c.Request().Context() + + databases, err := h.servs.DatabasesService.GetAllDatabases(ctx) + if err != nil { + return htmx.RespondToastError(c, err.Error()) + } + + destinations, err := h.servs.DestinationsService.GetAllDestinations(ctx) + if err != nil { + return htmx.RespondToastError(c, err.Error()) + } + + return echoutil.RenderGomponent( + c, http.StatusOK, createBackupForm(databases, destinations), + ) +} + +func createBackupForm( + databases []dbgen.DatabasesServiceGetAllDatabasesRow, + destinations []dbgen.DestinationsServiceGetAllDestinationsRow, +) gomponents.Node { + yesNoOptions := func() gomponents.Node { + return gomponents.Group([]gomponents.Node{ + html.Option(html.Value("true"), gomponents.Text("Yes")), + html.Option(html.Value("false"), gomponents.Text("No"), html.Selected()), + }) + } + + return html.Form( + htmx.HxPost("/dashboard/backups"), + htmx.HxDisabledELT("find button"), + html.Class("space-y-2"), + + component.SelectControl(component.SelectControlParams{ + Name: "database_id", + Label: "Database", + Required: true, + Placeholder: "Select a database", + Children: []gomponents.Node{ + component.GMap( + databases, + func(db dbgen.DatabasesServiceGetAllDatabasesRow) gomponents.Node { + return html.Option(html.Value(db.ID.String()), gomponents.Text(db.Name)) + }, + ), + }, + }), + + component.SelectControl(component.SelectControlParams{ + Name: "destination_id", + Label: "Destination", + Required: true, + Placeholder: "Select a destination", + Children: []gomponents.Node{ + component.GMap( + destinations, + func(dest dbgen.DestinationsServiceGetAllDestinationsRow) gomponents.Node { + return html.Option(html.Value(dest.ID.String()), gomponents.Text(dest.Name)) + }, + ), + }, + }), + + component.InputControl(component.InputControlParams{ + Name: "name", + Label: "Name", + Placeholder: "My backup", + Required: true, + Type: component.InputTypeText, + HelpText: "A name to easily identify the backup", + }), + + component.InputControl(component.InputControlParams{ + Name: "cron_expression", + Label: "Cron expression", + Placeholder: "* * * * *", + Required: true, + Type: component.InputTypeText, + HelpText: "The cron expression to schedule the backup", + }), + + component.SelectControl(component.SelectControlParams{ + Name: "time_zone", + Label: "Time zone", + Required: true, + Placeholder: "Select a time zone", + HelpText: "The time zone in which the cron expression will be evaluated", + Children: []gomponents.Node{ + component.GMap( + staticdata.Timezones, + func(tz staticdata.Timezone) gomponents.Node { + return html.Option(html.Value(tz.TzCode), gomponents.Text(tz.Label)) + }, + ), + }, + }), + + component.InputControl(component.InputControlParams{ + Name: "dest_dir", + Label: "Destination directory", + Placeholder: "/path/to/backup", + Required: true, + Type: component.InputTypeText, + }), + + component.InputControl(component.InputControlParams{ + Name: "retention_days", + Label: "Retention days", + Placeholder: "30", + Required: true, + Type: component.InputTypeNumber, + HelpText: "The number of days to keep the backup. All backups older than this will be deleted", + Children: []gomponents.Node{ + html.Min("0"), + html.Max("36500"), + html.Pattern("[0-9]+"), + }, + }), + + component.SelectControl(component.SelectControlParams{ + Name: "is_active", + Label: "Activate backup", + Required: true, + Children: []gomponents.Node{ + html.Option(html.Value("true"), gomponents.Text("Yes")), + html.Option(html.Value("false"), gomponents.Text("No")), + }, + }), + + html.Div( + html.Class("grid grid-cols-2 gap-2"), + + component.SelectControl(component.SelectControlParams{ + Name: "opt_data_only", + Label: "--data-only", + Required: true, + Children: []gomponents.Node{ + yesNoOptions(), + }, + }), + + component.SelectControl(component.SelectControlParams{ + Name: "opt_schema_only", + Label: "--schema-only", + Required: true, + Children: []gomponents.Node{ + yesNoOptions(), + }, + }), + + component.SelectControl(component.SelectControlParams{ + Name: "opt_clean", + Label: "--clean", + Required: true, + Children: []gomponents.Node{ + yesNoOptions(), + }, + }), + + component.SelectControl(component.SelectControlParams{ + Name: "opt_if_exists", + Label: "--if-exists", + Required: true, + Children: []gomponents.Node{ + yesNoOptions(), + }, + }), + + component.SelectControl(component.SelectControlParams{ + Name: "opt_create", + Label: "--create", + Required: true, + Children: []gomponents.Node{ + yesNoOptions(), + }, + }), + + component.SelectControl(component.SelectControlParams{ + Name: "opt_no_comments", + Label: "--no-comments", + Required: true, + Children: []gomponents.Node{ + yesNoOptions(), + }, + }), + ), + + html.Div( + html.Class("flex justify-end items-center space-x-2 pt-2"), + component.HxLoadingMd(), + html.Button( + html.Class("btn btn-primary"), + html.Type("submit"), + component.SpanText("Save"), + lucide.Save(), + ), + ), + ) +} + +func createBackupButton() gomponents.Node { + mo := component.Modal(component.ModalParams{ + Size: component.SizeLg, + Title: "Create backup", + Content: []gomponents.Node{ + html.Div( + htmx.HxGet("/dashboard/backups/create-form"), + htmx.HxSwap("outerHTML"), + htmx.HxTrigger("intersect once"), + html.Class("p-10 flex justify-center"), + component.HxLoadingMd(), + ), + }, + }) + + button := html.Button( + mo.OpenerAttr, + html.Class("btn btn-primary"), + component.SpanText("Create backup"), + lucide.Plus(), + ) + + return html.Div( + html.Class("inline-block"), + mo.HTML, + button, + ) +} diff --git a/internal/view/web/dashboard/backups/delete_backup.go b/internal/view/web/dashboard/backups/delete_backup.go new file mode 100644 index 0000000..83d99a8 --- /dev/null +++ b/internal/view/web/dashboard/backups/delete_backup.go @@ -0,0 +1,38 @@ +package backups + +import ( + lucide "github.com/eduardolat/gomponents-lucide" + "github.com/eduardolat/pgbackweb/internal/view/web/htmx" + "github.com/google/uuid" + "github.com/labstack/echo/v4" + "github.com/maragudk/gomponents" + "github.com/maragudk/gomponents/html" +) + +func (h *handlers) deleteBackupHandler(c echo.Context) error { + ctx := c.Request().Context() + + backupID, err := uuid.Parse(c.Param("backupID")) + if err != nil { + return htmx.RespondToastError(c, err.Error()) + } + + if err = h.servs.BackupsService.DeleteBackup(ctx, backupID); err != nil { + return htmx.RespondToastError(c, err.Error()) + } + + return htmx.RespondRefresh(c) +} + +func deleteBackupButton(backupID uuid.UUID) gomponents.Node { + return html.Div( + html.Class("inline-block tooltip tooltip-right"), + html.Data("tip", "Delete backup"), + html.Button( + htmx.HxDelete("/dashboard/backups/"+backupID.String()), + htmx.HxConfirm("Are you sure you want to delete this backup?"), + html.Class("btn btn-error btn-square btn-sm btn-ghost"), + lucide.Trash(), + ), + ) +} diff --git a/internal/view/web/dashboard/backups/edit_backup.go b/internal/view/web/dashboard/backups/edit_backup.go new file mode 100644 index 0000000..f13da38 --- /dev/null +++ b/internal/view/web/dashboard/backups/edit_backup.go @@ -0,0 +1,265 @@ +package backups + +import ( + "database/sql" + "fmt" + + lucide "github.com/eduardolat/gomponents-lucide" + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/staticdata" + "github.com/eduardolat/pgbackweb/internal/validate" + "github.com/eduardolat/pgbackweb/internal/view/web/component" + "github.com/eduardolat/pgbackweb/internal/view/web/htmx" + "github.com/google/uuid" + "github.com/labstack/echo/v4" + "github.com/maragudk/gomponents" + "github.com/maragudk/gomponents/html" +) + +func (h *handlers) editBackupHandler(c echo.Context) error { + ctx := c.Request().Context() + + backupID, err := uuid.Parse(c.Param("backupID")) + if err != nil { + return htmx.RespondToastError(c, err.Error()) + } + + var formData struct { + Name string `form:"name" validate:"required"` + CronExpression string `form:"cron_expression" validate:"required"` + TimeZone string `form:"time_zone" validate:"required"` + IsActive string `form:"is_active" validate:"required,oneof=true false"` + DestDir string `form:"dest_dir" validate:"required"` + RetentionDays int16 `form:"retention_days" validate:"required"` + OptDataOnly string `form:"opt_data_only" validate:"required,oneof=true false"` + OptSchemaOnly string `form:"opt_schema_only" validate:"required,oneof=true false"` + OptClean string `form:"opt_clean" validate:"required,oneof=true false"` + OptIfExists string `form:"opt_if_exists" validate:"required,oneof=true false"` + OptCreate string `form:"opt_create" validate:"required,oneof=true false"` + OptNoComments string `form:"opt_no_comments" validate:"required,oneof=true false"` + } + if err := c.Bind(&formData); err != nil { + return htmx.RespondToastError(c, err.Error()) + } + if err := validate.Struct(&formData); err != nil { + return htmx.RespondToastError(c, err.Error()) + } + + _, err = h.servs.BackupsService.UpdateBackup( + ctx, dbgen.BackupsServiceUpdateBackupParams{ + ID: backupID, + Name: sql.NullString{String: formData.Name, Valid: true}, + CronExpression: sql.NullString{String: formData.CronExpression, Valid: true}, + TimeZone: sql.NullString{String: formData.TimeZone, Valid: true}, + IsActive: sql.NullBool{Bool: formData.IsActive == "true", Valid: true}, + DestDir: sql.NullString{String: formData.DestDir, Valid: true}, + RetentionDays: sql.NullInt16{Int16: formData.RetentionDays, Valid: true}, + OptDataOnly: sql.NullBool{Bool: formData.OptDataOnly == "true", Valid: true}, + OptSchemaOnly: sql.NullBool{Bool: formData.OptSchemaOnly == "true", Valid: true}, + OptClean: sql.NullBool{Bool: formData.OptClean == "true", Valid: true}, + OptIfExists: sql.NullBool{Bool: formData.OptIfExists == "true", Valid: true}, + OptCreate: sql.NullBool{Bool: formData.OptCreate == "true", Valid: true}, + OptNoComments: sql.NullBool{Bool: formData.OptNoComments == "true", Valid: true}, + }, + ) + if err != nil { + return htmx.RespondToastError(c, err.Error()) + } + + return htmx.RespondToastSuccess(c, "Backup updated") +} + +func editBackupButton(backup dbgen.BackupsServicePaginateBackupsRow) gomponents.Node { + yesNoOptions := func(value bool) gomponents.Node { + return gomponents.Group([]gomponents.Node{ + html.Option( + html.Value("true"), + gomponents.Text("Yes"), + gomponents.If(value, html.Selected()), + ), + html.Option( + html.Value("false"), + gomponents.Text("No"), + gomponents.If(!value, html.Selected()), + ), + }) + } + + mo := component.Modal(component.ModalParams{ + Size: component.SizeLg, + Title: "Edit backup", + Content: []gomponents.Node{ + html.Form( + htmx.HxPost("/dashboard/backups/"+backup.ID.String()+"/edit"), + htmx.HxDisabledELT("find button"), + html.Class("space-y-2"), + + component.InputControl(component.InputControlParams{ + Name: "name", + Label: "Name", + Placeholder: "My backup", + Required: true, + Type: component.InputTypeText, + HelpText: "A name to easily identify the backup", + Children: []gomponents.Node{ + html.Value(backup.Name), + }, + }), + + component.InputControl(component.InputControlParams{ + Name: "cron_expression", + Label: "Cron expression", + Placeholder: "* * * * *", + Required: true, + Type: component.InputTypeText, + HelpText: "The cron expression to schedule the backup", + Children: []gomponents.Node{ + html.Value(backup.CronExpression), + }, + }), + + component.SelectControl(component.SelectControlParams{ + Name: "time_zone", + Label: "Time zone", + Required: true, + Placeholder: "Select a time zone", + HelpText: "The time zone in which the cron expression will be evaluated", + Children: []gomponents.Node{ + component.GMap( + staticdata.Timezones, + func(tz staticdata.Timezone) gomponents.Node { + return html.Option( + html.Value(tz.TzCode), + gomponents.Text(tz.Label), + gomponents.If( + tz.TzCode == backup.TimeZone, + html.Selected(), + ), + ) + }, + ), + }, + }), + + component.InputControl(component.InputControlParams{ + Name: "dest_dir", + Label: "Destination directory", + Placeholder: "/path/to/backup", + Required: true, + Type: component.InputTypeText, + Children: []gomponents.Node{ + html.Value(backup.DestDir), + }, + }), + + component.InputControl(component.InputControlParams{ + Name: "retention_days", + Label: "Retention days", + Placeholder: "30", + Required: true, + Type: component.InputTypeNumber, + HelpText: "The number of days to keep the backup. All backups older than this will be deleted", + Children: []gomponents.Node{ + html.Min("0"), + html.Max("36500"), + html.Pattern("[0-9]+"), + html.Value(fmt.Sprintf("%d", backup.RetentionDays)), + }, + }), + + component.SelectControl(component.SelectControlParams{ + Name: "is_active", + Label: "Activate backup", + Required: true, + Children: []gomponents.Node{ + yesNoOptions(backup.IsActive), + }, + }), + + html.Div( + html.Class("grid grid-cols-2 gap-2"), + component.SelectControl(component.SelectControlParams{ + Name: "opt_data_only", + Label: "--data-only", + Required: true, + Children: []gomponents.Node{ + yesNoOptions(backup.OptDataOnly), + }, + }), + + component.SelectControl(component.SelectControlParams{ + Name: "opt_schema_only", + Label: "--schema-only", + Required: true, + Children: []gomponents.Node{ + yesNoOptions(backup.OptSchemaOnly), + }, + }), + + component.SelectControl(component.SelectControlParams{ + Name: "opt_clean", + Label: "--clean", + Required: true, + Children: []gomponents.Node{ + yesNoOptions(backup.OptClean), + }, + }), + + component.SelectControl(component.SelectControlParams{ + Name: "opt_if_exists", + Label: "--if-exists", + Required: true, + Children: []gomponents.Node{ + yesNoOptions(backup.OptIfExists), + }, + }), + + component.SelectControl(component.SelectControlParams{ + Name: "opt_create", + Label: "--create", + Required: true, + Children: []gomponents.Node{ + yesNoOptions(backup.OptCreate), + }, + }), + + component.SelectControl(component.SelectControlParams{ + Name: "opt_no_comments", + Label: "--no-comments", + Required: true, + Children: []gomponents.Node{ + yesNoOptions(backup.OptNoComments), + }, + }), + ), + + html.Div( + html.Class("flex justify-end items-center space-x-2 pt-2"), + component.HxLoadingMd(), + html.Button( + html.Class("btn btn-primary"), + html.Type("submit"), + component.SpanText("Save"), + lucide.Save(), + ), + ), + ), + }, + }) + + button := html.Button( + mo.OpenerAttr, + html.Class("btn btn-neutral btn-sm btn-square btn-ghost"), + lucide.Pencil(), + ) + + return html.Div( + html.Class("inline-block"), + mo.HTML, + html.Div( + html.Class("inline-block tooltip tooltip-right"), + html.Data("tip", "Edit backup"), + button, + ), + ) +} diff --git a/internal/view/web/dashboard/backups/index.go b/internal/view/web/dashboard/backups/index.go new file mode 100644 index 0000000..47d0138 --- /dev/null +++ b/internal/view/web/dashboard/backups/index.go @@ -0,0 +1,66 @@ +package backups + +import ( + "net/http" + + "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/view/web/component" + "github.com/eduardolat/pgbackweb/internal/view/web/htmx" + "github.com/eduardolat/pgbackweb/internal/view/web/layout" + "github.com/labstack/echo/v4" + "github.com/maragudk/gomponents" + "github.com/maragudk/gomponents/html" +) + +func (h *handlers) indexPageHandler(c echo.Context) error { + return echoutil.RenderGomponent(c, http.StatusOK, indexPage()) +} + +func indexPage() gomponents.Node { + content := []gomponents.Node{ + html.Div( + html.Class("flex justify-between items-start"), + component.H1Text("Backups"), + createBackupButton(), + ), + html.Div( + html.Div( + html.Class("mt-4 overflow-x-auto"), + html.Table( + html.Class("table text-nowrap"), + html.THead( + html.Tr( + html.Th(), + html.Th(component.SpanText("Name")), + html.Th(component.SpanText("Database")), + html.Th(component.SpanText("Destination")), + html.Th(component.SpanText("Schedule")), + html.Th(component.SpanText("Retention")), + html.Th(component.SpanText("--data-only")), + html.Th(component.SpanText("--schema-only")), + html.Th(component.SpanText("--clean")), + html.Th(component.SpanText("--if-exists")), + html.Th(component.SpanText("--create")), + html.Th(component.SpanText("--no-comments")), + html.Th(component.SpanText("Created at")), + ), + ), + html.TBody( + htmx.HxGet("/dashboard/backups/list?page=1"), + htmx.HxTrigger("load"), + htmx.HxIndicator("#list-backups-loading"), + ), + ), + ), + html.Div( + html.Class("flex justify-center mt-4"), + component.HxLoadingLg("list-backups-loading"), + ), + ), + } + + return layout.Dashboard(layout.DashboardParams{ + Title: "Backups", + Body: content, + }) +} diff --git a/internal/view/web/dashboard/backups/list_backups.go b/internal/view/web/dashboard/backups/list_backups.go new file mode 100644 index 0000000..744a224 --- /dev/null +++ b/internal/view/web/dashboard/backups/list_backups.go @@ -0,0 +1,117 @@ +package backups + +import ( + "fmt" + "net/http" + + "github.com/eduardolat/pgbackweb/internal/database/dbgen" + "github.com/eduardolat/pgbackweb/internal/service/backups" + "github.com/eduardolat/pgbackweb/internal/util/echoutil" + "github.com/eduardolat/pgbackweb/internal/util/paginateutil" + "github.com/eduardolat/pgbackweb/internal/util/timeutil" + "github.com/eduardolat/pgbackweb/internal/validate" + "github.com/eduardolat/pgbackweb/internal/view/web/component" + "github.com/eduardolat/pgbackweb/internal/view/web/htmx" + "github.com/labstack/echo/v4" + "github.com/maragudk/gomponents" + "github.com/maragudk/gomponents/html" +) + +func (h *handlers) listBackupsHandler(c echo.Context) error { + ctx := c.Request().Context() + + var formData struct { + Page int `query:"page" validate:"required,min=1"` + } + if err := c.Bind(&formData); err != nil { + return htmx.RespondToastError(c, err.Error()) + } + if err := validate.Struct(&formData); err != nil { + return htmx.RespondToastError(c, err.Error()) + } + + pagination, backups, err := h.servs.BackupsService.PaginateBackups( + ctx, backups.PaginateBackupsParams{ + Page: formData.Page, + Limit: 8, + }, + ) + if err != nil { + return htmx.RespondToastError(c, err.Error()) + } + + return echoutil.RenderGomponent( + c, http.StatusOK, listBackups(pagination, backups), + ) +} + +func listBackups( + pagination paginateutil.PaginateResponse, + backups []dbgen.BackupsServicePaginateBackupsRow, +) gomponents.Node { + yesNoSpan := func(b bool) gomponents.Node { + if b { + return component.SpanText("Yes") + } + return component.SpanText("No") + } + + trs := []gomponents.Node{} + for _, backup := range backups { + trs = append(trs, html.Tr( + html.Td( + html.Class("w-[40px]"), + html.Div( + html.Class("flex justify-start space-x-1"), + editBackupButton(backup), + deleteBackupButton(backup.ID), + ), + ), + html.Td( + html.Div( + html.Class("flex items-center space-x-2"), + html.Div( + html.Class("tooltip tooltip-right"), + gomponents.If(backup.IsActive, html.Data("tip", "Active")), + gomponents.If(!backup.IsActive, html.Data("tip", "Inactive")), + component.EnabledPing(backup.IsActive), + ), + component.SpanText(backup.Name), + ), + ), + html.Td(component.SpanText(backup.DatabaseName)), + html.Td(component.SpanText(backup.DestinationName)), + html.Td( + html.Class("font-mono"), + html.Div( + html.Class("flex flex-col items-start space-y-1"), + component.SpanText(backup.CronExpression), + component.SpanText(backup.TimeZone), + ), + ), + html.Td(component.SpanText(fmt.Sprintf("%d days", backup.RetentionDays))), + html.Td(yesNoSpan(backup.OptDataOnly)), + html.Td(yesNoSpan(backup.OptSchemaOnly)), + html.Td(yesNoSpan(backup.OptClean)), + html.Td(yesNoSpan(backup.OptIfExists)), + html.Td(yesNoSpan(backup.OptCreate)), + html.Td(yesNoSpan(backup.OptNoComments)), + html.Td(component.SpanText( + backup.CreatedAt.Format(timeutil.LayoutYYYYMMDDHHMMSSPretty), + )), + )) + } + + if pagination.HasNextPage { + trs = append(trs, html.Tr( + htmx.HxGet(fmt.Sprintf( + "/dashboard/backups/list?page=%d", pagination.NextPage, + )), + htmx.HxTrigger("intersect once"), + htmx.HxSwap("afterend"), + htmx.HxIndicator("#list-backups-loading"), + )) + } + + return component.RenderableGroup(trs) +} diff --git a/internal/view/web/dashboard/backups/router.go b/internal/view/web/dashboard/backups/router.go new file mode 100644 index 0000000..424c5b3 --- /dev/null +++ b/internal/view/web/dashboard/backups/router.go @@ -0,0 +1,28 @@ +package backups + +import ( + "github.com/eduardolat/pgbackweb/internal/service" + "github.com/eduardolat/pgbackweb/internal/view/middleware" + "github.com/labstack/echo/v4" +) + +type handlers struct { + servs *service.Service +} + +func newHandlers(servs *service.Service) *handlers { + return &handlers{servs: servs} +} + +func MountRouter( + parent *echo.Group, mids *middleware.Middleware, servs *service.Service, +) { + h := newHandlers(servs) + + parent.GET("", h.indexPageHandler) + parent.GET("/list", h.listBackupsHandler) + parent.GET("/create-form", h.createBackupFormHandler) + parent.POST("", h.createBackupHandler) + parent.DELETE("/:backupID", h.deleteBackupHandler) + parent.POST("/:backupID/edit", h.editBackupHandler) +} diff --git a/internal/view/web/dashboard/router.go b/internal/view/web/dashboard/router.go index a609b21..dc14e78 100644 --- a/internal/view/web/dashboard/router.go +++ b/internal/view/web/dashboard/router.go @@ -4,6 +4,7 @@ import ( "github.com/eduardolat/pgbackweb/internal/service" "github.com/eduardolat/pgbackweb/internal/view/middleware" "github.com/eduardolat/pgbackweb/internal/view/web/dashboard/about" + "github.com/eduardolat/pgbackweb/internal/view/web/dashboard/backups" "github.com/eduardolat/pgbackweb/internal/view/web/dashboard/databases" "github.com/eduardolat/pgbackweb/internal/view/web/dashboard/destinations" "github.com/eduardolat/pgbackweb/internal/view/web/dashboard/profile" @@ -17,6 +18,7 @@ func MountRouter( summary.MountRouter(parent.Group(""), mids, servs) databases.MountRouter(parent.Group("/databases"), mids, servs) destinations.MountRouter(parent.Group("/destinations"), mids, servs) + backups.MountRouter(parent.Group("/backups"), mids, servs) profile.MountRouter(parent.Group("/profile"), mids, servs) about.MountRouter(parent.Group("/about"), mids, servs) }