mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Frontend: Reload the browser when backend configuration/assets change (#79057)
* Detect frontend asset changes * Update * merge main * Frontend: Detect new assets / versions / config changes (#79258) * avoid first check * Updates and add tests * Update * Update * Updated code * refine * use context --------- Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
parent
7210e378b8
commit
e924627659
@ -135,7 +135,7 @@ export interface BootData {
|
||||
user: CurrentUserDTO;
|
||||
settings: GrafanaConfig;
|
||||
navTree: NavLinkDTO[];
|
||||
themePaths: {
|
||||
assets: {
|
||||
light: string;
|
||||
dark: string;
|
||||
};
|
||||
|
@ -416,6 +416,8 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
}
|
||||
|
||||
apiRoute.Get("/frontend/settings/", hs.GetFrontendSettings)
|
||||
apiRoute.Get("/frontend/assets", hs.GetFrontendAssets)
|
||||
|
||||
apiRoute.Any("/datasources/proxy/:id/*", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), authorize(ac.EvalPermission(datasources.ActionQuery)), hs.ProxyDataSourceRequest)
|
||||
apiRoute.Any("/datasources/proxy/uid/:uid/*", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), authorize(ac.EvalPermission(datasources.ActionQuery)), hs.ProxyDataSourceRequestWithUID)
|
||||
apiRoute.Any("/datasources/proxy/:id", requestmeta.SetSLOGroup(requestmeta.SLOGroupHighSlow), authorize(ac.EvalPermission(datasources.ActionQuery)), hs.ProxyDataSourceRequest)
|
||||
|
@ -26,7 +26,6 @@ type IndexViewData struct {
|
||||
FavIcon template.URL
|
||||
AppleTouchIcon template.URL
|
||||
AppTitle string
|
||||
ContentDeliveryURL string
|
||||
LoadingLogo template.URL
|
||||
CSPContent string
|
||||
CSPEnabled bool
|
||||
@ -34,16 +33,29 @@ type IndexViewData struct {
|
||||
// Nonce is a cryptographic identifier for use with Content Security Policy.
|
||||
Nonce string
|
||||
NewsFeedEnabled bool
|
||||
Assets *EntryPointAssets
|
||||
Assets *EntryPointAssets // Includes CDN info
|
||||
}
|
||||
|
||||
type EntryPointAssets struct {
|
||||
JSFiles []EntryPointAsset
|
||||
CSSDark string
|
||||
CSSLight string
|
||||
ContentDeliveryURL string `json:"cdn,omitempty"`
|
||||
JSFiles []EntryPointAsset `json:"jsFiles"`
|
||||
Dark string `json:"dark"`
|
||||
Light string `json:"light"`
|
||||
}
|
||||
|
||||
type EntryPointAsset struct {
|
||||
FilePath string
|
||||
Integrity string
|
||||
FilePath string `json:"filePath"`
|
||||
Integrity string `json:"integrity"`
|
||||
}
|
||||
|
||||
func (a *EntryPointAssets) SetContentDeliveryURL(prefix string) {
|
||||
if prefix == "" {
|
||||
return
|
||||
}
|
||||
a.ContentDeliveryURL = prefix
|
||||
a.Dark = prefix + a.Dark
|
||||
a.Light = prefix + a.Light
|
||||
for i, p := range a.JSFiles {
|
||||
a.JSFiles[i].FilePath = prefix + p.FilePath
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,16 @@ package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"hash"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/webassets"
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
@ -24,6 +28,61 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
// Returns a file that is easy to check for changes
|
||||
// Any changes to the file means we should refresh the frontend
|
||||
func (hs *HTTPServer) GetFrontendAssets(c *contextmodel.ReqContext) {
|
||||
hash := sha256.New()
|
||||
keys := map[string]any{}
|
||||
|
||||
// BuildVersion
|
||||
hash.Reset()
|
||||
_, _ = hash.Write([]byte(setting.BuildVersion))
|
||||
_, _ = hash.Write([]byte(setting.BuildCommit))
|
||||
_, _ = hash.Write([]byte(fmt.Sprintf("%d", setting.BuildStamp)))
|
||||
keys["version"] = fmt.Sprintf("%x", hash.Sum(nil))
|
||||
|
||||
// Plugin configs
|
||||
plugins := []string{}
|
||||
for _, p := range hs.pluginStore.Plugins(c.Req.Context()) {
|
||||
plugins = append(plugins, fmt.Sprintf("%s@%s", p.Name, p.Info.Version))
|
||||
}
|
||||
keys["plugins"] = sortedHash(plugins, hash)
|
||||
|
||||
// Feature flags
|
||||
enabled := []string{}
|
||||
for flag, set := range hs.Features.GetEnabled(c.Req.Context()) {
|
||||
if set {
|
||||
enabled = append(enabled, flag)
|
||||
}
|
||||
}
|
||||
keys["flags"] = sortedHash(enabled, hash)
|
||||
|
||||
// Assets
|
||||
hash.Reset()
|
||||
dto, err := webassets.GetWebAssets(c.Req.Context(), hs.Cfg, hs.License)
|
||||
if err == nil && dto != nil {
|
||||
_, _ = hash.Write([]byte(dto.ContentDeliveryURL))
|
||||
_, _ = hash.Write([]byte(dto.Dark))
|
||||
_, _ = hash.Write([]byte(dto.Light))
|
||||
for _, f := range dto.JSFiles {
|
||||
_, _ = hash.Write([]byte(f.FilePath))
|
||||
_, _ = hash.Write([]byte(f.Integrity))
|
||||
}
|
||||
}
|
||||
keys["assets"] = fmt.Sprintf("%x", hash.Sum(nil))
|
||||
|
||||
c.JSON(http.StatusOK, keys)
|
||||
}
|
||||
|
||||
func sortedHash(vals []string, hash hash.Hash) string {
|
||||
hash.Reset()
|
||||
sort.Strings(vals)
|
||||
for _, v := range vals {
|
||||
_, _ = hash.Write([]byte(v))
|
||||
}
|
||||
return fmt.Sprintf("%x", hash.Sum(nil))
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) GetFrontendSettings(c *contextmodel.ReqContext) {
|
||||
settings, err := hs.getFrontendSettings(c)
|
||||
if err != nil {
|
||||
|
@ -656,7 +656,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
|
||||
m.UseMiddleware(middleware.Gziper())
|
||||
}
|
||||
|
||||
m.UseMiddleware(middleware.Recovery(hs.Cfg))
|
||||
m.UseMiddleware(middleware.Recovery(hs.Cfg, hs.License))
|
||||
m.UseMiddleware(hs.Csrf.Middleware())
|
||||
|
||||
hs.mapStatic(m, hs.Cfg.StaticRootPath, "build", "public/build")
|
||||
|
@ -81,7 +81,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
|
||||
}
|
||||
|
||||
theme := hs.getThemeForIndexData(prefs.Theme, c.Query("theme"))
|
||||
assets, err := webassets.GetWebAssets(hs.Cfg)
|
||||
assets, err := webassets.GetWebAssets(c.Req.Context(), hs.Cfg, hs.License)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -98,10 +98,6 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
|
||||
|
||||
hasAccess := ac.HasAccess(hs.AccessControl, c)
|
||||
hasEditPerm := hasAccess(ac.EvalAny(ac.EvalPermission(dashboards.ActionDashboardsCreate), ac.EvalPermission(dashboards.ActionFoldersCreate)))
|
||||
cdnURL, err := hs.Cfg.GetContentDeliveryURL(hs.License.ContentDeliveryPrefix())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := dtos.IndexViewData{
|
||||
User: &dtos.CurrentUser{
|
||||
@ -147,7 +143,6 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
|
||||
AppTitle: "Grafana",
|
||||
NavTree: navTree,
|
||||
Nonce: c.RequestNonce,
|
||||
ContentDeliveryURL: cdnURL,
|
||||
LoadingLogo: "public/img/grafana_icon.svg",
|
||||
IsDevelopmentEnv: hs.Cfg.Env == setting.Dev,
|
||||
Assets: assets,
|
||||
|
@ -53,9 +53,9 @@ func fakeSetIndexViewData(t *testing.T) {
|
||||
Settings: &dtos.FrontendSettingsDTO{},
|
||||
NavTree: &navtree.NavTreeRoot{},
|
||||
Assets: &dtos.EntryPointAssets{
|
||||
JSFiles: []dtos.EntryPointAsset{},
|
||||
CSSDark: "dark.css",
|
||||
CSSLight: "light.css",
|
||||
JSFiles: []dtos.EntryPointAsset{},
|
||||
Dark: "dark.css",
|
||||
Light: "light.css",
|
||||
},
|
||||
}
|
||||
return data, nil
|
||||
|
@ -1,12 +1,16 @@
|
||||
package webassets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@ -29,27 +33,67 @@ type EntryPointInfo struct {
|
||||
|
||||
var entryPointAssetsCache *dtos.EntryPointAssets = nil
|
||||
|
||||
func GetWebAssets(cfg *setting.Cfg) (*dtos.EntryPointAssets, error) {
|
||||
func GetWebAssets(ctx context.Context, cfg *setting.Cfg, license licensing.Licensing) (*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
|
||||
var err error
|
||||
var result *dtos.EntryPointAssets
|
||||
|
||||
cdn := "" // "https://grafana-assets.grafana.net/grafana/10.3.0-64123/"
|
||||
if cdn != "" {
|
||||
result, err = readWebAssetsFromCDN(ctx, cdn)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
result, err = readWebAssetsFromFile(filepath.Join(cfg.StaticRootPath, "build", "assets-manifest.json"))
|
||||
if err == nil {
|
||||
cdn, _ = cfg.GetContentDeliveryURL(license.ContentDeliveryPrefix())
|
||||
if cdn != "" {
|
||||
result.SetContentDeliveryURL(cdn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entryPointAssetsCache = result
|
||||
return entryPointAssetsCache, err
|
||||
}
|
||||
|
||||
func readWebAssets(manifestpath string) (*dtos.EntryPointAssets, error) {
|
||||
func readWebAssetsFromFile(manifestpath string) (*dtos.EntryPointAssets, error) {
|
||||
//nolint:gosec
|
||||
bytes, err := os.ReadFile(manifestpath)
|
||||
f, err := os.Open(manifestpath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load assets-manifest.json %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
return readWebAssets(f)
|
||||
}
|
||||
|
||||
manifest := map[string]ManifestInfo{}
|
||||
err = json.Unmarshal(bytes, &manifest)
|
||||
func readWebAssetsFromCDN(ctx context.Context, baseURL string) (*dtos.EntryPointAssets, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"public/build/assets-manifest.json", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
_ = response.Body.Close()
|
||||
}()
|
||||
dto, err := readWebAssets(response.Body)
|
||||
if err == nil {
|
||||
dto.SetContentDeliveryURL(baseURL)
|
||||
}
|
||||
return dto, err
|
||||
}
|
||||
|
||||
func readWebAssets(r io.Reader) (*dtos.EntryPointAssets, error) {
|
||||
manifest := map[string]ManifestInfo{}
|
||||
if err := json.NewDecoder(r).Decode(&manifest); err != nil {
|
||||
return nil, fmt.Errorf("failed to read assets-manifest.json %w", err)
|
||||
}
|
||||
|
||||
@ -84,8 +128,8 @@ func readWebAssets(manifestpath string) (*dtos.EntryPointAssets, error) {
|
||||
}
|
||||
|
||||
return &dtos.EntryPointAssets{
|
||||
JSFiles: entryPointJSAssets,
|
||||
CSSDark: entryPoints.Dark.Assets.CSS[0],
|
||||
CSSLight: entryPoints.Light.Assets.CSS[0],
|
||||
JSFiles: entryPointJSAssets,
|
||||
Dark: entryPoints.Dark.Assets.CSS[0],
|
||||
Light: entryPoints.Light.Assets.CSS[0],
|
||||
}, nil
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package webassets
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
@ -8,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
func TestReadWebassets(t *testing.T) {
|
||||
assets, err := readWebAssets("testdata/sample-assets-manifest.json")
|
||||
assets, err := readWebAssetsFromFile("testdata/sample-assets-manifest.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
dto, err := json.MarshalIndent(assets, "", " ")
|
||||
@ -16,33 +17,114 @@ func TestReadWebassets(t *testing.T) {
|
||||
//fmt.Printf("%s\n", string(dto))
|
||||
|
||||
require.JSONEq(t, `{
|
||||
"JSFiles": [
|
||||
"jsFiles": [
|
||||
{
|
||||
"FilePath": "public/build/runtime.20ed8c01880b812ed29f.js",
|
||||
"Integrity": "sha256-rcdxIHk6cWgu4jiFa1a+pWlileYD/R72GaS8ZACBUdw= sha384-I/VJZQkt+TuJTvu61ihdWPds7EHfLrW5CxeQ0x9gtSqoPg9Z17Uawz1yoYaTdxqQ sha512-4CPAbh4KdTmGxHoQw4pgpYmgAquupVfwfo6UBV2cGU3vGFnEwkhq320037ETwWs+n9xB/bAMOvrdabp1SA1+8g=="
|
||||
"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/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/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/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/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=="
|
||||
"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"
|
||||
"dark": "public/build/grafana.dark.a28b24b45b2bbcc628cc.css",
|
||||
"light": "public/build/grafana.light.3572f6d5f8b7daa8d8d0.css"
|
||||
}`, string(dto))
|
||||
|
||||
assets.SetContentDeliveryURL("https://grafana-assets.grafana.net/grafana/10.3.0-64123/")
|
||||
|
||||
dto, err = json.MarshalIndent(assets, "", " ")
|
||||
require.NoError(t, err)
|
||||
//fmt.Printf("%s\n", string(dto))
|
||||
|
||||
require.JSONEq(t, `{
|
||||
"cdn": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/",
|
||||
"jsFiles": [
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/runtime.20ed8c01880b812ed29f.js",
|
||||
"integrity": "sha256-rcdxIHk6cWgu4jiFa1a+pWlileYD/R72GaS8ZACBUdw= sha384-I/VJZQkt+TuJTvu61ihdWPds7EHfLrW5CxeQ0x9gtSqoPg9Z17Uawz1yoYaTdxqQ sha512-4CPAbh4KdTmGxHoQw4pgpYmgAquupVfwfo6UBV2cGU3vGFnEwkhq320037ETwWs+n9xB/bAMOvrdabp1SA1+8g=="
|
||||
},
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/3951.4e474348841d792ab1ba.js",
|
||||
"integrity": "sha256-dHqXXTRA3osYhHr9rol8hOV0nC4VP0pr5tbMp5VD95Q= sha384-4QJaSTibnxdYeYsLnmXtd1+If6IkAmXlLR0uYHN5+N+fS0FegHRH7MIFaRGjiO1B sha512-vRLEeEGbxBCx0z+l/m14fSK49reqWGA9zQzsCrD+TQQBmP07YIoRPwopMMyxtKljbbRFV0bW2bUZ7ZvzOZYoIQ=="
|
||||
},
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/3651.4e8f7603e9778e1e9b59.js",
|
||||
"integrity": "sha256-+N7caL91pVANd7C/aquAneRTjBQenCwaEKqj+3qkjxc= sha384-GQR7GyHPEwwEVph9gGYWEWvMYxkITwcOjieehbPidXZrybuQyw9cpDkjnWo1tj/w sha512-zyPM+8AxyLuECEXjb9w6Z2Sy8zmJdkfTWQphcvAb8AU4ZdkCqLmyjmOs/QQlpfKDe0wdOLyR3V9QgTDDlxtVlQ=="
|
||||
},
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/1272.8c79fc44bf7cd993c953.js",
|
||||
"integrity": "sha256-d7MRVimV83v4YQ5rdURfTaaFtiedXP3EMLT06gvvBuQ= sha384-8tRpYHQ+sEkZ8ptiIbKAbKPpHTJVnmaWDN56vJoWWUCzV1Q2w034wcJNKDJDJdAs sha512-cIZWoJHusF8qODBOj2j4b18ewcLLMo/92YQSwYQjln2G5e3o1bSO476ox2I2iecJ/tnhQK5j01h9BzTt3dNTrA=="
|
||||
},
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/6902.070074e8f5a989b8f4c3.js",
|
||||
"integrity": "sha256-TMo/uTZueyEHtkBzlLZzhwYKWF0epE4qbouo5xcwZkU= sha384-xylZJMtJ7+EsUBBdQZvPh+BeHJ3BnfclqI2vx/8QC9jvfYe/lhRsWW9OMJsxE/Aq sha512-EOmf+KZQMFPoTWAROL8bBLFfHhgvDH8ONycq37JaV7lz+sQOTaWBN2ZD0F/mMdOD5zueTg/Y1RAUP6apoEcHNQ=="
|
||||
},
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/app.0439db6f56ee4aa501b2.js",
|
||||
"integrity": "sha256-q6muaKY7BuN2Ff+00aw69628MXatcFnLNzWRnAD98DI= sha384-gv6lAbkngOHR05bvyOR8dm/J3wIjQQWSjyxK7W8vt2rG9uxcjvvDQV7aI6YbUhfX sha512-o/0mSlJ/OoqrpGdOIWCE3ZCe8n+qqLbgNCERtx9G8FIzsv++CvIWSGbbILjOTGfnEfEQWcKMH0macVpVBSe1Og=="
|
||||
}
|
||||
],
|
||||
"dark": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/grafana.dark.a28b24b45b2bbcc628cc.css",
|
||||
"light": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/grafana.light.3572f6d5f8b7daa8d8d0.css"
|
||||
}`, string(dto))
|
||||
}
|
||||
|
||||
func TestReadWebassetsFromCDN(t *testing.T) {
|
||||
t.Skip()
|
||||
|
||||
assets, err := readWebAssetsFromCDN(context.Background(), "https://grafana-assets.grafana.net/grafana/10.3.0-64123/")
|
||||
require.NoError(t, err)
|
||||
|
||||
dto, err := json.MarshalIndent(assets, "", " ")
|
||||
require.NoError(t, err)
|
||||
//fmt.Printf("%s\n", string(dto))
|
||||
|
||||
require.JSONEq(t, `{
|
||||
"cdn": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/",
|
||||
"jsFiles": [
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/runtime.6d702760ddd47772f116.js",
|
||||
"integrity": "sha256-6tSxwMwqd9McukcH+i56v1v+8JsVlMXPWKUCIK30yK8= sha384-dfRWJ5QfPAiQKJ9fUugmeXVdRSx8OS3XUdkEyEhxkm9CZQf9KeUyUe6fGV7VL7s9 sha512-0kjFCSBeQtdS3F9B/uqX45KMMUffYpsU7Ve7AYjy75HiBzovxRGG4hWPZD7d4Gha0Y3Oj4AmZA37TJoafptlRQ=="
|
||||
},
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/7653.f5c70a70add3b711f560.js",
|
||||
"integrity": "sha256-p65DYfZPt9NU7vDwlxW+sY9sK+wQ9tJgTGlCJt+LvxY= sha384-P1TDQw3ZJ4X6Fiyn6UpLpVuHq+UW3zKRUM6U0vjucSl/bjFmQJfGR9XE64uEn6sJ sha512-sPqhDs/mWUBL6txtyoTdlgyZvVfdttUAXdV39aEroYpSnl/uEoLIcNBem5mNxoh4ut4TpSb9hlW6tTD7QV07/g=="
|
||||
},
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/182.0b85a6da60c3ae0a9093.js",
|
||||
"integrity": "sha256-4vJBytomvJYkSsXlAo7BXDiXRsi5JVWBosIZSMCYlqs= sha384-MWfyWG85/+OvsA4E9CvG1NGiSzrp/EH37Xd/+qfdMFKmvAEGzGx9N/4xF+3N3/yj sha512-j1h6qobFAJYU+7QFdcChEeHa/FPXuArEsHJuXSYtaqrDU7oNHyW1PqFz6kNUwqE674Hutl93EeY+UsUlpZgZZQ=="
|
||||
},
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/8781.91ede282a7f6078508e7.js",
|
||||
"integrity": "sha256-b68VAYMTugwWaHtffKI4qCMSWTN/fg0xQv+MnSILQgg= sha384-ptDkcAAAQhuG9Mhvs6gvGIp0HIjCfAP+ysaMltIr3L5alN6Ki71Si/zO6C70YArC sha512-N5tkcDgTPcNvQymegqnx0syp0kS7wVzPnt7i5KSu/RAi6cfM9XiRfz7bZh6fcZAJxApvpL1OJhUQQwPFFBN4ZA=="
|
||||
},
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/3958.1d29ae9e8eb421432f48.js",
|
||||
"integrity": "sha256-9c+QGDOI8HtAzVBLA3nJOOU+LzhoENAhIEw7gGSkgWY= sha384-Y05zEdrM/ab9jzGH6segO9GyE8OTV5RvWPZFgynXX4XgvMOyWJcySqwW4RoIVo6P sha512-+ro4iXipgz1zUySd8oMbOY6XX+RjP4gi8bksFNjJGiLQOHVb/EKZKDj5UBeIE96XMd1AoEvZdymCvaft3d8oeA=="
|
||||
},
|
||||
{
|
||||
"filePath": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/app.18e8d3e07edcc1356a6a.js",
|
||||
"integrity": "sha256-ueeH8P/rDaft7jtzRmTN4UpNtiPfhzYa7c1VbBiRLTo= sha384-SijeOWlmIMzm/WNVg5e+yMieef6LOFXMu8d2laBtaY/2m/fviGI+8W55jazWzb+C sha512-qr5MoBZ4wNTCm6aRQ5/mglO8gShmKFpvr066SJgKyAJA4j8cK0snL2XhubUNxND+KkpKAnRe7EjsHYd28/uvkw=="
|
||||
}
|
||||
],
|
||||
"dark": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/grafana.dark.b44253d019cd9cb46428.css",
|
||||
"light": "https://grafana-assets.grafana.net/grafana/10.3.0-64123/public/build/grafana.light.e8e11c59b604d62836be.css"
|
||||
}`, string(dto))
|
||||
}
|
||||
|
@ -150,9 +150,9 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
Settings: &dtos.FrontendSettingsDTO{},
|
||||
NavTree: &navtree.NavTreeRoot{},
|
||||
Assets: &dtos.EntryPointAssets{
|
||||
JSFiles: []dtos.EntryPointAsset{},
|
||||
CSSDark: "dark.css",
|
||||
CSSLight: "light.css",
|
||||
JSFiles: []dtos.EntryPointAsset{},
|
||||
Dark: "dark.css",
|
||||
Light: "light.css",
|
||||
},
|
||||
}
|
||||
t.Log("Calling HTML", "data", data)
|
||||
|
@ -27,6 +27,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/webassets"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
@ -104,7 +105,7 @@ func function(pc uintptr) []byte {
|
||||
|
||||
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
|
||||
// While Martini is in development mode, Recovery will also output the panic as HTML.
|
||||
func Recovery(cfg *setting.Cfg) web.Middleware {
|
||||
func Recovery(cfg *setting.Cfg, license licensing.Licensing) web.Middleware {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
c := web.FromContext(req.Context())
|
||||
@ -137,7 +138,7 @@ func Recovery(cfg *setting.Cfg) web.Middleware {
|
||||
return
|
||||
}
|
||||
|
||||
assets, _ := webassets.GetWebAssets(cfg)
|
||||
assets, _ := webassets.GetWebAssets(req.Context(), cfg, license)
|
||||
if assets == nil {
|
||||
assets = &dtos.EntryPointAssets{JSFiles: []dtos.EntryPointAsset{}}
|
||||
}
|
||||
@ -146,7 +147,7 @@ func Recovery(cfg *setting.Cfg) web.Middleware {
|
||||
Title string
|
||||
AppTitle string
|
||||
AppSubUrl string
|
||||
Theme string
|
||||
ThemeType string
|
||||
ErrorMsg string
|
||||
Assets *dtos.EntryPointAssets
|
||||
}{"Server Error", "Grafana", cfg.AppSubURL, cfg.DefaultTheme, "", assets}
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
"github.com/grafana/grafana/pkg/services/authn/authntest"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
@ -62,7 +63,7 @@ func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) {
|
||||
require.NoError(t, err)
|
||||
|
||||
sc.m = web.New()
|
||||
sc.m.UseMiddleware(Recovery(cfg))
|
||||
sc.m.UseMiddleware(Recovery(cfg, &licensing.OSSLicensingService{}))
|
||||
|
||||
sc.m.Use(AddDefaultResponseHeaders(cfg))
|
||||
sc.m.UseMiddleware(web.Renderer(viewsPath, "[[", "]]"))
|
||||
|
@ -43,7 +43,7 @@ func (ctx *ReqContext) Handle(cfg *setting.Cfg, status int, title string, err er
|
||||
Title string
|
||||
AppTitle string
|
||||
AppSubUrl string
|
||||
Theme string
|
||||
ThemeType string
|
||||
ErrorMsg error
|
||||
}{title, "Grafana", cfg.AppSubURL, "dark", nil}
|
||||
|
||||
|
@ -56,6 +56,7 @@ import { initIconCache } from './core/icons/iconBundle';
|
||||
import { initializeI18n } from './core/internationalization';
|
||||
import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks';
|
||||
import { ModalManager } from './core/services/ModalManager';
|
||||
import { NewFrontendAssetsChecker } from './core/services/NewFrontendAssetsChecker';
|
||||
import { backendSrv } from './core/services/backend_srv';
|
||||
import { contextSrv } from './core/services/context_srv';
|
||||
import { Echo } from './core/services/echo/Echo';
|
||||
@ -218,6 +219,8 @@ export class GrafanaApp {
|
||||
const queryParams = locationService.getSearchObject();
|
||||
const chromeService = new AppChromeService();
|
||||
const keybindingsService = new KeybindingSrv(locationService, chromeService);
|
||||
const newAssetsChecker = new NewFrontendAssetsChecker();
|
||||
newAssetsChecker.start();
|
||||
|
||||
// Read initial kiosk mode from url at app startup
|
||||
chromeService.setKioskModeFromUrl(queryParams.kiosk);
|
||||
@ -234,6 +237,7 @@ export class GrafanaApp {
|
||||
location: locationService,
|
||||
chrome: chromeService,
|
||||
keybindings: keybindingsService,
|
||||
newAssetsChecker,
|
||||
config,
|
||||
};
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { LocationService } from '@grafana/runtime/src/services/LocationService';
|
||||
import { BackendSrv } from '@grafana/runtime/src/services/backendSrv';
|
||||
|
||||
import { AppChromeService } from '../components/AppChrome/AppChromeService';
|
||||
import { NewFrontendAssetsChecker } from '../services/NewFrontendAssetsChecker';
|
||||
import { KeybindingSrv } from '../services/keybindingSrv';
|
||||
|
||||
export interface GrafanaContextType {
|
||||
@ -13,6 +14,7 @@ export interface GrafanaContextType {
|
||||
config: GrafanaConfig;
|
||||
chrome: AppChromeService;
|
||||
keybindings: KeybindingSrv;
|
||||
newAssetsChecker: NewFrontendAssetsChecker;
|
||||
}
|
||||
|
||||
export const GrafanaContext = React.createContext<GrafanaContextType | undefined>(undefined);
|
||||
|
49
public/app/core/services/NewFrontendAssetsChecker.test.ts
Normal file
49
public/app/core/services/NewFrontendAssetsChecker.test.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { locationService, setBackendSrv, BackendSrv } from '@grafana/runtime';
|
||||
|
||||
import { NewFrontendAssetsChecker } from './NewFrontendAssetsChecker';
|
||||
|
||||
describe('NewFrontendAssetsChecker', () => {
|
||||
const backendApiGet = jest.fn().mockReturnValue(Promise.resolve({}));
|
||||
const locationReload = jest.fn();
|
||||
|
||||
const originalLocation = window.location;
|
||||
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
value: { reload: locationReload },
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
Object.defineProperty(window, 'location', { configurable: true, value: originalLocation });
|
||||
});
|
||||
|
||||
setBackendSrv({
|
||||
get: backendApiGet,
|
||||
} as unknown as BackendSrv);
|
||||
|
||||
it('Should skip update checks if below interval', () => {
|
||||
const checker = new NewFrontendAssetsChecker();
|
||||
checker.start();
|
||||
|
||||
locationService.push('/d/123');
|
||||
|
||||
expect(backendApiGet).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should do update check when changing dashboard or going home', async () => {
|
||||
const checker = new NewFrontendAssetsChecker(0);
|
||||
checker.start();
|
||||
|
||||
locationService.push('/d/asd');
|
||||
locationService.push('/d/other');
|
||||
locationService.push('/d/other?viewPanel=2');
|
||||
locationService.push('/ignored');
|
||||
locationService.push('/ignored?asd');
|
||||
locationService.push('/ignored/sub');
|
||||
locationService.push('/home');
|
||||
|
||||
expect(backendApiGet).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
112
public/app/core/services/NewFrontendAssetsChecker.ts
Normal file
112
public/app/core/services/NewFrontendAssetsChecker.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { Location } from 'history';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
import { getBackendSrv, getGrafanaLiveSrv, locationService, reportInteraction } from '@grafana/runtime';
|
||||
|
||||
export class NewFrontendAssetsChecker {
|
||||
private hasUpdates = false;
|
||||
private previous?: FrontendAssetsAPIDTO;
|
||||
private interval: number;
|
||||
private checked = Date.now();
|
||||
private prevLocationPath = '';
|
||||
|
||||
public constructor(interval?: number) {
|
||||
// Default to never check for updates if last check was 5 minutes ago
|
||||
this.interval = interval ?? 1000 * 60 * 5;
|
||||
}
|
||||
|
||||
public start() {
|
||||
// Subscribe to live connection state changes and check for new assets when re-connected
|
||||
const live = getGrafanaLiveSrv();
|
||||
|
||||
if (live) {
|
||||
live.getConnectionState().subscribe((connected) => {
|
||||
if (connected) {
|
||||
this._checkForUpdates();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Subscribe to location changes
|
||||
locationService.getHistory().listen(this.locationUpdated.bind(this));
|
||||
this.prevLocationPath = locationService.getLocation().pathname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to detect some navigation events where it's safe to trigger a reload
|
||||
*/
|
||||
private locationUpdated(location: Location) {
|
||||
if (this.prevLocationPath === location.pathname) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newLocationSegments = location.pathname.split('/');
|
||||
|
||||
// We are going to home
|
||||
if (newLocationSegments[1] === '/' && this.prevLocationPath !== '/') {
|
||||
this.reloadIfUpdateDetected();
|
||||
}
|
||||
// Moving to dashboard (or changing dashboards)
|
||||
else if (newLocationSegments[1] === 'd') {
|
||||
this.reloadIfUpdateDetected();
|
||||
}
|
||||
// Track potential page change
|
||||
else if (this.hasUpdates) {
|
||||
reportInteraction('new_frontend_assets_reload_ignored', {
|
||||
newLocation: location.pathname,
|
||||
prevLocation: this.prevLocationPath,
|
||||
});
|
||||
}
|
||||
|
||||
this.prevLocationPath = location.pathname;
|
||||
}
|
||||
|
||||
private async _checkForUpdates() {
|
||||
if (this.hasUpdates) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't check too often
|
||||
if (Date.now() - this.checked < this.interval) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.checked = Date.now();
|
||||
|
||||
const previous = this.previous;
|
||||
const result: FrontendAssetsAPIDTO = await getBackendSrv().get('/api/frontend/assets');
|
||||
|
||||
if (previous && !isEqual(previous, result)) {
|
||||
this.hasUpdates = true;
|
||||
|
||||
// Report that we detected new assets
|
||||
reportInteraction('new_frontend_assets_detected', {
|
||||
assets: previous.assets !== result.assets,
|
||||
plugins: previous.plugins !== result.plugins,
|
||||
version: previous.version !== result.version,
|
||||
flags: previous.flags !== result.flags,
|
||||
});
|
||||
}
|
||||
|
||||
this.previous = result;
|
||||
}
|
||||
|
||||
/** This is called on page navigation events */
|
||||
public reloadIfUpdateDetected() {
|
||||
if (this.hasUpdates) {
|
||||
// Report that we detected new assets
|
||||
reportInteraction('new_frontend_assets_reload', {});
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
// Async check if the assets have changed
|
||||
this._checkForUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
interface FrontendAssetsAPIDTO {
|
||||
assets: string;
|
||||
flags: string;
|
||||
plugins: string;
|
||||
version: string;
|
||||
}
|
@ -18,7 +18,7 @@ export async function changeTheme(themeId: string, runtimeOnly?: boolean) {
|
||||
if (oldTheme.colors.mode !== newTheme.colors.mode) {
|
||||
const newCssLink = document.createElement('link');
|
||||
newCssLink.rel = 'stylesheet';
|
||||
newCssLink.href = config.bootData.themePaths[newTheme.colors.mode];
|
||||
newCssLink.href = config.bootData.assets[newTheme.colors.mode];
|
||||
newCssLink.onload = () => {
|
||||
// Remove old css file
|
||||
const bodyLinks = document.getElementsByTagName('link');
|
||||
|
@ -2,6 +2,7 @@ import { GrafanaConfig } from '@grafana/data';
|
||||
import { LocationService } from '@grafana/runtime';
|
||||
import { AppChromeService } from 'app/core/components/AppChrome/AppChromeService';
|
||||
import { GrafanaContextType } from 'app/core/context/GrafanaContext';
|
||||
import { NewFrontendAssetsChecker } from 'app/core/services/NewFrontendAssetsChecker';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { KeybindingSrv } from 'app/core/services/keybindingSrv';
|
||||
|
||||
@ -20,6 +21,10 @@ export function getGrafanaContextMock(overrides: Partial<GrafanaContextType> = {
|
||||
setupDashboardBindings: jest.fn(),
|
||||
setupTimeRangeBindings: jest.fn(),
|
||||
} as unknown as KeybindingSrv,
|
||||
newAssetsChecker: {
|
||||
start: jest.fn(),
|
||||
reloadIfUpdateDetected: jest.fn(),
|
||||
} as unknown as NewFrontendAssetsChecker,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
@ -10,17 +10,17 @@
|
||||
|
||||
<base href="[[.AppSubUrl]]/" />
|
||||
|
||||
[[ if eq .Theme "light" ]]
|
||||
<link rel="stylesheet" href="[[.ContentDeliveryURL]]public/build/[[.Assets.CSSLight]]" />
|
||||
[[ else if eq .Theme "dark" ]]
|
||||
<link rel="stylesheet" href="[[.ContentDeliveryURL]]public/build/[[.Assets.CSSDark]]" />
|
||||
[[ if eq .ThemeType "light" ]]
|
||||
<link rel="stylesheet" href="[[.Assets.Light]]" />
|
||||
[[ else ]]
|
||||
<link rel="stylesheet" href="[[.Assets.Dark]]" />
|
||||
[[ end ]]
|
||||
|
||||
<link rel="icon" type="image/png" href="public/img/fav32.png" />
|
||||
<link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28" />
|
||||
</head>
|
||||
|
||||
<body class="theme-[[ .Theme ]]">
|
||||
<body class="theme-[[ .ThemeType ]]">
|
||||
<div class="main-view">
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
|
@ -16,13 +16,13 @@
|
||||
|
||||
<link rel="icon" type="image/png" href="[[.FavIcon]]" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="[[.AppleTouchIcon]]" />
|
||||
<link rel="mask-icon" href="[[.ContentDeliveryURL]]public/img/grafana_mask_icon.svg" color="#F05A28" />
|
||||
<link rel="mask-icon" href="[[.Assets.ContentDeliveryURL]]public/img/grafana_mask_icon.svg" color="#F05A28" />
|
||||
|
||||
<!-- If theme is "system", we inject the stylesheets with javascript further down the page -->
|
||||
[[ if eq .ThemeType "light" ]]
|
||||
<link rel="stylesheet" href="[[.ContentDeliveryURL]][[.Assets.CSSLight]]" />
|
||||
<link rel="stylesheet" href="[[.Assets.Light]]" />
|
||||
[[ else if eq .ThemeType "dark" ]]
|
||||
<link rel="stylesheet" href="[[.ContentDeliveryURL]][[.Assets.CSSDark]]" />
|
||||
<link rel="stylesheet" href="[[.Assets.Dark]]" />
|
||||
[[ end ]]
|
||||
|
||||
<script nonce="[[.Nonce]]">
|
||||
@ -243,10 +243,7 @@
|
||||
user: [[.User]],
|
||||
settings: [[.Settings]],
|
||||
navTree: [[.NavTree]],
|
||||
themePaths: {
|
||||
light: '[[.ContentDeliveryURL]][[.Assets.CSSLight]]',
|
||||
dark: '[[.ContentDeliveryURL]][[.Assets.CSSDark]]'
|
||||
}
|
||||
assets: [[.Assets]]
|
||||
};
|
||||
|
||||
// Set theme to match system only on startup.
|
||||
@ -284,8 +281,8 @@
|
||||
window.__grafana_load_failed();
|
||||
};
|
||||
|
||||
[[if .ContentDeliveryURL]]
|
||||
window.public_cdn_path = '[[.ContentDeliveryURL]]public/build/';
|
||||
[[if .Assets.ContentDeliveryURL]]
|
||||
window.public_cdn_path = '[[.Assets.ContentDeliveryURL]]public/build/';
|
||||
[[end]]
|
||||
[[if .Nonce]]
|
||||
window.nonce = '[[.Nonce]]';
|
||||
@ -332,7 +329,7 @@
|
||||
[[range $asset := .Assets.JSFiles]]
|
||||
<script
|
||||
nonce="[[$.Nonce]]"
|
||||
src="[[$.ContentDeliveryURL]][[$asset.FilePath]]"
|
||||
src="[[$asset.FilePath]]"
|
||||
type="text/javascript"
|
||||
></script>
|
||||
[[end]]
|
||||
|
Loading…
Reference in New Issue
Block a user