diff --git a/public/app/fn-app/create-mfe.ts b/public/app/fn-app/create-mfe.ts index 4e44d4191db97..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'; @@ -28,7 +29,7 @@ import { updateMfeMode, } from 'app/store/configureMfeStore'; -import { FNDashboardProps, FailedToMountGrafanaErrorName } from './types'; +import { FNDashboardProps, FailedToMountGrafanaErrorName, MfeContainer } from './types'; /** * NOTE: @@ -59,6 +60,7 @@ type DeepPartial = { class createMfe { private static readonly containerSelector = '#grafanaRoot'; + private static themeStyleRoot: MfeContainer = document; private static logger = FnLoggerService; mode: FNDashboardProps['mode']; @@ -101,6 +103,33 @@ class createMfe { return stylesheetLink; } + private static setThemeStyleRoot(container: FNDashboardProps['container']) { + createMfe.themeStyleRoot = container || document; + } + + private static getThemeStyleRoot() { + 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 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: { @@ -135,8 +164,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 +195,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 +216,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 +237,8 @@ class createMfe { const lifeCycleFn: FrameworkLifeCycles['mount'] = (props: FNDashboardProps) => { return new Promise((res, rej) => { try { + createMfe.setThemeStyleRoot(props.container); + createMfe.moveEmotionStylesToStyleRoot(); createMfe.loadFnTheme(props.mode); createMfe.Component = Component; @@ -250,6 +291,7 @@ class createMfe { } backendSrv.cancelAllInFlightRequests(); + createMfe.setThemeStyleRoot(null); return Promise.resolve(!!container); }; @@ -260,10 +302,14 @@ class createMfe { static updateFnApp() { const lifeCycleFn: FrameworkLifeCycles['update'] = ({ mode, + container, ...other }: FNDashboardProps & { readonly renderingDashboardUid?: string; }) => { + 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/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']; } 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 + }; + + + + + + + + +