diff --git a/server/.env.example b/server/.env.example index 264595b245..1f15b17349 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1319,3 +1319,14 @@ # Type: string # Default: #162425 # AVATAR_INITIALS_COLOR=#162425 + +############################################################################### +############################################################################### +# webapp +# + +############################################################################### +# Path to custom SCSS source files directory +# Type: string +# Default: +# WEBAPP_SCSS_DIR_PATH= diff --git a/server/app/app.cue b/server/app/app.cue index 27744b159e..4c4f9da046 100644 --- a/server/app/app.cue +++ b/server/app/app.cue @@ -41,6 +41,7 @@ corteza: schema.#platform & { options.workflow, options.discovery, options.attachment, + options.webapp, ] // platform resources diff --git a/server/app/options/webapp.cue b/server/app/options/webapp.cue new file mode 100644 index 0000000000..dc1ccb5a45 --- /dev/null +++ b/server/app/options/webapp.cue @@ -0,0 +1,15 @@ +package options + +import ( + "github.com/cortezaproject/corteza/server/codegen/schema" +) + +webapp: schema.#optionsGroup & { + handle: "webapp" + + options: { + SCSS_dir_path: { + description: "Path to custom SCSS source files directory" + } + } +} diff --git a/server/app/servers.go b/server/app/servers.go index fe2b903cdc..a10fac8c63 100644 --- a/server/app/servers.go +++ b/server/app/servers.go @@ -57,7 +57,7 @@ func (app *CortezaApp) mountHttpRoutes(r chi.Router) { return } - r.Route(options.CleanBase(ho.WebappBaseUrl), webapp.MakeWebappServer(app.Log, ho, app.Opt.Auth, app.Opt.Discovery, app.Opt.Sentry)) + r.Route(options.CleanBase(ho.WebappBaseUrl), webapp.MakeWebappServer(app.Log, ho, app.Opt.Auth, app.Opt.Discovery, app.Opt.Sentry, app.Opt.Webapp)) app.Log.Info( "client web applications enabled", diff --git a/server/assets/embed.go b/server/assets/embed.go index 0d767b460c..06a6cd5b16 100644 --- a/server/assets/embed.go +++ b/server/assets/embed.go @@ -67,26 +67,26 @@ func fromPath(path string) (assets fs.FS, err error) { return } -func DirFileNames(dir string) (names []string, err error) { - files, err := fs.ReadDir(ff, "src/"+dir) +func DirEntries(dir string) (fileNames, subDirs []string, err error) { + dirEntries, err := fs.ReadDir(ff, "src/"+dir) if err != nil { - // something is seriously wrong, we might as well panic - panic(err) + return nil, nil, err } - for _, file := range files { - fileInfo, err := file.Info() + for _, dirEntry := range dirEntries { + fileInfo, err := dirEntry.Info() if err != nil { - return nil, err + return nil, nil, err } - // if the file is a directory skip it + // if the entry is a directory skip it if fileInfo.IsDir() { + subDirs = append(subDirs, dirEntry.Name()) continue } - names = append(names, file.Name()) + fileNames = append(fileNames, dirEntry.Name()) } - return names, nil + return fileNames, subDirs, err } diff --git a/server/pkg/options/options.gen.go b/server/pkg/options/options.gen.go index 035a3452b9..ad479105c7 100644 --- a/server/pkg/options/options.gen.go +++ b/server/pkg/options/options.gen.go @@ -268,6 +268,10 @@ type ( AvatarInitialsBackgroundColor string `env:"AVATAR_INITIALS_BACKGROUND_COLOR"` AvatarInitialsColor string `env:"AVATAR_INITIALS_COLOR"` } + + WebappOpt struct { + SCSSDirPath string `env:"WEBAPP_SCSS_DIR_PATH"` + } ) // DB initializes and returns a DBOpt with default values @@ -1100,3 +1104,28 @@ func Attachment() (o *AttachmentOpt) { return } + +// Webapp initializes and returns a WebappOpt with default values +// +// This function is auto-generated +func Webapp() (o *WebappOpt) { + o = &WebappOpt{} + + // Custom defaults + func(o interface{}) { + if def, ok := o.(interface{ Defaults() }); ok { + def.Defaults() + } + }(o) + + fill(o) + + // Custom cleanup + func(o interface{}) { + if def, ok := o.(interface{ Cleanup() }); ok { + def.Cleanup() + } + }(o) + + return +} diff --git a/server/pkg/options/options.go b/server/pkg/options/options.go index ad62bba9cc..e8f6b2e7d8 100644 --- a/server/pkg/options/options.go +++ b/server/pkg/options/options.go @@ -29,6 +29,7 @@ type ( Discovery DiscoveryOpt Apigw ApigwOpt Attachment AttachmentOpt + Webapp WebappOpt } ) @@ -61,5 +62,6 @@ func Init() *Options { Discovery: *Discovery(), Apigw: *Apigw(), Attachment: *Attachment(), + Webapp: *Webapp(), } } diff --git a/server/pkg/sass/processor.go b/server/pkg/sass/processor.go index 86c78f4c64..3cbb897546 100644 --- a/server/pkg/sass/processor.go +++ b/server/pkg/sass/processor.go @@ -17,7 +17,15 @@ import ( var stylesheet = newStylesheetCache() -func GenerateCSS(brandingSass, customCSS string) (string, error) { +// GenerateCSS takes care of creating CSS for webapps by reading SASS content from embedded assets, +// combining it with brandingSass and customCSS, and then transpiling it using the dart-sass compiler. +// +// If dart sass isn't installed on the host machine, it will default to css content from the minified-custom.css which is +// generated from [Boostrap, bootstrap-vue and custom variables sass content]. +// If dart isn't installed on the host machine, customCustom css will continue to function, but without sass support. +// +// In case of an error, it will return default css and log out the error +func GenerateCSS(brandingSass, customCSS, sassDirPath string) (string, error) { var ( stringsBuilder strings.Builder @@ -29,8 +37,8 @@ func GenerateCSS(brandingSass, customCSS string) (string, error) { return stylesheet.get("css"), nil } - // if dart sass is not installed, or when the SCSS transpiler creation and startup process fails. - // return contents from minified-custom.css + // if dart sass is not installed, or when the sass transpiler creation and startup process fails. + // return contents from default css if transpiler == nil { stringsBuilder.WriteString(defaultCSS()) if customCSS != "" { @@ -53,7 +61,8 @@ func GenerateCSS(brandingSass, customCSS string) (string, error) { if brandingSass != "" { err := jsonToSass(brandingSass, &stringsBuilder) if err != nil { - return "", err + logger.Default().Error("failed to unmarshal branding sass variables", zap.Error(err)) + return defaultCSS(), err } } @@ -68,11 +77,19 @@ func GenerateCSS(brandingSass, customCSS string) (string, error) { } // get Boostrap, bootstrap-vue and custom variables sass content - scssFromAssets, err := readSassFiles() + mainSass, err := readSassFiles("scss") if err != nil { - return "", err + return defaultCSS(), err + } + stringsBuilder.WriteString(mainSass) + + if sassDirPath != "" { + customSass, err := readSassFiles(sassDirPath) + if err != nil { + return defaultCSS(), err + } + stringsBuilder.WriteString(customSass) } - stringsBuilder.WriteString(scssFromAssets) if customCSS != "" { //Custom CSS editor selector block @@ -85,10 +102,9 @@ func GenerateCSS(brandingSass, customCSS string) (string, error) { Source: stringsBuilder.String(), } execute, err := transpiler.Execute(args) + // if there's an error during compilation process, return contents from minified-custom.css if err != nil { logger.Default().Error("Sass syntax error", zap.Error(err)) - - // return contents from minified-custom.css return defaultCSS(), err } @@ -110,40 +126,62 @@ func dartSass() *godartsass.Transpiler { }) if err != nil { - logger.Default().Info("dart sass is not installed in your system", zap.Error(err)) + logger.Default().Warn("dart sass is not installed in your system", zap.Error(err)) return nil } return transpiler } -// readSassFiles reads SASS files from assets and converts them to a string -func readSassFiles() (string, error) { +// readSassFiles reads SASS contents from provided embedded directory and subdirectories then converts them to a string +func readSassFiles(dirPath string) (string, error) { var stringsBuilder strings.Builder - assetFiles := assets.Files(logger.Default(), "") - fileNames, err := assets.DirFileNames("scss") + filenames, subDirs, err := assets.DirEntries(dirPath) if err != nil { - logger.Default().Error("failed to read assets/scss file names", zap.Error(err)) + logger.Default().Error(fmt.Sprintf("failed to read assets/src/%s entries", dirPath), zap.Error(err)) return "", err } - for _, fileName := range fileNames { - open, err := assetFiles.Open("scss/" + fileName) + if len(filenames) > 0 { + err := readSassContents(dirPath, filenames, &stringsBuilder) if err != nil { - logger.Default().Error(fmt.Sprintf("failed to open asset %s file", fileName), zap.Error(err)) return "", err } + } + + if len(subDirs) > 0 { + for _, subDir := range subDirs { + sassContents, err := readSassFiles(dirPath + "/" + subDir) + if err != nil { + return "", err + } + stringsBuilder.WriteString(sassContents) + } + } + + return stringsBuilder.String(), nil +} + +func readSassContents(dirPath string, filenames []string, stringsBuilder *strings.Builder) error { + assetFiles := assets.Files(logger.Default(), "") + + for _, fileName := range filenames { + open, err := assetFiles.Open(dirPath + "/" + fileName) + if err != nil { + logger.Default().Error(fmt.Sprintf("failed to open asset %s file", fileName), zap.Error(err)) + return err + } reader := bufio.NewReader(open) - _, err = io.Copy(&stringsBuilder, reader) + _, err = io.Copy(stringsBuilder, reader) if err != nil { - logger.Default().Error(fmt.Sprintf("failed to copy scss content from %s", fileName), zap.Error(err)) - return "", err + logger.Default().Error(fmt.Sprintf("failed to copy sass content from %s", fileName), zap.Error(err)) + return err } } - return stringsBuilder.String(), nil + return nil } func defaultCSS() string { diff --git a/server/pkg/webapp/serve.go b/server/pkg/webapp/serve.go index 371c679ec4..2b2c25ed06 100644 --- a/server/pkg/webapp/serve.go +++ b/server/pkg/webapp/serve.go @@ -27,6 +27,7 @@ type ( webappBaseUrl string discoveryApiBaseUrl string sentryUrl string + scssDirPath string settings *types.AppSettings } ) @@ -35,7 +36,7 @@ var ( baseHrefMatcher = regexp.MustCompile(``) ) -func MakeWebappServer(log *zap.Logger, httpSrvOpt options.HttpServerOpt, authOpt options.AuthOpt, discoveryOpt options.DiscoveryOpt, sentryOpt options.SentryOpt) func(r chi.Router) { +func MakeWebappServer(log *zap.Logger, httpSrvOpt options.HttpServerOpt, authOpt options.AuthOpt, discoveryOpt options.DiscoveryOpt, sentryOpt options.SentryOpt, webappOpt options.WebappOpt) func(r chi.Router) { var ( apiBaseUrl = options.CleanBase(httpSrvOpt.BaseUrl, httpSrvOpt.ApiBaseUrl) webappSentryUrl = sentryOpt.WebappDSN @@ -73,6 +74,7 @@ func MakeWebappServer(log *zap.Logger, httpSrvOpt options.HttpServerOpt, authOpt discoveryApiBaseUrl: discoveryApiBaseUrl, sentryUrl: webappSentryUrl, settings: service.CurrentSettings, + scssDirPath: webappOpt.SCSSDirPath, }) r.Get(webBaseUrl+"*", serveIndex(httpSrvOpt, appIndexHTMLs[app], fs)) } @@ -86,6 +88,7 @@ func MakeWebappServer(log *zap.Logger, httpSrvOpt options.HttpServerOpt, authOpt discoveryApiBaseUrl: discoveryApiBaseUrl, sentryUrl: webappSentryUrl, settings: service.CurrentSettings, + scssDirPath: webappOpt.SCSSDirPath, }) r.Get(webBaseUrl+"*", serveIndex(httpSrvOpt, appIndexHTMLs[""], fs)) } @@ -149,7 +152,7 @@ func serveConfig(r chi.Router, config webappConfig) { r.Get(options.CleanBase(config.appUrl, "custom.css"), func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "text/css") - stylesheet, err := sass.GenerateCSS(config.settings.UI.BrandingSASS, config.settings.UI.CustomCSS) + stylesheet, err := sass.GenerateCSS(config.settings.UI.BrandingSASS, config.settings.UI.CustomCSS, config.scssDirPath) if err != nil { logger.Default().Error("failed to generate CSS", zap.Error(err)) }