garm/webapp/assets/assets.go
Gabriel Adrian Samfira eec158b32c Add SPA UI for GARM
This change adds a single page application front-end to GARM. It uses
a generated REST client, built from the swagger definitions, the websocket
interface for live updates of entities and eager loading of everything
except runners, as users may have many runners and we don't want to load
hundreds of runners in memory.

Proper pagination should be implemented in the API, in future commits,
to avoid loading lots of elements for no reason.

Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
2025-08-16 09:09:13 +00:00

83 lines
2.5 KiB
Go

package assets
import (
"embed"
"net/http"
"path/filepath"
"strings"
)
//go:generate go run github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0 generate spec --output=../swagger.yaml --scan-models --work-dir=../../
//go:generate go run github.com/go-swagger/go-swagger/cmd/swagger@v0.31.0 validate ../swagger.yaml
//go:generate rm -rf ../src/lib/api/generated
//go:generate openapi-generator-cli generate --skip-validate-spec -i ../swagger.yaml -g typescript-axios -o ../src/lib/api/generated
//go:embed all:*
var EmbeddedSPA embed.FS
// GetSPAFileSystem returns the embedded SPA file system for use with http.FileServer
func GetSPAFileSystem() http.FileSystem {
return http.FS(EmbeddedSPA)
}
// ServeSPA serves the embedded SPA with proper content types and SPA routing
// This is kept for backward compatibility
func ServeSPA(w http.ResponseWriter, r *http.Request) {
ServeSPAWithPath(w, r, "/ui/")
}
// ServeSPAWithPath serves the embedded SPA with a custom webapp path
func ServeSPAWithPath(w http.ResponseWriter, r *http.Request, webappPath string) {
filename := strings.TrimPrefix(r.URL.Path, webappPath)
// Handle root path and SPA routing - serve index.html for all routes
if filename == "" || !strings.Contains(filename, ".") {
filename = "index.html"
}
// Security check - prevent directory traversal
if strings.Contains(filename, "..") {
http.NotFound(w, r)
return
}
// Read file from embedded filesystem
content, err := EmbeddedSPA.ReadFile(filename)
if err != nil {
// If file not found, serve index.html for SPA routing
content, err = EmbeddedSPA.ReadFile("index.html")
if err != nil {
http.NotFound(w, r)
return
}
filename = "index.html"
}
// Set appropriate content type based on file extension
ext := strings.ToLower(filepath.Ext(filename))
switch ext {
case ".html":
w.Header().Set("Content-Type", "text/html; charset=utf-8")
case ".js":
w.Header().Set("Content-Type", "application/javascript")
case ".css":
w.Header().Set("Content-Type", "text/css")
case ".json":
w.Header().Set("Content-Type", "application/json")
case ".svg":
w.Header().Set("Content-Type", "image/svg+xml")
case ".png":
w.Header().Set("Content-Type", "image/png")
default:
w.Header().Set("Content-Type", "text/plain")
}
// Set cache headers for static assets (but not for HTML to ensure fresh content)
if ext != ".html" {
w.Header().Set("Cache-Control", "public, max-age=3600")
} else {
w.Header().Set("Cache-Control", "no-cache, must-revalidate")
}
w.Write(content)
}