embed frontend files into binary (issue #2)

This commit is contained in:
Lou Knauer 2022-07-06 14:55:39 +02:00
parent c1b7c8c6ae
commit 4f61580b2b
3 changed files with 96 additions and 5 deletions

View File

@ -33,6 +33,7 @@ import (
"github.com/ClusterCockpit/cc-backend/internal/runtimeEnv" "github.com/ClusterCockpit/cc-backend/internal/runtimeEnv"
"github.com/ClusterCockpit/cc-backend/internal/templates" "github.com/ClusterCockpit/cc-backend/internal/templates"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/web"
"github.com/google/gops/agent" "github.com/google/gops/agent"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -53,8 +54,11 @@ type ProgramConfig struct {
// Disable authentication (for everything: API, Web-UI, ...) // Disable authentication (for everything: API, Web-UI, ...)
DisableAuthentication bool `json:"disable-authentication"` DisableAuthentication bool `json:"disable-authentication"`
// Folder where static assets can be found, will be served directly // If `embed-static-files` is true (default), the frontend files are directly
StaticFiles string `json:"static-files"` // embeded into the go binary and expected to be in web/frontend. Only if
// it is false the files in `static-files` are served instead.
EmbedStaticFiles bool `json:"embed-static-files"`
StaticFiles string `json:"static-files"`
// 'sqlite3' or 'mysql' (mysql will work for mariadb as well) // 'sqlite3' or 'mysql' (mysql will work for mariadb as well)
DBDriver string `json:"db-driver"` DBDriver string `json:"db-driver"`
@ -100,7 +104,7 @@ type ProgramConfig struct {
var programConfig ProgramConfig = ProgramConfig{ var programConfig ProgramConfig = ProgramConfig{
Addr: ":8080", Addr: ":8080",
DisableAuthentication: false, DisableAuthentication: false,
StaticFiles: "./web/frontend/public", EmbedStaticFiles: true,
DBDriver: "sqlite3", DBDriver: "sqlite3",
DB: "./var/job.db", DB: "./var/job.db",
JobArchive: "./var/job-archive", JobArchive: "./var/job-archive",
@ -379,7 +383,12 @@ func main() {
routerConfig.SetupRoutes(secured) routerConfig.SetupRoutes(secured)
api.MountRoutes(secured) api.MountRoutes(secured)
r.PathPrefix("/").Handler(http.FileServer(http.Dir(programConfig.StaticFiles))) if programConfig.EmbedStaticFiles {
r.PathPrefix("/").Handler(web.ServeFiles())
} else {
r.PathPrefix("/").Handler(http.FileServer(http.Dir(programConfig.StaticFiles)))
}
r.Use(handlers.CompressHandler) r.Use(handlers.CompressHandler)
r.Use(handlers.RecoveryHandler(handlers.PrintRecoveryStack(true))) r.Use(handlers.RecoveryHandler(handlers.PrintRecoveryStack(true)))
r.Use(handlers.CORS( r.Use(handlers.CORS(

View File

@ -1 +0,0 @@
../node_modules/uplot/dist/uPlot.min.css

1
web/frontend/public/uPlot.min.css vendored Normal file
View File

@ -0,0 +1 @@
.uplot, .uplot *, .uplot *::before, .uplot *::after {box-sizing: border-box;}.uplot {font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height: 1.5;width: min-content;}.u-title {text-align: center;font-size: 18px;font-weight: bold;}.u-wrap {position: relative;user-select: none;}.u-over, .u-under {position: absolute;}.u-under {overflow: hidden;}.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}.u-axis {position: absolute;}.u-legend {font-size: 14px;margin: auto;text-align: center;}.u-inline {display: block;}.u-inline * {display: inline-block;}.u-inline tr {margin-right: 16px;}.u-legend th {font-weight: 600;}.u-legend th > * {vertical-align: middle;display: inline-block;}.u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}.u-inline.u-live th::after {content: ":";vertical-align: middle;}.u-inline:not(.u-live) .u-value {display: none;}.u-series > * {padding: 4px;}.u-series th {cursor: pointer;}.u-legend .u-off > * {opacity: 0.3;}.u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}.u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;z-index: 100;}.u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}.u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}.u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;z-index: 100;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}.u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;}

82
web/web.go Normal file
View File

@ -0,0 +1,82 @@
package web
import (
"embed"
"html/template"
"io/fs"
"net/http"
"strings"
"github.com/ClusterCockpit/cc-backend/internal/config"
"github.com/ClusterCockpit/cc-backend/pkg/log"
)
/// Go's embed is only allowed to embed files in a subdirectory of the embedding package ([see here](https://github.com/golang/go/issues/46056)).
//go:embed frontend/public/*
var frontendFiles embed.FS
func ServeFiles() http.Handler {
publicFiles, err := fs.Sub(frontendFiles, "frontend/public")
if err != nil {
panic(err)
}
return http.FileServer(http.FS(publicFiles))
}
//go:embed templates/*
var templateFiles embed.FS
var templates map[string]*template.Template = map[string]*template.Template{}
func init() {
base := template.Must(template.ParseFS(templateFiles, "templates/base.tmpl"))
if err := fs.WalkDir(templateFiles, "templates", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() || path == "templates/base.tmpl" {
return nil
}
templates[strings.TrimPrefix(path, "templates/")] = template.Must(template.Must(base.Clone()).ParseFS(templateFiles, path))
return nil
}); err != nil {
panic(err)
}
_ = base
}
type User struct {
Username string // Username of the currently logged in user
IsAdmin bool
}
type Page struct {
Title string // Page title
Error string // For generic use (e.g. the exact error message on /login)
Info string // For generic use (e.g. "Logout successfull" on /login)
User User // Information about the currently logged in user
Clusters []string // List of all clusters for use in the Header
FilterPresets map[string]interface{} // For pages with the Filter component, this can be used to set initial filters.
Infos map[string]interface{} // For generic use (e.g. username for /monitoring/user/<id>, job id for /monitoring/job/<id>)
Config map[string]interface{} // UI settings for the currently logged in user (e.g. line width, ...)
}
func RenderTemplate(rw http.ResponseWriter, r *http.Request, file string, page *Page) {
t, ok := templates[file]
if !ok {
panic("template not found")
}
if page.Clusters == nil {
for _, c := range config.Clusters {
page.Clusters = append(page.Clusters, c.Name)
}
}
if err := t.Execute(rw, page); err != nil {
log.Errorf("template error: %s", err.Error())
}
}