Frontend: Foundations for multi tenant frontend (#78815)

* Frontend: Foundations for multi tenant frontend

* improve manifest parsing for multi-tenant frontend (#78876)

* add test

* add test

* ??

* Updates

* Added cache

* test cleanup

* lint

* fix test

* fix error templates

* cleanup

* remove copy

* revert changes to list testdata

* comment cleanup

* prepare integration tests

* Remove integrety

---------

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Torkel Ödegaard
2023-12-05 08:34:22 +01:00
committed by GitHub
co-authored by Ryan McKinley
parent 7b78061235
commit ed128ea964
21 changed files with 1861 additions and 124 deletions
+12
View File
@@ -34,4 +34,16 @@ type IndexViewData struct {
// Nonce is a cryptographic identifier for use with Content Security Policy.
Nonce string
NewsFeedEnabled bool
Assets *EntryPointAssets
}
type EntryPointAssets struct {
JSFiles []EntryPointAsset
CSSDark string
CSSLight string
}
type EntryPointAsset struct {
FilePath string
Integrity string
}
+6
View File
@@ -11,6 +11,7 @@ import (
"strings"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/webassets"
"github.com/grafana/grafana/pkg/middleware"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/auth/identity"
@@ -81,6 +82,10 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
}
theme := hs.getThemeForIndexData(prefs.Theme, c.Query("theme"))
assets, err := webassets.GetWebAssets(hs.Cfg)
if err != nil {
return nil, err
}
userOrgCount := 1
userOrgs, err := hs.orgService.GetUserOrgList(c.Req.Context(), &org.GetUserOrgListQuery{UserID: userID})
@@ -142,6 +147,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
ContentDeliveryURL: hs.Cfg.GetContentDeliveryURL(hs.License.ContentDeliveryPrefix()),
LoadingLogo: "public/img/grafana_icon.svg",
IsDevelopmentEnv: hs.Cfg.Env == setting.Dev,
Assets: assets,
}
if hs.Cfg.CSPEnabled {
+6 -1
View File
@@ -52,6 +52,11 @@ func fakeSetIndexViewData(t *testing.T) {
User: &dtos.CurrentUser{},
Settings: &dtos.FrontendSettingsDTO{},
NavTree: &navtree.NavTreeRoot{},
Assets: &dtos.EntryPointAssets{
JSFiles: []dtos.EntryPointAsset{},
CSSDark: "dark.css",
CSSLight: "light.css",
},
}
return data, nil
}
@@ -63,7 +68,7 @@ func fakeViewIndex(t *testing.T) {
getViewIndex = origGetViewIndex
})
getViewIndex = func() string {
return "index-template"
return "index"
}
}
File diff suppressed because it is too large Load Diff
+91
View File
@@ -0,0 +1,91 @@
package webassets
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/setting"
)
type ManifestInfo struct {
FilePath string `json:"src,omitempty"`
Integrity string `json:"integrity,omitempty"`
// The known entrypoints
App *EntryPointInfo `json:"app,omitempty"`
Dark *EntryPointInfo `json:"dark,omitempty"`
Light *EntryPointInfo `json:"light,omitempty"`
}
type EntryPointInfo struct {
Assets struct {
JS []string `json:"js,omitempty"`
CSS []string `json:"css,omitempty"`
} `json:"assets,omitempty"`
}
var entryPointAssetsCache *dtos.EntryPointAssets = nil
func GetWebAssets(cfg *setting.Cfg) (*dtos.EntryPointAssets, error) {
if cfg.Env != setting.Dev && entryPointAssetsCache != nil {
return entryPointAssetsCache, nil
}
result, err := readWebAssets(filepath.Join(cfg.StaticRootPath, "build", "assets-manifest.json"))
entryPointAssetsCache = result
return entryPointAssetsCache, err
}
func readWebAssets(manifestpath string) (*dtos.EntryPointAssets, error) {
//nolint:gosec
bytes, err := os.ReadFile(manifestpath)
if err != nil {
return nil, fmt.Errorf("failed to load assets-manifest.json %w", err)
}
manifest := map[string]ManifestInfo{}
err = json.Unmarshal(bytes, &manifest)
if err != nil {
return nil, fmt.Errorf("failed to read assets-manifest.json %w", err)
}
integrity := make(map[string]string, 100)
for _, v := range manifest {
if v.Integrity != "" && v.FilePath != "" {
integrity[v.FilePath] = v.Integrity
}
}
entryPoints, ok := manifest["entrypoints"]
if !ok {
return nil, fmt.Errorf("could not find entrypoints in asssets-manifest")
}
if entryPoints.App == nil || len(entryPoints.App.Assets.JS) == 0 {
return nil, fmt.Errorf("missing app entry")
}
if entryPoints.Dark == nil || len(entryPoints.Dark.Assets.CSS) == 0 {
return nil, fmt.Errorf("missing dark entry")
}
if entryPoints.Light == nil || len(entryPoints.Light.Assets.CSS) == 0 {
return nil, fmt.Errorf("missing light entry")
}
entryPointJSAssets := make([]dtos.EntryPointAsset, 0)
for _, entry := range entryPoints.App.Assets.JS {
entryPointJSAssets = append(entryPointJSAssets, dtos.EntryPointAsset{
FilePath: entry,
Integrity: integrity[entry],
})
}
return &dtos.EntryPointAssets{
JSFiles: entryPointJSAssets,
CSSDark: entryPoints.Dark.Assets.CSS[0],
CSSLight: entryPoints.Light.Assets.CSS[0],
}, nil
}
+48
View File
@@ -0,0 +1,48 @@
package webassets
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
func TestReadWebassets(t *testing.T) {
assets, err := readWebAssets("testdata/sample-assets-manifest.json")
require.NoError(t, err)
dto, err := json.MarshalIndent(assets, "", " ")
require.NoError(t, err)
//fmt.Printf("%s\n", string(dto))
require.JSONEq(t, `{
"JSFiles": [
{
"FilePath": "public/build/runtime.20ed8c01880b812ed29f.js",
"Integrity": "sha256-rcdxIHk6cWgu4jiFa1a+pWlileYD/R72GaS8ZACBUdw= sha384-I/VJZQkt+TuJTvu61ihdWPds7EHfLrW5CxeQ0x9gtSqoPg9Z17Uawz1yoYaTdxqQ sha512-4CPAbh4KdTmGxHoQw4pgpYmgAquupVfwfo6UBV2cGU3vGFnEwkhq320037ETwWs+n9xB/bAMOvrdabp1SA1+8g=="
},
{
"FilePath": "public/build/3951.4e474348841d792ab1ba.js",
"Integrity": "sha256-dHqXXTRA3osYhHr9rol8hOV0nC4VP0pr5tbMp5VD95Q= sha384-4QJaSTibnxdYeYsLnmXtd1+If6IkAmXlLR0uYHN5+N+fS0FegHRH7MIFaRGjiO1B sha512-vRLEeEGbxBCx0z+l/m14fSK49reqWGA9zQzsCrD+TQQBmP07YIoRPwopMMyxtKljbbRFV0bW2bUZ7ZvzOZYoIQ=="
},
{
"FilePath": "public/build/3651.4e8f7603e9778e1e9b59.js",
"Integrity": "sha256-+N7caL91pVANd7C/aquAneRTjBQenCwaEKqj+3qkjxc= sha384-GQR7GyHPEwwEVph9gGYWEWvMYxkITwcOjieehbPidXZrybuQyw9cpDkjnWo1tj/w sha512-zyPM+8AxyLuECEXjb9w6Z2Sy8zmJdkfTWQphcvAb8AU4ZdkCqLmyjmOs/QQlpfKDe0wdOLyR3V9QgTDDlxtVlQ=="
},
{
"FilePath": "public/build/1272.8c79fc44bf7cd993c953.js",
"Integrity": "sha256-d7MRVimV83v4YQ5rdURfTaaFtiedXP3EMLT06gvvBuQ= sha384-8tRpYHQ+sEkZ8ptiIbKAbKPpHTJVnmaWDN56vJoWWUCzV1Q2w034wcJNKDJDJdAs sha512-cIZWoJHusF8qODBOj2j4b18ewcLLMo/92YQSwYQjln2G5e3o1bSO476ox2I2iecJ/tnhQK5j01h9BzTt3dNTrA=="
},
{
"FilePath": "public/build/6902.070074e8f5a989b8f4c3.js",
"Integrity": "sha256-TMo/uTZueyEHtkBzlLZzhwYKWF0epE4qbouo5xcwZkU= sha384-xylZJMtJ7+EsUBBdQZvPh+BeHJ3BnfclqI2vx/8QC9jvfYe/lhRsWW9OMJsxE/Aq sha512-EOmf+KZQMFPoTWAROL8bBLFfHhgvDH8ONycq37JaV7lz+sQOTaWBN2ZD0F/mMdOD5zueTg/Y1RAUP6apoEcHNQ=="
},
{
"FilePath": "public/build/app.0439db6f56ee4aa501b2.js",
"Integrity": "sha256-q6muaKY7BuN2Ff+00aw69628MXatcFnLNzWRnAD98DI= sha384-gv6lAbkngOHR05bvyOR8dm/J3wIjQQWSjyxK7W8vt2rG9uxcjvvDQV7aI6YbUhfX sha512-o/0mSlJ/OoqrpGdOIWCE3ZCe8n+qqLbgNCERtx9G8FIzsv++CvIWSGbbILjOTGfnEfEQWcKMH0macVpVBSe1Og=="
}
],
"CSSDark": "public/build/grafana.dark.a28b24b45b2bbcc628cc.css",
"CSSLight": "public/build/grafana.light.3572f6d5f8b7daa8d8d0.css"
}`, string(dto))
}