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>
83 lines
2.5 KiB
Go
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)
|
|
}
|