mirror of
				https://github.com/ClusterCockpit/cc-backend
				synced 2025-11-04 09:35:07 +01:00 
			
		
		
		
	embed frontend files into binary (issue #2)
This commit is contained in:
		@@ -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,7 +54,10 @@ 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
 | 
				
			||||||
 | 
						// 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"`
 | 
						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)
 | 
				
			||||||
@@ -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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if programConfig.EmbedStaticFiles {
 | 
				
			||||||
 | 
							r.PathPrefix("/").Handler(web.ServeFiles())
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
		r.PathPrefix("/").Handler(http.FileServer(http.Dir(programConfig.StaticFiles)))
 | 
							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(
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								web/frontend/public/uPlot.min.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								web/frontend/public/uPlot.min.css
									
									
									
									
										vendored
									
									
								
							@@ -1 +0,0 @@
 | 
				
			|||||||
../node_modules/uplot/dist/uPlot.min.css
 | 
					 | 
				
			||||||
							
								
								
									
										1
									
								
								web/frontend/public/uPlot.min.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web/frontend/public/uPlot.min.css
									
									
									
									
										vendored
									
									
										Normal 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
									
								
							
							
						
						
									
										82
									
								
								web/web.go
									
									
									
									
									
										Normal 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())
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user