mirror of
https://github.com/eduardolat/pgbackweb.git
synced 2026-05-13 06:58:27 -05:00
Add backups router and handlers to dashboard
This commit is contained in:
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user