Add new UI
This commit is contained in:
parent
9d861f2180
commit
ba3ff0bd5e
11 changed files with 175 additions and 48 deletions
1
go.mod
1
go.mod
|
@ -17,4 +17,5 @@ require (
|
|||
go.uber.org/zap v1.26.0 // indirect
|
||||
golang.org/x/sys v0.1.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
maragu.dev/gomponents v1.0.0 // indirect
|
||||
)
|
||||
|
|
2
go.sum
2
go.sum
|
@ -33,3 +33,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
|
|||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
maragu.dev/gomponents v1.0.0 h1:eeLScjq4PqP1l+r5z/GC+xXZhLHXa6RWUWGW7gSfLh4=
|
||||
maragu.dev/gomponents v1.0.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM=
|
||||
|
|
|
@ -89,14 +89,14 @@ func makeErrorHandler(statusCode int, message string) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func (c *Core) RouteRequest(rq *http.Request) http.Handler {
|
||||
func (c *Core) RouteRequest(rq *http.Request) (http.Handler, error) {
|
||||
c.routeLock.RLock()
|
||||
defer c.routeLock.RUnlock()
|
||||
|
||||
host := strings.ToLower(rq.Host)
|
||||
|
||||
if host == "" {
|
||||
return invalidRequestHandler
|
||||
return invalidRequestHandler, nil
|
||||
}
|
||||
|
||||
c.Logger.Debug("routing request", "host", host, "path", rq.URL.Path, "foundRoutes", c.knownRoutes[host])
|
||||
|
@ -110,23 +110,23 @@ func (c *Core) RouteRequest(rq *http.Request) http.Handler {
|
|||
if h, found := c.handlerCache[v.ContentPath]; found {
|
||||
c.Logger.Debug("handler cache HIT", "content", v.ContentPath)
|
||||
c.handlerCacheLock.Unlock()
|
||||
return h.Handler
|
||||
return h.Handler, nil
|
||||
}
|
||||
c.Logger.Debug("handler cache MISS", "content", v.ContentPath)
|
||||
|
||||
fi, err := os.Stat(p)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, fmt.Errorf("stat content file: %w", err)
|
||||
}
|
||||
|
||||
f, err := os.Open(p)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, fmt.Errorf("open content file: %w", err)
|
||||
}
|
||||
|
||||
zr, err := zip.NewReader(f, fi.Size())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return nil, fmt.Errorf("create ZIP reader: %w", err)
|
||||
}
|
||||
|
||||
fs := http.FileServerFS(zr)
|
||||
|
@ -147,9 +147,9 @@ func (c *Core) RouteRequest(rq *http.Request) http.Handler {
|
|||
|
||||
c.handlerCacheLock.Unlock()
|
||||
|
||||
return handler
|
||||
return handler, nil
|
||||
}
|
||||
}
|
||||
|
||||
return notFoundHandler
|
||||
return notFoundHandler, nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/codemicro/palmatum/palmatum/internal/database"
|
||||
|
@ -78,8 +79,10 @@ func (c *Core) DeleteSite(siteSlug string) error {
|
|||
return fmt.Errorf("commit transaction: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Remove(c.getPathOnDisk(contentPath)); err != nil {
|
||||
return fmt.Errorf("remove path: %w", err)
|
||||
if contentPath != "" {
|
||||
if err := os.Remove(c.getPathOnDisk(contentPath)); err != nil {
|
||||
return fmt.Errorf("remove path: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.BuildKnownRoutes(); err != nil {
|
||||
|
@ -99,11 +102,24 @@ func (c *Core) UpdateSite(s *database.SiteModel) error {
|
|||
|
||||
var (
|
||||
ErrInvalidDomain = newError("invalid domain")
|
||||
ErrInvalidPath = newError("invalid path")
|
||||
ErrInvalidPath = newError("invalid path (must start with /)")
|
||||
ErrRouteNotUnique = newError("route is not unique")
|
||||
)
|
||||
|
||||
func (c *Core) CreateRoute(siteSlug, domain, path string) (*database.RouteModel, error) {
|
||||
tx, err := c.Database.Beginx()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("begin database transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
if _, err := database.GetSite(tx, siteSlug); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrInvalidSlug
|
||||
}
|
||||
return nil, fmt.Errorf("get site from database: %w", err)
|
||||
}
|
||||
|
||||
domain = strings.TrimSpace(domain)
|
||||
|
||||
if domain == "" {
|
||||
|
@ -118,7 +134,7 @@ func (c *Core) CreateRoute(siteSlug, domain, path string) (*database.RouteModel,
|
|||
|
||||
var id int
|
||||
|
||||
if err := c.Database.QueryRowx("INSERT INTO routes(site, domain, path) VALUES (?, ?, ?) RETURNING id", siteSlug, domain, path).Scan(&id); err != nil {
|
||||
if err := tx.QueryRowx("INSERT INTO routes(site, domain, path) VALUES (?, ?, ?) RETURNING id", siteSlug, domain, path).Scan(&id); err != nil {
|
||||
var e sqlite3.Error
|
||||
if errors.As(err, &e) {
|
||||
if e.ExtendedCode == sqlite3.ErrConstraintForeignKey {
|
||||
|
@ -131,6 +147,10 @@ func (c *Core) CreateRoute(siteSlug, domain, path string) (*database.RouteModel,
|
|||
return nil, fmt.Errorf("call database: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, fmt.Errorf("commit databse transaction: %w", err)
|
||||
}
|
||||
|
||||
if err := c.BuildKnownRoutes(); err != nil {
|
||||
return nil, fmt.Errorf("rebuild known routes: %w", err)
|
||||
}
|
||||
|
|
|
@ -7,13 +7,15 @@ import (
|
|||
"github.com/codemicro/palmatum/palmatum/internal/config"
|
||||
"github.com/codemicro/palmatum/palmatum/internal/core"
|
||||
"github.com/codemicro/palmatum/palmatum/internal/database"
|
||||
"go.uber.org/fx"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func NewManagementServer(args ServerArgs) *http.Server {
|
||||
func NewManagementServer(lc fx.Lifecycle, args ServerArgs) *http.Server {
|
||||
mux := http.NewServeMux()
|
||||
mr := managementRoutes{
|
||||
logger: args.Logger,
|
||||
|
@ -21,6 +23,10 @@ func NewManagementServer(args ServerArgs) *http.Server {
|
|||
config: args.Config,
|
||||
}
|
||||
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: mr.initManagementTemplates,
|
||||
})
|
||||
|
||||
mux.HandleFunc("POST /api/site", handleErrors(args.Logger, mr.apiCreateSite))
|
||||
mux.HandleFunc("POST /api/site/bundle", handleErrors(args.Logger, mr.apiUploadSiteBundle))
|
||||
mux.HandleFunc("DELETE /api/site", handleErrors(args.Logger, mr.apiDeleteSite))
|
||||
|
@ -28,6 +34,8 @@ func NewManagementServer(args ServerArgs) *http.Server {
|
|||
mux.HandleFunc("DELETE /api/site/route", handleErrors(args.Logger, mr.apiDeleteRoute))
|
||||
|
||||
mux.HandleFunc("GET /{$}", handleErrors(args.Logger, mr.index))
|
||||
mux.HandleFunc("GET /uploadSite", handleErrors(args.Logger, mr.uploadSitePartial))
|
||||
mux.HandleFunc("GET /addRoute", handleErrors(args.Logger, mr.addRoutePartial))
|
||||
|
||||
return newServer(args, args.Config.HTTP.ManagementAddress, mux)
|
||||
}
|
||||
|
@ -36,11 +44,8 @@ type managementRoutes struct {
|
|||
logger *slog.Logger
|
||||
core *core.Core
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func (mr *managementRoutes) index(rw http.ResponseWriter, rq *http.Request) error {
|
||||
rw.Write([]byte("hello!"))
|
||||
return nil
|
||||
templates *template.Template
|
||||
}
|
||||
|
||||
func (mr *managementRoutes) apiCreateSite(rw http.ResponseWriter, rq *http.Request) error {
|
||||
|
@ -62,6 +67,7 @@ func (mr *managementRoutes) apiCreateSite(rw http.ResponseWriter, rq *http.Reque
|
|||
return fmt.Errorf("create new site: %w", err)
|
||||
}
|
||||
|
||||
rw.Header().Set("HX-Refresh", "true")
|
||||
rw.WriteHeader(http.StatusCreated)
|
||||
return nil
|
||||
}
|
||||
|
@ -79,6 +85,7 @@ func (mr *managementRoutes) apiDeleteSite(rw http.ResponseWriter, rq *http.Reque
|
|||
return fmt.Errorf("delete site: %w", err)
|
||||
}
|
||||
|
||||
rw.Header().Set("HX-Refresh", "true")
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return nil
|
||||
}
|
||||
|
@ -128,13 +135,12 @@ func (mr *managementRoutes) apiUploadSiteBundle(rw http.ResponseWriter, rq *http
|
|||
return fmt.Errorf("update site: %w", err)
|
||||
}
|
||||
|
||||
rw.Header().Set("HX-Refresh", "true")
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mr *managementRoutes) apiCreateRoute(rw http.ResponseWriter, rq *http.Request) error {
|
||||
// TODO: check the route in question actually exist
|
||||
|
||||
siteSlug := rq.FormValue("slug")
|
||||
domain := rq.FormValue("domain")
|
||||
path := rq.FormValue("path")
|
||||
|
@ -151,6 +157,7 @@ func (mr *managementRoutes) apiCreateRoute(rw http.ResponseWriter, rq *http.Requ
|
|||
return fmt.Errorf("create new route: %w", err)
|
||||
}
|
||||
|
||||
rw.Header().Set("HX-Refresh", "true")
|
||||
rw.WriteHeader(http.StatusCreated)
|
||||
return nil
|
||||
}
|
||||
|
@ -167,6 +174,7 @@ func (mr *managementRoutes) apiDeleteRoute(rw http.ResponseWriter, rq *http.Requ
|
|||
return fmt.Errorf("delete route: %w", err)
|
||||
}
|
||||
|
||||
rw.Header().Set("HX-Refresh", "true")
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Palmatum management portal</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.tedted.dev/neat/neat.css">
|
||||
<script src="https://unpkg.com/htmx.org@1.9.4" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Palmatum Management Portal</h1>
|
||||
<h2>Active sites</h2>
|
||||
<ul>
|
||||
{{ range $name := .ActiveSites }}<li><a href="/{{ $name }}">{{ $name }}</a> <a href="" hx-post="/-/delete" hx-vals='{"siteName": "{{ $name }}"}' hx-confirm="Are you sure you want to delete '{{ $name }}'?">[x]</a></li>{{ else }}<li><i>No active sites</i></li>{{ end }}
|
||||
</ul>
|
||||
<h2>Upload new site bundle</h2>
|
||||
<form action="/-/upload" method="post" enctype="multipart/form-data">
|
||||
<label for="siteNameInput">Site name/path: </label>
|
||||
<input type="text" name="siteName" id="siteNameInput">
|
||||
<br>
|
||||
<label for="archiveInput">Site archive: </label>
|
||||
<input type="file" name="archive" id="archiveInput">
|
||||
<br>
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
49
palmatum/internal/httpsrv/managementUI.go
Normal file
49
palmatum/internal/httpsrv/managementUI.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package httpsrv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/codemicro/palmatum/palmatum/internal/database"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed templates/*
|
||||
var managementTemplateSource embed.FS
|
||||
|
||||
func (mr *managementRoutes) initManagementTemplates(_ context.Context) error {
|
||||
f, err := fs.Sub(fs.FS(managementTemplateSource), "templates")
|
||||
if err != nil {
|
||||
return fmt.Errorf("subset filesystem: %w", err)
|
||||
}
|
||||
|
||||
mr.templates, err = mr.templates.ParseFS(f, "*.html")
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse templates: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mr *managementRoutes) index(rw http.ResponseWriter, rq *http.Request) error {
|
||||
var templateData = struct {
|
||||
Sites []*database.SiteModel
|
||||
}{}
|
||||
|
||||
s, err := database.GetSitesWithRoutes(mr.core.Database)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get sites with routes: %w", err)
|
||||
}
|
||||
templateData.Sites = s
|
||||
|
||||
return mr.templates.ExecuteTemplate(rw, "index.html", &templateData)
|
||||
}
|
||||
|
||||
func (mr *managementRoutes) uploadSitePartial(rw http.ResponseWriter, rq *http.Request) error {
|
||||
return mr.templates.ExecuteTemplate(rw, "uploadSite.html", rq.URL.Query().Get("slug"))
|
||||
}
|
||||
|
||||
func (mr *managementRoutes) addRoutePartial(rw http.ResponseWriter, rq *http.Request) error {
|
||||
return mr.templates.ExecuteTemplate(rw, "addRoute.html", rq.URL.Query().Get("slug"))
|
||||
}
|
|
@ -1,11 +1,17 @@
|
|||
package httpsrv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func NewSitesServer(args ServerArgs) *http.Server {
|
||||
return newServer(args, args.Config.HTTP.SitesAddress, http.HandlerFunc(func(rw http.ResponseWriter, rq *http.Request) {
|
||||
args.Core.RouteRequest(rq).ServeHTTP(rw, rq)
|
||||
return newServer(args, args.Config.HTTP.SitesAddress, handleErrors(args.Logger, func(rw http.ResponseWriter, rq *http.Request) error {
|
||||
h, err := args.Core.RouteRequest(rq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("handle sites request: %w", err)
|
||||
}
|
||||
h.ServeHTTP(rw, rq)
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
|
13
palmatum/internal/httpsrv/templates/addRoute.html
Normal file
13
palmatum/internal/httpsrv/templates/addRoute.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<div>
|
||||
<a href="/">[go back]</a>
|
||||
<h2>Add route to {{ . }}</h2>
|
||||
<form hx-post="/api/site/route" hx-vals='{"slug": "{{ js . }}"}'>
|
||||
<label for="domainBox">Domain*: </label>
|
||||
<input type="text" name="domain" id="domainBox">
|
||||
<br>
|
||||
<label for="pathBox">Path: </label>
|
||||
<input type="text" name="path" placeholder="/" id="pathBox">
|
||||
<br>
|
||||
<input type="submit" value="Save">
|
||||
</form>
|
||||
</div>
|
46
palmatum/internal/httpsrv/templates/index.html
Normal file
46
palmatum/internal/httpsrv/templates/index.html
Normal file
|
@ -0,0 +1,46 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Palmatum management portal</title>
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.tedted.dev/neat/neat.css">
|
||||
<script src="https://unpkg.com/htmx.org@1.9.4" defer></script>
|
||||
<script defer>
|
||||
document.addEventListener("htmx:responseError", (evt) => {
|
||||
alert(`${evt.detail.xhr.statusText} - ${evt.detail.xhr.responseText}`)
|
||||
})
|
||||
</script>
|
||||
</head>
|
||||
<body hx-encoding="multipart/form-data">
|
||||
<h1>Palmatum Management Portal</h1>
|
||||
<div id="swapBox">
|
||||
<h2>Active sites</h2>
|
||||
<ul>
|
||||
{{ range .Sites }}
|
||||
<li>{{ .Slug }}
|
||||
<a hx-delete="/api/site" hx-vals='{"slug": "{{ js .Slug }}"}' hx-confirm="Are you sure you want to delete {{ js .Slug }}?" href="">[delete]</a>
|
||||
<a hx-get="/uploadSite" hx-vals='{"slug": "{{ js .Slug }}"}' hx-target="#swapBox" hx-swap="outerHTML" href="">[upload]</a>
|
||||
{{ if eq .ContentPath "" }}<span style="color: red; font-style: italic;">no site uploaded</span>{{ end }}
|
||||
<ul>
|
||||
{{ range .Routes }}
|
||||
<li><a href="//{{ .Domain }}{{ .Path }}">{{ .Domain }}{{ .Path }}</a> <a hx-delete="/api/site/route" hx-vals='{"id": {{ .ID }}}' hx-confirm="Are you sure you want to delete {{ .Domain }}{{ .Path }}?" href="">[delete]</a></li>
|
||||
{{ else }}
|
||||
<li><span style="color: red; font-style: italic">no routes</span></li>
|
||||
{{ end }}
|
||||
<li><a hx-get="/addRoute" hx-vals='{"slug": "{{ js .Slug }}"}' hx-target="#swapBox" hx-swap="outerHTML" href="">[add route]</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{{ else }}
|
||||
<li><span style="color: red; font-style: italic">no sites</span></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<h2>Create new site</h2>
|
||||
<form hx-post="/api/site">
|
||||
<label for="siteNameInput">Site slug: </label>
|
||||
<input type="text" name="slug" id="siteNameInput">
|
||||
<input type="submit" value="Submit">
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
9
palmatum/internal/httpsrv/templates/uploadSite.html
Normal file
9
palmatum/internal/httpsrv/templates/uploadSite.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<div>
|
||||
<a href="/">[go back]</a>
|
||||
<h2>Upload to {{ . }}</h2>
|
||||
<form hx-post="/api/site/bundle" hx-vals='{"slug": "{{ js . }}"}'>
|
||||
<label for="siteBundleBox">Site bundle: </label>
|
||||
<input type="file" name="bundle" id="siteBundleBox">
|
||||
<input type="submit" value="Upload">
|
||||
</form>
|
||||
</div>
|
Loading…
Add table
Add a link
Reference in a new issue