From 2b7da9b940ad7279894778f95ae684a30003450e Mon Sep 17 00:00:00 2001 From: Aquib Baig Date: Mon, 1 Jun 2026 16:02:44 +0530 Subject: [PATCH 1/3] pass custom container to mount mfe app --- public/app/fn-app/create-mfe.ts | 49 +++++++++++++++++++++++++++++---- public/app/fn-app/types.ts | 5 +++- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/public/app/fn-app/create-mfe.ts b/public/app/fn-app/create-mfe.ts index 4e44d4191db97..749bf672c05d1 100644 --- a/public/app/fn-app/create-mfe.ts +++ b/public/app/fn-app/create-mfe.ts @@ -28,7 +28,7 @@ import { updateMfeMode, } from 'app/store/configureMfeStore'; -import { FNDashboardProps, FailedToMountGrafanaErrorName } from './types'; +import { FNDashboardProps, FailedToMountGrafanaErrorName, MfeContainer } from './types'; /** * NOTE: @@ -59,6 +59,7 @@ type DeepPartial = { class createMfe { private static readonly containerSelector = '#grafanaRoot'; + private static themeStyleRoot: MfeContainer | null = null; private static logger = FnLoggerService; mode: FNDashboardProps['mode']; @@ -101,6 +102,28 @@ class createMfe { return stylesheetLink; } + private static setThemeStyleRoot(container: FNDashboardProps['container']) { + if (container) { + createMfe.themeStyleRoot = container; + } + } + + private static getThemeStyleRoot() { + if (!createMfe.themeStyleRoot) { + throw new Error('[FN Grafana]: Failed to load theme. MFE container does not exist.'); + } + + return createMfe.themeStyleRoot; + } + + private static getThemeLinkTarget(styleRoot: MfeContainer) { + return styleRoot instanceof Document ? styleRoot.body : styleRoot; + } + + private static getThemeLinks(styleRoot: MfeContainer) { + return Array.from(styleRoot.querySelectorAll('link')); + } + private static createGrafanaTheme2(mode: FNDashboardProps['mode']) { config.theme2 = createTheme({ colors: { @@ -135,8 +158,12 @@ class createMfe { } // NOTE: based on grafana function: 'toggleTheme' - private static removeThemeLinks(modeToBeTurnedOff: GrafanaThemeType.Light | GrafanaThemeType.Dark, timeout?: number) { - Array.from(document.getElementsByTagName('link')).forEach(createMfe.removeThemeLink(modeToBeTurnedOff, timeout)); + private static removeThemeLinks( + modeToBeTurnedOff: GrafanaThemeType.Light | GrafanaThemeType.Dark, + styleRoot: MfeContainer, + timeout?: number + ) { + createMfe.getThemeLinks(styleRoot).forEach(createMfe.removeThemeLink(modeToBeTurnedOff, timeout)); } private static removeThemeLink(modeToBeTurnedOff: FNDashboardProps['mode'], timeout?: number) { @@ -162,7 +189,11 @@ class createMfe { * NOTE: * If isRuntimeOnly then the stylesheets of the turned off theme are not removed */ - private static loadFnTheme = (mode: FNDashboardProps['mode'] = GrafanaThemeType.Light, isRuntimeOnly = false) => { + private static loadFnTheme = ( + mode: FNDashboardProps['mode'] = GrafanaThemeType.Light, + isRuntimeOnly = false, + styleRoot?: MfeContainer + ) => { createMfe.logger.info('Trying to load theme.', { mode }); const grafanaTheme2 = createMfe.createGrafanaTheme2(mode); @@ -179,11 +210,13 @@ class createMfe { return; } - createMfe.removeThemeLinks(createMfe.toggleTheme(mode)); + const themeStyleRoot = styleRoot ?? createMfe.getThemeStyleRoot(); + + createMfe.removeThemeLinks(createMfe.toggleTheme(mode), themeStyleRoot); const newCssLink = createMfe.styleSheetLink; newCssLink.href = config.bootData.themePaths[mode]; - document.body.appendChild(newCssLink); + createMfe.getThemeLinkTarget(themeStyleRoot).appendChild(newCssLink); createMfe.logger.info('Successfully loaded theme.', { mode }); }; @@ -198,6 +231,7 @@ class createMfe { const lifeCycleFn: FrameworkLifeCycles['mount'] = (props: FNDashboardProps) => { return new Promise((res, rej) => { try { + createMfe.setThemeStyleRoot(props.container); createMfe.loadFnTheme(props.mode); createMfe.Component = Component; @@ -260,10 +294,13 @@ class createMfe { static updateFnApp() { const lifeCycleFn: FrameworkLifeCycles['update'] = ({ mode, + container, ...other }: FNDashboardProps & { readonly renderingDashboardUid?: string; }) => { + createMfe.setThemeStyleRoot(container); + if (mode && mfeGetStoreState().fnGlobalReducer.mode !== mode) { mfeDispatch(updateMfeMode(mode)); diff --git a/public/app/fn-app/types.ts b/public/app/fn-app/types.ts index 70be5009ca215..7bd67afff48b2 100644 --- a/public/app/fn-app/types.ts +++ b/public/app/fn-app/types.ts @@ -16,11 +16,14 @@ export type GrafanaMicroFrontendActions = { export type AnyObject = { [key in K]: V; }; + +export type MfeContainer = Document | DocumentFragment | HTMLElement; + export interface FNDashboardProps extends FnState { name: string; fnError?: ReactNode; isLoading: (isLoading: boolean) => void; setErrors: (errors?: { [K: number | string]: string }) => void; - container?: HTMLElement | null; + container?: MfeContainer | null; mode: FnGlobalState['mode']; } From 3652015d39e019629a22b5908dff24d7568f08f4 Mon Sep 17 00:00:00 2001 From: Aquib Baig Date: Mon, 1 Jun 2026 16:12:53 +0530 Subject: [PATCH 2/3] coderabbit conversations --- public/app/fn-app/create-mfe.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/public/app/fn-app/create-mfe.ts b/public/app/fn-app/create-mfe.ts index 749bf672c05d1..6a90dd5243203 100644 --- a/public/app/fn-app/create-mfe.ts +++ b/public/app/fn-app/create-mfe.ts @@ -59,7 +59,7 @@ type DeepPartial = { class createMfe { private static readonly containerSelector = '#grafanaRoot'; - private static themeStyleRoot: MfeContainer | null = null; + private static themeStyleRoot: MfeContainer = document; private static logger = FnLoggerService; mode: FNDashboardProps['mode']; @@ -103,16 +103,10 @@ class createMfe { } private static setThemeStyleRoot(container: FNDashboardProps['container']) { - if (container) { - createMfe.themeStyleRoot = container; - } + createMfe.themeStyleRoot = container || document; } private static getThemeStyleRoot() { - if (!createMfe.themeStyleRoot) { - throw new Error('[FN Grafana]: Failed to load theme. MFE container does not exist.'); - } - return createMfe.themeStyleRoot; } From fc60ffff5f6530d3fc1c19b6fd323dd8a5199d96 Mon Sep 17 00:00:00 2001 From: Aquib Baig Date: Wed, 3 Jun 2026 06:30:11 +0530 Subject: [PATCH 3/3] inject styles into shadow root --- public/app/fn-app/create-mfe.ts | 17 +++++- public/app/fn-app/fn-app-provider.tsx | 57 ++++++++++++++----- .../fn-app/fn-dashboard-page/fn-dashboard.tsx | 26 +++++---- public/app/fn-app/utils.tsx | 14 +++-- public/microfrontends/fn_dashboard/index.html | 27 +++++++-- 5 files changed, 105 insertions(+), 36 deletions(-) diff --git a/public/app/fn-app/create-mfe.ts b/public/app/fn-app/create-mfe.ts index 6a90dd5243203..19a29682127a0 100644 --- a/public/app/fn-app/create-mfe.ts +++ b/public/app/fn-app/create-mfe.ts @@ -4,6 +4,7 @@ declare let __webpack_public_path__: string; window.__grafana_public_path__ = __webpack_public_path__.substring(0, __webpack_public_path__.lastIndexOf('build/')) || __webpack_public_path__; +import { cache as emotionCssCache } from '@emotion/css'; import { isNull, merge, noop, pick } from 'lodash'; import React, { ComponentType } from 'react'; import { createRoot } from 'react-dom/client'; @@ -118,6 +119,17 @@ class createMfe { return Array.from(styleRoot.querySelectorAll('link')); } + private static moveEmotionStylesToStyleRoot(styleRoot = createMfe.getThemeStyleRoot()) { + const styleTarget = createMfe.getThemeLinkTarget(styleRoot); + + emotionCssCache.sheet.container = styleTarget; + emotionCssCache.sheet.tags.forEach((tag) => { + if (tag.parentNode !== styleTarget) { + styleTarget.appendChild(tag); + } + }); + } + private static createGrafanaTheme2(mode: FNDashboardProps['mode']) { config.theme2 = createTheme({ colors: { @@ -226,6 +238,7 @@ class createMfe { return new Promise((res, rej) => { try { createMfe.setThemeStyleRoot(props.container); + createMfe.moveEmotionStylesToStyleRoot(); createMfe.loadFnTheme(props.mode); createMfe.Component = Component; @@ -278,6 +291,7 @@ class createMfe { } backendSrv.cancelAllInFlightRequests(); + createMfe.setThemeStyleRoot(null); return Promise.resolve(!!container); }; @@ -293,7 +307,8 @@ class createMfe { }: FNDashboardProps & { readonly renderingDashboardUid?: string; }) => { - createMfe.setThemeStyleRoot(container); + createMfe.setThemeStyleRoot(container ?? createMfe.getThemeStyleRoot()); + createMfe.moveEmotionStylesToStyleRoot(); if (mode && mfeGetStoreState().fnGlobalReducer.mode !== mode) { mfeDispatch(updateMfeMode(mode)); diff --git a/public/app/fn-app/fn-app-provider.tsx b/public/app/fn-app/fn-app-provider.tsx index f8ac8d39bac34..0e00e9c2339f9 100644 --- a/public/app/fn-app/fn-app-provider.tsx +++ b/public/app/fn-app/fn-app-provider.tsx @@ -1,4 +1,7 @@ -import { useState, useEffect, FC, PropsWithChildren } from 'react'; +import createCache from '@emotion/cache'; +import { cache as emotionCssCache } from '@emotion/css'; +import { CacheProvider } from '@emotion/react'; +import { useState, useEffect, FC, PropsWithChildren, useMemo } from 'react'; import { Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; import { Store } from 'redux'; @@ -18,11 +21,32 @@ import app from '../fn_app'; import { FNDashboardProps } from './types'; type FnAppProviderProps = Pick & { + container?: FNDashboardProps['container']; store: Store; }; export const FnAppProvider: FC> = (props) => { const { children } = props; + const emotionCache = useMemo( + () => { + const container = + props.container instanceof Document ? props.container.head : props.container || document.head; + + emotionCssCache.sheet.container = container; + emotionCssCache.sheet.tags.forEach((tag) => { + if (tag.parentNode !== container) { + container.appendChild(tag); + } + }); + + return createCache({ + key: 'grafana-mf', + container, + }); + }, + [props.container] + ); + const themeHref = config.bootData.themePaths[config.theme2.colors.mode]; const [ready, setReady] = useState(false); navigationLogger('AppWrapper', false, 'rendering'); @@ -40,19 +64,22 @@ export const FnAppProvider: FC> = (props) } return ( - - - - - -
- - {children} -
-
-
-
-
-
+ + + + + + + +
+ + {children} +
+
+
+
+
+
+
); }; diff --git a/public/app/fn-app/fn-dashboard-page/fn-dashboard.tsx b/public/app/fn-app/fn-dashboard-page/fn-dashboard.tsx index 666223f611eaf..65b6502930904 100644 --- a/public/app/fn-app/fn-dashboard-page/fn-dashboard.tsx +++ b/public/app/fn-app/fn-dashboard-page/fn-dashboard.tsx @@ -71,18 +71,20 @@ export const DashboardPortal: FC = (p) => { return ( - -
- -
-
+ {(container) => ( + +
+ +
+
+ )}
); }); diff --git a/public/app/fn-app/utils.tsx b/public/app/fn-app/utils.tsx index 44110ffc2fd6e..6f3924f342dda 100644 --- a/public/app/fn-app/utils.tsx +++ b/public/app/fn-app/utils.tsx @@ -1,16 +1,22 @@ -import { Portal } from '@mui/material'; -import { FC, PropsWithChildren } from 'react'; +import { FC, ReactNode } from 'react'; +import { createPortal } from 'react-dom'; + +import { MfeContainer } from './types'; export interface RenderPortalProps { ID: string; + children: ReactNode | ((container: MfeContainer) => ReactNode); } -export const RenderPortal: FC> = ({ ID, children }) => { +export const RenderPortal: FC = ({ ID, children }) => { const container = document.getElementById(ID); if (!container) { return null; } - return {children}; + const shadowRoot = container.shadowRoot || container.attachShadow({ mode: 'open' }); + const content = typeof children === 'function' ? children(shadowRoot) : children; + + return createPortal(content, shadowRoot); }; diff --git a/public/microfrontends/fn_dashboard/index.html b/public/microfrontends/fn_dashboard/index.html index 9abf944185cdd..c1c0ad61df777 100644 --- a/public/microfrontends/fn_dashboard/index.html +++ b/public/microfrontends/fn_dashboard/index.html @@ -1,6 +1,25 @@ -CodeRabbit Micro-frontend
\ No newline at end of file + }; + + + + + + + + +