Update backups forms with is_local option and better docs

This commit is contained in:
Luis Eduardo Jeréz Girón
2024-08-03 21:28:35 -06:00
parent ed929cc931
commit fe7344e7b2
4 changed files with 256 additions and 124 deletions

View 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")),
),
),
),
}
}

View File

@@ -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"),

View File

@@ -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(

View File

@@ -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(