mirror of
https://github.com/eduardolat/pgbackweb.git
synced 2026-02-12 07:08:27 -06:00
Update backups forms with is_local option and better docs
This commit is contained in:
147
internal/view/web/dashboard/backups/common.go
Normal file
147
internal/view/web/dashboard/backups/common.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package backups
|
||||
|
||||
import (
|
||||
lucide "github.com/eduardolat/gomponents-lucide"
|
||||
"github.com/eduardolat/pgbackweb/internal/view/web/component"
|
||||
"github.com/maragudk/gomponents"
|
||||
"github.com/maragudk/gomponents/components"
|
||||
"github.com/maragudk/gomponents/html"
|
||||
)
|
||||
|
||||
func localBackupsHelp() []gomponents.Node {
|
||||
return []gomponents.Node{
|
||||
component.H3Text("Local backups"),
|
||||
component.PText(`
|
||||
Local backups are stored in the server where PG Back Web is running.
|
||||
They are stored under /backups directory so you can mount a docker
|
||||
volume to this directory to persist the backups in any way you want.
|
||||
`),
|
||||
|
||||
html.Div(
|
||||
html.Class("mt-2"),
|
||||
component.H3Text("Remote backups"),
|
||||
component.PText(`
|
||||
Remote backups are stored in a destination. A destination is a remote
|
||||
S3 compatible storage. With this option you don't need to worry about
|
||||
creating and managing docker volumes.
|
||||
`),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func cronExpressionHelp() []gomponents.Node {
|
||||
return []gomponents.Node{
|
||||
component.PText(`
|
||||
A cron expression is a string used to define a schedule for running tasks
|
||||
in Unix-like operating systems. It consists of five fields representing
|
||||
the minute, hour, day of the month, month, and day of the week.
|
||||
Cron expressions enable precise scheduling of periodic tasks.
|
||||
`),
|
||||
|
||||
html.Div(
|
||||
html.Class("mt-4 flex justify-end items-center"),
|
||||
html.A(
|
||||
html.Href("https://en.wikipedia.org/wiki/Cron"),
|
||||
html.Target("_blank"),
|
||||
html.Class("btn btn-ghost"),
|
||||
component.SpanText("Learn more"),
|
||||
lucide.ExternalLink(),
|
||||
),
|
||||
html.A(
|
||||
html.Href("https://crontab.guru/examples.html"),
|
||||
html.Target("_blank"),
|
||||
html.Class("btn btn-ghost"),
|
||||
component.SpanText("Examples & common expressions"),
|
||||
lucide.ExternalLink(),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func destinationDirectoryHelp() []gomponents.Node {
|
||||
return []gomponents.Node{
|
||||
component.PText(`
|
||||
The destination directory is the directory where the backups will be
|
||||
stored. This directory is relative to the base directory of the
|
||||
destination. It should start with a slash, should not contain any
|
||||
spaces, and should not end with a slash.
|
||||
`),
|
||||
|
||||
html.Div(
|
||||
html.Class("mt-2"),
|
||||
component.H3Text("Local backups"),
|
||||
component.PText(`
|
||||
For local backups, the base directory is /backups. So, the backup files
|
||||
will be stored in:
|
||||
`),
|
||||
html.Div(
|
||||
components.Classes{
|
||||
"whitespace-nowrap p-1": true,
|
||||
"overflow-x-scroll": true,
|
||||
"font-mono": true,
|
||||
},
|
||||
component.BText(
|
||||
"/backups/<destination-directory>/<YYYY>/<MM>/<DD>/dump-<random-suffix>.zip",
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
html.Div(
|
||||
html.Class("mt-2"),
|
||||
component.H3Text("Remote backups"),
|
||||
component.PText(`
|
||||
For remote backups, the base directory is the root of the bucket. So,
|
||||
the backup files will be stored in:
|
||||
`),
|
||||
html.Div(
|
||||
components.Classes{
|
||||
"whitespace-nowrap p-1": true,
|
||||
"overflow-x-scroll": true,
|
||||
"font-mono": true,
|
||||
},
|
||||
component.BText(
|
||||
"s3://<bucket>/<destination-directory>/<YYYY>/<MM>/<DD>/dump-<random-suffix>.zip",
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func retentionDaysHelp() []gomponents.Node {
|
||||
return []gomponents.Node{
|
||||
component.PText(`
|
||||
Retention days specifies the number of days to keep backup files before
|
||||
they are automatically deleted. This ensures that old backups are removed
|
||||
to save storage space. The retention period is evaluated by execution.
|
||||
`),
|
||||
}
|
||||
}
|
||||
|
||||
func pgDumpOptionsHelp() []gomponents.Node {
|
||||
return []gomponents.Node{
|
||||
html.Div(
|
||||
html.Class("space-y-2"),
|
||||
|
||||
component.PText(`
|
||||
This software uses the battle tested pg_dump utility to create backups. It
|
||||
makes consistent backups even if the database is being used concurrently.
|
||||
`),
|
||||
|
||||
component.PText(`
|
||||
These are options that will be passed to the pg_dump utility. By default,
|
||||
PG Back Web does not pass any options so the backups are full backups.
|
||||
`),
|
||||
|
||||
html.Div(
|
||||
html.Class("flex justify-end"),
|
||||
html.A(
|
||||
html.Class("btn btn-ghost"),
|
||||
html.Href("https://www.postgresql.org/docs/current/app-pgdump.html"),
|
||||
html.Target("_blank"),
|
||||
component.SpanText("Learn more in pg_dump documentation"),
|
||||
lucide.ExternalLink(html.Class("ml-1")),
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"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/alpine"
|
||||
"github.com/eduardolat/pgbackweb/internal/view/web/component"
|
||||
"github.com/eduardolat/pgbackweb/internal/view/web/htmx"
|
||||
"github.com/google/uuid"
|
||||
@@ -21,7 +22,8 @@ func (h *handlers) createBackupHandler(c echo.Context) error {
|
||||
|
||||
var formData struct {
|
||||
DatabaseID uuid.UUID `form:"database_id" validate:"required,uuid"`
|
||||
DestinationID uuid.UUID `form:"destination_id" validate:"required,uuid"`
|
||||
DestinationID uuid.UUID `form:"destination_id" validate:"omitempty,uuid"`
|
||||
IsLocal string `form:"is_local" validate:"required,oneof=true false"`
|
||||
Name string `form:"name" validate:"required"`
|
||||
CronExpression string `form:"cron_expression" validate:"required"`
|
||||
TimeZone string `form:"time_zone" validate:"required"`
|
||||
@@ -44,8 +46,11 @@ func (h *handlers) createBackupHandler(c echo.Context) error {
|
||||
|
||||
_, err := h.servs.BackupsService.CreateBackup(
|
||||
ctx, dbgen.BackupsServiceCreateBackupParams{
|
||||
DatabaseID: formData.DatabaseID,
|
||||
DestinationID: formData.DestinationID,
|
||||
DatabaseID: formData.DatabaseID,
|
||||
DestinationID: uuid.NullUUID{
|
||||
Valid: formData.IsLocal == "false", UUID: formData.DestinationID,
|
||||
},
|
||||
IsLocal: formData.IsLocal == "true",
|
||||
Name: formData.Name,
|
||||
CronExpression: formData.CronExpression,
|
||||
TimeZone: formData.TimeZone,
|
||||
@@ -101,13 +106,16 @@ func createBackupForm(
|
||||
htmx.HxDisabledELT("find button"),
|
||||
html.Class("space-y-2 text-base"),
|
||||
|
||||
alpine.XData(`{
|
||||
is_local: "false",
|
||||
}`),
|
||||
|
||||
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.SelectControl(component.SelectControlParams{
|
||||
@@ -126,53 +134,46 @@ func createBackupForm(
|
||||
}),
|
||||
|
||||
component.SelectControl(component.SelectControlParams{
|
||||
Name: "destination_id",
|
||||
Label: "Destination",
|
||||
Required: true,
|
||||
Placeholder: "Select a destination",
|
||||
Name: "is_local",
|
||||
Label: "Local backup",
|
||||
Required: true,
|
||||
Children: []gomponents.Node{
|
||||
component.GMap(
|
||||
destinations,
|
||||
func(dest dbgen.DestinationsServiceGetAllDestinationsRow) gomponents.Node {
|
||||
return html.Option(html.Value(dest.ID.String()), gomponents.Text(dest.Name))
|
||||
},
|
||||
),
|
||||
alpine.XModel("is_local"),
|
||||
html.Option(html.Value("true"), gomponents.Text("Yes")),
|
||||
html.Option(html.Value("false"), gomponents.Text("No"), html.Selected()),
|
||||
},
|
||||
HelpButtonChildren: localBackupsHelp(),
|
||||
}),
|
||||
|
||||
html.Div(
|
||||
component.InputControl(component.InputControlParams{
|
||||
Name: "cron_expression",
|
||||
Label: "Cron expression",
|
||||
Placeholder: "* * * * *",
|
||||
alpine.Template(
|
||||
alpine.XIf("is_local == 'false'"),
|
||||
component.SelectControl(component.SelectControlParams{
|
||||
Name: "destination_id",
|
||||
Label: "Destination",
|
||||
Required: true,
|
||||
Type: component.InputTypeText,
|
||||
HelpText: "The cron expression to schedule the backup",
|
||||
Placeholder: "Select a destination",
|
||||
Children: []gomponents.Node{
|
||||
html.Pattern(`^\S+\s+\S+\s+\S+\s+\S+\s+\S+$`),
|
||||
component.GMap(
|
||||
destinations,
|
||||
func(dest dbgen.DestinationsServiceGetAllDestinationsRow) gomponents.Node {
|
||||
return html.Option(html.Value(dest.ID.String()), gomponents.Text(dest.Name))
|
||||
},
|
||||
),
|
||||
},
|
||||
}),
|
||||
html.P(
|
||||
html.Class("pl-1"),
|
||||
gomponents.Text("Learn more about "),
|
||||
html.A(
|
||||
html.Class("link"),
|
||||
html.Href("https://en.wikipedia.org/wiki/Cron"),
|
||||
html.Target("_blank"),
|
||||
gomponents.Text("cron expressions"),
|
||||
lucide.ExternalLink(html.Class("inline ml-1")),
|
||||
),
|
||||
gomponents.Text(" and "),
|
||||
html.A(
|
||||
html.Class("link"),
|
||||
html.Href("https://crontab.guru/examples.html"),
|
||||
html.Target("_blank"),
|
||||
gomponents.Text("see some examples"),
|
||||
lucide.ExternalLink(html.Class("inline ml-1")),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
component.InputControl(component.InputControlParams{
|
||||
Name: "cron_expression",
|
||||
Label: "Cron expression",
|
||||
Placeholder: "* * * * *",
|
||||
Required: true,
|
||||
Type: component.InputTypeText,
|
||||
HelpText: "The cron expression to schedule the backup",
|
||||
Pattern: `^\S+\s+\S+\s+\S+\s+\S+\s+\S+$`,
|
||||
HelpButtonChildren: cronExpressionHelp(),
|
||||
}),
|
||||
|
||||
component.SelectControl(component.SelectControlParams{
|
||||
Name: "time_zone",
|
||||
Label: "Time zone",
|
||||
@@ -190,25 +191,27 @@ func createBackupForm(
|
||||
}),
|
||||
|
||||
component.InputControl(component.InputControlParams{
|
||||
Name: "dest_dir",
|
||||
Label: "Destination directory",
|
||||
Placeholder: "/path/to/backup",
|
||||
Required: true,
|
||||
Type: component.InputTypeText,
|
||||
HelpText: "The directory where the backups will be stored",
|
||||
Name: "dest_dir",
|
||||
Label: "Destination directory",
|
||||
Placeholder: "/path/to/backup",
|
||||
Required: true,
|
||||
Type: component.InputTypeText,
|
||||
HelpText: "Relative to the base directory of the destination",
|
||||
Pattern: `^\/\S*[^\/]$`,
|
||||
HelpButtonChildren: destinationDirectoryHelp(),
|
||||
}),
|
||||
|
||||
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 backups. It is evaluated by execution and all backups before this will be deleted. Use 0 to keep them indefinitely",
|
||||
Name: "retention_days",
|
||||
Label: "Retention days",
|
||||
Placeholder: "30",
|
||||
Required: true,
|
||||
Type: component.InputTypeNumber,
|
||||
Pattern: "[0-9]+",
|
||||
HelpButtonChildren: retentionDaysHelp(),
|
||||
Children: []gomponents.Node{
|
||||
html.Min("0"),
|
||||
html.Max("36500"),
|
||||
html.Pattern("[0-9]+"),
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -224,18 +227,15 @@ func createBackupForm(
|
||||
|
||||
html.Div(
|
||||
html.Class("pt-4"),
|
||||
component.H2Text("Options"),
|
||||
component.PText("These options are passed to the pg_dump command."),
|
||||
html.P(
|
||||
gomponents.Text("Learn more in the "),
|
||||
html.A(
|
||||
html.Class("link"),
|
||||
html.Href("https://www.postgresql.org/docs/current/app-pgdump.html"),
|
||||
html.Target("_blank"),
|
||||
component.SpanText("pg_dump documentation"),
|
||||
lucide.ExternalLink(html.Class("inline ml-1")),
|
||||
),
|
||||
html.Div(
|
||||
html.Class("flex justify-start items-center space-x-1"),
|
||||
component.H2Text("Options"),
|
||||
component.HelpButtonModal(component.HelpButtonModalParams{
|
||||
ModalTitle: "Backup options",
|
||||
Children: pgDumpOptionsHelp(),
|
||||
}),
|
||||
),
|
||||
|
||||
html.Div(
|
||||
html.Class("mt-2 grid grid-cols-2 gap-2"),
|
||||
|
||||
|
||||
@@ -100,45 +100,24 @@ func editBackupButton(backup dbgen.BackupsServicePaginateBackupsRow) gomponents.
|
||||
Placeholder: "My backup",
|
||||
Required: true,
|
||||
Type: component.InputTypeText,
|
||||
HelpText: "A name to easily identify the backup",
|
||||
Children: []gomponents.Node{
|
||||
html.Value(backup.Name),
|
||||
},
|
||||
}),
|
||||
|
||||
html.Div(
|
||||
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.Pattern(`^\S+\s+\S+\s+\S+\s+\S+\s+\S+$`),
|
||||
html.Value(backup.CronExpression),
|
||||
},
|
||||
}),
|
||||
html.P(
|
||||
html.Class("pl-1"),
|
||||
gomponents.Text("Learn more about "),
|
||||
html.A(
|
||||
html.Class("link"),
|
||||
html.Href("https://en.wikipedia.org/wiki/Cron"),
|
||||
html.Target("_blank"),
|
||||
gomponents.Text("cron expressions"),
|
||||
lucide.ExternalLink(html.Class("inline ml-1")),
|
||||
),
|
||||
gomponents.Text(" and "),
|
||||
html.A(
|
||||
html.Class("link"),
|
||||
html.Href("https://crontab.guru/examples.html"),
|
||||
html.Target("_blank"),
|
||||
gomponents.Text("see some examples"),
|
||||
lucide.ExternalLink(html.Class("inline ml-1")),
|
||||
),
|
||||
),
|
||||
),
|
||||
component.InputControl(component.InputControlParams{
|
||||
Name: "cron_expression",
|
||||
Label: "Cron expression",
|
||||
Placeholder: "* * * * *",
|
||||
Required: true,
|
||||
Type: component.InputTypeText,
|
||||
HelpText: "The cron expression to schedule the backup",
|
||||
Pattern: `^\S+\s+\S+\s+\S+\s+\S+\s+\S+$`,
|
||||
Children: []gomponents.Node{
|
||||
html.Value(backup.CronExpression),
|
||||
},
|
||||
HelpButtonChildren: cronExpressionHelp(),
|
||||
}),
|
||||
|
||||
component.SelectControl(component.SelectControlParams{
|
||||
Name: "time_zone",
|
||||
@@ -164,28 +143,30 @@ func editBackupButton(backup dbgen.BackupsServicePaginateBackupsRow) gomponents.
|
||||
}),
|
||||
|
||||
component.InputControl(component.InputControlParams{
|
||||
Name: "dest_dir",
|
||||
Label: "Destination directory",
|
||||
Placeholder: "/path/to/backup",
|
||||
Required: true,
|
||||
Type: component.InputTypeText,
|
||||
HelpText: "The directory where the backups will be stored",
|
||||
Name: "dest_dir",
|
||||
Label: "Destination directory",
|
||||
Placeholder: "/path/to/backup",
|
||||
Required: true,
|
||||
Type: component.InputTypeText,
|
||||
HelpText: "Relative to the base directory of the destination",
|
||||
HelpButtonChildren: destinationDirectoryHelp(),
|
||||
Pattern: `^\/\S*[^\/]$`,
|
||||
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 backups. It is evaluated by execution and all backups before this will be deleted. Use 0 to keep them indefinitely",
|
||||
Name: "retention_days",
|
||||
Label: "Retention days",
|
||||
Placeholder: "30",
|
||||
Required: true,
|
||||
Type: component.InputTypeNumber,
|
||||
Pattern: "[0-9]+",
|
||||
HelpButtonChildren: retentionDaysHelp(),
|
||||
Children: []gomponents.Node{
|
||||
html.Min("0"),
|
||||
html.Max("36500"),
|
||||
html.Pattern("[0-9]+"),
|
||||
html.Value(fmt.Sprintf("%d", backup.RetentionDays)),
|
||||
},
|
||||
}),
|
||||
@@ -201,17 +182,13 @@ func editBackupButton(backup dbgen.BackupsServicePaginateBackupsRow) gomponents.
|
||||
|
||||
html.Div(
|
||||
html.Class("pt-4"),
|
||||
component.H2Text("Options"),
|
||||
component.PText("These options are passed to the pg_dump command."),
|
||||
html.P(
|
||||
gomponents.Text("Learn more in the "),
|
||||
html.A(
|
||||
html.Class("link"),
|
||||
html.Href("https://www.postgresql.org/docs/current/app-pgdump.html"),
|
||||
html.Target("_blank"),
|
||||
component.SpanText("pg_dump documentation"),
|
||||
lucide.ExternalLink(html.Class("inline ml-1")),
|
||||
),
|
||||
html.Div(
|
||||
html.Class("flex justify-start items-center space-x-1"),
|
||||
component.H2Text("Options"),
|
||||
component.HelpButtonModal(component.HelpButtonModalParams{
|
||||
ModalTitle: "Backup options",
|
||||
Children: pgDumpOptionsHelp(),
|
||||
}),
|
||||
),
|
||||
|
||||
html.Div(
|
||||
|
||||
@@ -94,7 +94,15 @@ func listBackups(
|
||||
),
|
||||
),
|
||||
html.Td(component.SpanText(backup.DatabaseName)),
|
||||
html.Td(component.SpanText(backup.DestinationName)),
|
||||
html.Td(component.SpanText(func() string {
|
||||
if backup.DestinationName.Valid {
|
||||
return backup.DestinationName.String
|
||||
}
|
||||
if backup.IsLocal {
|
||||
return "Local"
|
||||
}
|
||||
return "Unknown"
|
||||
}())),
|
||||
html.Td(
|
||||
html.Class("font-mono"),
|
||||
html.Div(
|
||||
|
||||
Reference in New Issue
Block a user