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
pull/105311/head^2
Josh Hunt 7 days ago committed by GitHub
parent 621414ea6f
commit 91d9cac157
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      .github/CODEOWNERS
  2. 18
      packages/grafana-runtime/src/config.ts
  3. 2
      pkg/api/api.go
  4. 2
      pkg/api/dtos/index.go
  5. 37
      pkg/services/frontend/index.go
  6. 77
      pkg/services/frontend/index.html
  7. 21
      public/app/index.ts
  8. 7
      public/app/initApp.ts
  9. 5
      public/app/routes/routes.tsx
  10. 7
      public/app/types/window.d.ts
  11. 3
      public/views/index.html

@ -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;

@ -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>

@ -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(),

@ -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") {

Loading…
Cancel
Save