diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f62fe0e1748..607a91d45cd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -605,6 +605,7 @@ playwright.config.ts @grafana/plugins-platform-frontend /public/app/dev.ts @grafana/frontend-ops /public/app/core/utils/metrics.ts @grafana/plugins-platform-frontend /public/app/index.ts @grafana/frontend-ops +/public/app/initApp.ts @grafana/frontend-ops /public/app/AppWrapper.tsx @grafana/frontend-ops /public/app/partials/ @grafana/grafana-frontend-platform diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 7031216db7c..33f325f9f42 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -306,11 +306,19 @@ function overrideFeatureTogglesFromUrl(config: GrafanaBootConfig) { }); } -const bootData = (window as any).grafanaBootData || { - settings: {}, - user: {}, - navTree: [], -}; +let bootData = (window as any).grafanaBootData; + +if (!bootData) { + if (process.env.NODE_ENV !== 'test') { + console.error('window.grafanaBootData was not set by the time config was initialized'); + } + + bootData = { + settings: {}, + user: {}, + navTree: [], + }; +} const options = bootData.settings; options.bootData = bootData; diff --git a/pkg/api/api.go b/pkg/api/api.go index 5503e367dfe..d131d32aedb 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -91,7 +91,7 @@ func (hs *HTTPServer) registerRoutes() { if err != nil { panic(err) // ??? } - r.Get("/mtfe", index.HandleRequest) + r.Get("/femt", index.HandleRequest) // Temporarily expose the full bootdata via API r.Get("/bootdata", reqNoAuth, hs.GetBootdata) diff --git a/pkg/api/dtos/index.go b/pkg/api/dtos/index.go index 2a55017e613..7fb4722f18b 100644 --- a/pkg/api/dtos/index.go +++ b/pkg/api/dtos/index.go @@ -15,7 +15,7 @@ type IndexViewData struct { GoogleAnalytics4Id string `json:"-"` GoogleAnalytics4SendManualPageViews bool `json:"-"` GoogleTagManagerId string `json:"-"` - NavTree *navtree.NavTreeRoot `json:"navtree"` + NavTree *navtree.NavTreeRoot `json:"navTree"` BuildVersion string `json:"-"` BuildCommit string `json:"-"` ThemeType string `json:"-"` diff --git a/pkg/services/frontend/index.go b/pkg/services/frontend/index.go index 4f02bbe1127..f7fdf617b35 100644 --- a/pkg/services/frontend/index.go +++ b/pkg/services/frontend/index.go @@ -24,9 +24,13 @@ type IndexProvider struct { } type IndexViewData struct { - CSPContent string - CSPEnabled bool - IsDevelopmentEnv bool + CSPContent string + CSPReportOnlyContent string + CSPEnabled bool + IsDevelopmentEnv bool + + Config *setting.Cfg + License licensing.Licensing AppSubUrl string BuildVersion string @@ -49,10 +53,6 @@ var ( ) func NewIndexProvider(cfg *setting.Cfg, license licensing.Licensing) (*IndexProvider, error) { - assets, err := webassets.GetWebAssets(context.Background(), cfg, license) - if err != nil { - return nil, err - } t := htmlTemplates.Lookup("index.html") if t == nil { return nil, fmt.Errorf("missing index template") @@ -66,10 +66,12 @@ func NewIndexProvider(cfg *setting.Cfg, license licensing.Licensing) (*IndexProv AppSubUrl: cfg.AppSubURL, // Based on the request? BuildVersion: cfg.BuildVersion, BuildCommit: cfg.BuildCommit, - Assets: assets, + Config: cfg, + License: license, - CSPEnabled: cfg.CSPEnabled, - CSPContent: cfg.CSPTemplate, + CSPEnabled: cfg.CSPEnabled, + CSPContent: cfg.CSPTemplate, + CSPReportOnlyContent: cfg.CSPReportOnlyTemplate, IsDevelopmentEnv: cfg.Env == setting.Dev, }, @@ -95,8 +97,23 @@ func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http. if data.CSPEnabled { data.CSPContent = middleware.ReplacePolicyVariables(p.data.CSPContent, p.data.AppSubUrl, data.Nonce) + writer.Header().Set("Content-Security-Policy", data.CSPContent) + + policy := middleware.ReplacePolicyVariables(p.data.CSPReportOnlyContent, p.data.AppSubUrl, data.Nonce) + writer.Header().Set("Content-Security-Policy-Report-Only", policy) } + // TODO: moved to request handler to prevent stale assets during dev, + // but should we do this differently? + assets, err := webassets.GetWebAssets(context.Background(), data.Config, data.License) + if err != nil { + p.log.Error("error getting assets", "err", err) + writer.WriteHeader(500) + return + } + + data.Assets = assets + writer.Header().Set("Content-Type", "text/html; charset=UTF-8") writer.WriteHeader(200) if err := p.index.Execute(writer, &data); err != nil { diff --git a/pkg/services/frontend/index.html b/pkg/services/frontend/index.html index 6778c160ca7..c51574aa598 100644 --- a/pkg/services/frontend/index.html +++ b/pkg/services/frontend/index.html @@ -24,10 +24,83 @@ performance.mark('frontend_boot_css_time_seconds'); + -

Grafana Frontend Server ([[.BuildVersion]])

-

This is a simple static HTML page served by the Grafana frontend server module.

+
+
+ + + + [[range $asset := .Assets.JSFiles]] + + [[end]] diff --git a/public/app/index.ts b/public/app/index.ts index 7ca8aba2b14..e117cc3f598 100644 --- a/public/app/index.ts +++ b/public/app/index.ts @@ -1,6 +1,6 @@ -import './core/trustedTypePolicies'; -declare let __webpack_public_path__: string; -declare let __webpack_nonce__: string; +// The new index.html fetches window.grafanaBootData asynchronously. +// Since much of Grafana depends on it in includes side effects at import time, +// we delay loading the rest of the app using import() until the boot data is ready. // Check if we are hosting files on cdn and set webpack public path if (window.public_cdn_path) { @@ -18,6 +18,17 @@ if (window.nonce) { // This is an indication to the window.onLoad failure check that the app bundle has loaded. window.__grafana_app_bundle_loaded = true; -import app from './app'; +async function bootstrapWindowData() { + // Wait for window.grafanaBootData is ready. The new index.html loads it from + // an API call, but the old one just sets an immediately resolving promise. + await window.__grafana_boot_data_promise; -app.init(); + // Use eager to ensure the app is included in the initial chunk and does not + // require additional network requests to load. + await import(/* webpackMode: "eager" */ './initApp'); +} + +bootstrapWindowData().catch((error) => { + console.error('Error bootstrapping Grafana', error); + window.__grafana_load_failed(); +}); diff --git a/public/app/initApp.ts b/public/app/initApp.ts new file mode 100644 index 00000000000..2f5c7fa36c5 --- /dev/null +++ b/public/app/initApp.ts @@ -0,0 +1,7 @@ +// See ./index.ts for why this is in a seperate file + +// Trusted types must be initialised before the rest of the world is imported +import './core/trustedTypePolicies'; +import app from './app'; + +app.init(); diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index dc247d595f2..c79d39a2bb5 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -527,6 +527,11 @@ export function getAppRoutes(): RouteDescriptor[] { () => import(/* webpackChunkName: "BookmarksPage"*/ 'app/features/bookmarks/BookmarksPage') ), }, + { + // Redirect the /femt dev page to the root + path: '/femt', + component: () => , + }, ...getPluginCatalogRoutes(), ...getSupportBundleRoutes(), ...getAlertingRoutes(), diff --git a/public/app/types/window.d.ts b/public/app/types/window.d.ts index bf4e7c5f049..c1614d622cd 100644 --- a/public/app/types/window.d.ts +++ b/public/app/types/window.d.ts @@ -4,6 +4,13 @@ export declare global { __grafana_app_bundle_loaded: boolean; __grafana_public_path__: string; __grafana_load_failed: () => void; + + /** + * (Potential) wait for API call to fetch boot data and place it on `window.grafanaBootData`. + * Required in new index.html to fetch necessary data before app init() + **/ + __grafana_boot_data_promise: Promise; + public_cdn_path: string; nonce: string | undefined; System: typeof System; diff --git a/public/views/index.html b/public/views/index.html index d7c54f4967b..2ee208bbb78 100644 --- a/public/views/index.html +++ b/public/views/index.html @@ -275,6 +275,9 @@ assets: [[.Assets]] }; + // FEMT index.html uses this, and we want to keep the index.ts the same for both + window.__grafana_boot_data_promise = Promise.resolve(); + // Set theme to match system only on startup. // Do not react to changes in system theme after startup. if (window.grafanaBootData.user.theme === "system") {