FEMT: Call /bootdata and render grafana (#105176)
* rename /mtfe route to /femt to match project name * set correct navTree JSON property name * call GetWebAssets in the request handler to prevent stale assets during development * Call /bootdata and render grafana * set nonce on script * write csp header in index handler * write report-only csp as well * debug stuff * more debug logging * move importing app into a seperate, async-loaded module * Clean up comments * make /femt redirect to / in the frontend * remove console.log * remove stale commented code * call __grafana_load_failed if bootstrap fails * comment for __grafana_boot_data_promise * remove console.log * remove blank newline * codeowners
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
@@ -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:"-"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -24,10 +24,83 @@
|
||||
performance.mark('frontend_boot_css_time_seconds');
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/*
|
||||
Dev indicator that the page was loaded from the FEMT index.html.
|
||||
TODO: Will remove before deploying to staging.
|
||||
*/
|
||||
.femt-dev-frame {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 1px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 99999;
|
||||
pointer-events: none;
|
||||
border: 2px solid white;
|
||||
border-image-source: linear-gradient(to left, #F55F3E, #FF8833);
|
||||
border-image-slice: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Grafana Frontend Server ([[.BuildVersion]])</h1>
|
||||
<p>This is a simple static HTML page served by the Grafana frontend server module.</p>
|
||||
<div class="femt-dev-frame"></div>
|
||||
<div id="reactRoot"></div>
|
||||
|
||||
<script nonce="[[.Nonce]]">
|
||||
[[if .Nonce]]
|
||||
window.nonce = '[[.Nonce]]';
|
||||
[[end]]
|
||||
|
||||
window.__grafana_load_failed = function(...args) {
|
||||
console.error('Failed to load Grafana', ...args);
|
||||
};
|
||||
|
||||
window.__grafana_boot_data_promise = new Promise(async (resolve) => {
|
||||
const bootData = await fetch("/bootdata");
|
||||
|
||||
const rawBootData = await bootData.json();
|
||||
|
||||
window.grafanaBootData = {
|
||||
_femt: true,
|
||||
...rawBootData,
|
||||
}
|
||||
|
||||
// The per-theme CSS still contains some global styles needed
|
||||
// to render the page correctly.
|
||||
const cssLink = document.createElement("link");
|
||||
cssLink.rel = 'stylesheet';
|
||||
|
||||
let theme = window.grafanaBootData.user.theme;
|
||||
if (theme === "system") {
|
||||
const darkQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
theme = darkQuery.matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
if (theme === "light") {
|
||||
document.body.classList.add("theme-light");
|
||||
cssLink.href = window.grafanaBootData.assets.light;
|
||||
window.grafanaBootData.user.lightTheme = true;
|
||||
} else if (theme === "dark") {
|
||||
document.body.classList.add("theme-dark");
|
||||
cssLink.href = window.grafanaBootData.assets.dark;
|
||||
window.grafanaBootData.user.lightTheme = false;
|
||||
}
|
||||
|
||||
document.head.appendChild(cssLink);
|
||||
|
||||
resolve();
|
||||
});
|
||||
</script>
|
||||
|
||||
[[range $asset := .Assets.JSFiles]]
|
||||
<script
|
||||
nonce="[[$.Nonce]]"
|
||||
src="[[$asset.FilePath]]"
|
||||
type="text/javascript"
|
||||
defer
|
||||
></script>
|
||||
[[end]]
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+16
-5
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
@@ -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: () => <Navigate replace to="/" />,
|
||||
},
|
||||
...getPluginCatalogRoutes(),
|
||||
...getSupportBundleRoutes(),
|
||||
...getAlertingRoutes(),
|
||||
|
||||
Vendored
+7
@@ -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<void>;
|
||||
|
||||
public_cdn_path: string;
|
||||
nonce: string | undefined;
|
||||
System: typeof System;
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user