mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GrafanaContext: Exploring a way to get rid of global scope singletons (#52128)
* Context start * More progress on more generic react context for services * Update * Update Page test * Fixing tests * Moving to core app
This commit is contained in:
@@ -3356,9 +3356,7 @@ exports[`better eslint`] = {
|
|||||||
],
|
],
|
||||||
"public/app/core/utils/ConfigProvider.tsx:5381": [
|
"public/app/core/utils/ConfigProvider.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
|
||||||
],
|
],
|
||||||
"public/app/core/utils/acl.ts:5381": [
|
"public/app/core/utils/acl.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ import { GrafanaApp } from './app';
|
|||||||
import { AppChrome } from './core/components/AppChrome/AppChrome';
|
import { AppChrome } from './core/components/AppChrome/AppChrome';
|
||||||
import { AppNotificationList } from './core/components/AppNotifications/AppNotificationList';
|
import { AppNotificationList } from './core/components/AppNotifications/AppNotificationList';
|
||||||
import { NavBar } from './core/components/NavBar/NavBar';
|
import { NavBar } from './core/components/NavBar/NavBar';
|
||||||
|
import { GrafanaContext } from './core/context/GrafanaContext';
|
||||||
import { I18nProvider } from './core/internationalization';
|
import { I18nProvider } from './core/internationalization';
|
||||||
import { GrafanaRoute } from './core/navigation/GrafanaRoute';
|
import { GrafanaRoute } from './core/navigation/GrafanaRoute';
|
||||||
import { RouteDescriptor } from './core/navigation/types';
|
import { RouteDescriptor } from './core/navigation/types';
|
||||||
import { contextSrv } from './core/services/context_srv';
|
import { contextSrv } from './core/services/context_srv';
|
||||||
import { ConfigContext, ThemeProvider } from './core/utils/ConfigProvider';
|
import { ThemeProvider } from './core/utils/ConfigProvider';
|
||||||
import { CommandPalette } from './features/commandPalette/CommandPalette';
|
import { CommandPalette } from './features/commandPalette/CommandPalette';
|
||||||
import { LiveConnectionWarning } from './features/live/LiveConnectionWarning';
|
import { LiveConnectionWarning } from './features/live/LiveConnectionWarning';
|
||||||
|
|
||||||
@@ -99,6 +100,7 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { app } = this.props;
|
||||||
const { ready } = this.state;
|
const { ready } = this.state;
|
||||||
|
|
||||||
navigationLogger('AppWrapper', false, 'rendering');
|
navigationLogger('AppWrapper', false, 'rendering');
|
||||||
@@ -114,7 +116,7 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
|
|||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<ErrorBoundaryAlert style="page">
|
<ErrorBoundaryAlert style="page">
|
||||||
<ConfigContext.Provider value={config}>
|
<GrafanaContext.Provider value={app.context}>
|
||||||
<ThemeProvider value={config.theme2}>
|
<ThemeProvider value={config.theme2}>
|
||||||
<KBarProvider
|
<KBarProvider
|
||||||
actions={[]}
|
actions={[]}
|
||||||
@@ -147,7 +149,7 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
|
|||||||
</ModalsProvider>
|
</ModalsProvider>
|
||||||
</KBarProvider>
|
</KBarProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</ConfigContext.Provider>
|
</GrafanaContext.Provider>
|
||||||
</ErrorBoundaryAlert>
|
</ErrorBoundaryAlert>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ import { getStandardTransformers } from 'app/features/transformers/standardTrans
|
|||||||
import getDefaultMonacoLanguages from '../lib/monaco-languages';
|
import getDefaultMonacoLanguages from '../lib/monaco-languages';
|
||||||
|
|
||||||
import { AppWrapper } from './AppWrapper';
|
import { AppWrapper } from './AppWrapper';
|
||||||
|
import { AppChromeService } from './core/components/AppChrome/AppChromeService';
|
||||||
import { getAllOptionEditors, getAllStandardFieldConfigs } from './core/components/OptionsUI/registry';
|
import { getAllOptionEditors, getAllStandardFieldConfigs } from './core/components/OptionsUI/registry';
|
||||||
|
import { GrafanaContextType } from './core/context/GrafanaContext';
|
||||||
import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks';
|
import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks';
|
||||||
import { ModalManager } from './core/services/ModalManager';
|
import { ModalManager } from './core/services/ModalManager';
|
||||||
import { backendSrv } from './core/services/backend_srv';
|
import { backendSrv } from './core/services/backend_srv';
|
||||||
@@ -91,6 +93,8 @@ if (process.env.NODE_ENV === 'development') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class GrafanaApp {
|
export class GrafanaApp {
|
||||||
|
context!: GrafanaContextType;
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
try {
|
try {
|
||||||
setBackendSrv(backendSrv);
|
setBackendSrv(backendSrv);
|
||||||
@@ -147,6 +151,13 @@ export class GrafanaApp {
|
|||||||
// Preload selected app plugins
|
// Preload selected app plugins
|
||||||
await preloadPlugins(config.pluginsToPreload);
|
await preloadPlugins(config.pluginsToPreload);
|
||||||
|
|
||||||
|
this.context = {
|
||||||
|
backend: backendSrv,
|
||||||
|
location: locationService,
|
||||||
|
chrome: new AppChromeService(),
|
||||||
|
config,
|
||||||
|
};
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
React.createElement(AppWrapper, {
|
React.createElement(AppWrapper, {
|
||||||
app: this,
|
app: this,
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import React, { PropsWithChildren } from 'react';
|
|||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
|
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||||
|
|
||||||
import { MegaMenu } from '../MegaMenu/MegaMenu';
|
import { MegaMenu } from '../MegaMenu/MegaMenu';
|
||||||
|
|
||||||
import { appChromeService } from './AppChromeService';
|
|
||||||
import { NavToolbar } from './NavToolbar';
|
import { NavToolbar } from './NavToolbar';
|
||||||
import { TopSearchBar } from './TopSearchBar';
|
import { TopSearchBar } from './TopSearchBar';
|
||||||
import { TOP_BAR_LEVEL_HEIGHT } from './types';
|
import { TOP_BAR_LEVEL_HEIGHT } from './types';
|
||||||
@@ -16,7 +16,8 @@ export interface Props extends PropsWithChildren<{}> {}
|
|||||||
|
|
||||||
export function AppChrome({ children }: Props) {
|
export function AppChrome({ children }: Props) {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const state = appChromeService.useState();
|
const { chrome } = useGrafana();
|
||||||
|
const state = chrome.useState();
|
||||||
|
|
||||||
if (state.chromeless || !config.featureToggles.topnav) {
|
if (state.chromeless || !config.featureToggles.topnav) {
|
||||||
return <main className="main-view">{children} </main>;
|
return <main className="main-view">{children} </main>;
|
||||||
@@ -31,14 +32,12 @@ export function AppChrome({ children }: Props) {
|
|||||||
sectionNav={state.sectionNav}
|
sectionNav={state.sectionNav}
|
||||||
pageNav={state.pageNav}
|
pageNav={state.pageNav}
|
||||||
actions={state.actions}
|
actions={state.actions}
|
||||||
onToggleSearchBar={appChromeService.toggleSearchBar}
|
onToggleSearchBar={chrome.toggleSearchBar}
|
||||||
onToggleMegaMenu={appChromeService.toggleMegaMenu}
|
onToggleMegaMenu={chrome.toggleMegaMenu}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={cx(styles.content, state.searchBarHidden && styles.contentNoSearchBar)}>{children}</div>
|
<div className={cx(styles.content, state.searchBarHidden && styles.contentNoSearchBar)}>{children}</div>
|
||||||
{state.megaMenuOpen && (
|
{state.megaMenuOpen && <MegaMenu searchBarHidden={state.searchBarHidden} onClose={chrome.toggleMegaMenu} />}
|
||||||
<MegaMenu searchBarHidden={state.searchBarHidden} onClose={appChromeService.toggleMegaMenu} />
|
|
||||||
)}
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,5 +63,3 @@ export class AppChromeService {
|
|||||||
return useObservable(this.state, this.state.getValue());
|
return useObservable(this.state, this.state.getValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appChromeService = new AppChromeService();
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import { NavModelItem } from '@grafana/data';
|
import { NavModelItem } from '@grafana/data';
|
||||||
|
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||||
import { appChromeService } from './AppChromeService';
|
|
||||||
|
|
||||||
export interface AppChromeUpdateProps {
|
export interface AppChromeUpdateProps {
|
||||||
pageNav?: NavModelItem;
|
pageNav?: NavModelItem;
|
||||||
@@ -13,8 +12,10 @@ export interface AppChromeUpdateProps {
|
|||||||
* This is the way core pages and plugins update the breadcrumbs and page toolbar actions
|
* This is the way core pages and plugins update the breadcrumbs and page toolbar actions
|
||||||
*/
|
*/
|
||||||
export const AppChromeUpdate = React.memo<AppChromeUpdateProps>(({ pageNav, actions }: AppChromeUpdateProps) => {
|
export const AppChromeUpdate = React.memo<AppChromeUpdateProps>(({ pageNav, actions }: AppChromeUpdateProps) => {
|
||||||
|
const { chrome } = useGrafana();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
appChromeService.update({ pageNav, actions });
|
chrome.update({ pageNav, actions });
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||||
|
|
||||||
import { NavModelItem } from '@grafana/data';
|
import { NavModelItem } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
|
import { GrafanaContext } from 'app/core/context/GrafanaContext';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
|
|
||||||
import { PageProps } from '../Page/types';
|
import { PageProps } from '../Page/types';
|
||||||
@@ -31,15 +33,20 @@ const setup = (props: Partial<PageProps>) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const context = getGrafanaContextMock();
|
||||||
const store = configureStore();
|
const store = configureStore();
|
||||||
|
|
||||||
return render(
|
const renderResult = render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Page {...props}>
|
<GrafanaContext.Provider value={context}>
|
||||||
<div data-testid="page-children">Children</div>
|
<Page {...props}>
|
||||||
</Page>
|
<div data-testid="page-children">Children</div>
|
||||||
|
</Page>
|
||||||
|
</GrafanaContext.Provider>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return { renderResult, context };
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Render', () => {
|
describe('Render', () => {
|
||||||
@@ -68,6 +75,12 @@ describe('Render', () => {
|
|||||||
expect(screen.getAllByRole('tab').length).toBe(2);
|
expect(screen.getAllByRole('tab').length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update chrome with section and pageNav', async () => {
|
||||||
|
const { context } = setup({ navId: 'child1', pageNav });
|
||||||
|
expect(context.chrome.state.getValue().sectionNav.id).toBe('child1');
|
||||||
|
expect(context.chrome.state.getValue().pageNav).toBe(pageNav);
|
||||||
|
});
|
||||||
|
|
||||||
it('should render section nav model based on navId and item page nav', async () => {
|
it('should render section nav model based on navId and item page nav', async () => {
|
||||||
setup({ navId: 'child1', pageNav });
|
setup({ navId: 'child1', pageNav });
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ import React, { useEffect } from 'react';
|
|||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
|
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
|
||||||
|
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||||
|
|
||||||
// Components
|
|
||||||
import { appChromeService } from '../AppChrome/AppChromeService';
|
|
||||||
import { Footer } from '../Footer/Footer';
|
import { Footer } from '../Footer/Footer';
|
||||||
import { PageLayoutType, PageType } from '../Page/types';
|
import { PageLayoutType, PageType } from '../Page/types';
|
||||||
import { usePageNav } from '../Page/usePageNav';
|
import { usePageNav } from '../Page/usePageNav';
|
||||||
@@ -31,6 +30,7 @@ export const Page: PageType = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const navModel = usePageNav(navId, oldNavProp);
|
const navModel = usePageNav(navId, oldNavProp);
|
||||||
|
const { chrome } = useGrafana();
|
||||||
|
|
||||||
usePageTitle(navModel, pageNav);
|
usePageTitle(navModel, pageNav);
|
||||||
|
|
||||||
@@ -38,12 +38,12 @@ export const Page: PageType = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (navModel) {
|
if (navModel) {
|
||||||
appChromeService.update({
|
chrome.update({
|
||||||
sectionNav: navModel.node,
|
sectionNav: navModel.node,
|
||||||
...(pageNav && { pageNav }),
|
...(pageNav && { pageNav }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [navModel, pageNav]);
|
}, [navModel, pageNav, chrome]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(styles.wrapper, className)}>
|
<div className={cx(styles.wrapper, className)}>
|
||||||
|
|||||||
25
public/app/core/context/GrafanaContext.ts
Normal file
25
public/app/core/context/GrafanaContext.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React, { useContext } from 'react';
|
||||||
|
|
||||||
|
import { GrafanaConfig } from '@grafana/data';
|
||||||
|
import { LocationService } from '@grafana/runtime/src/services/LocationService';
|
||||||
|
import { BackendSrv } from '@grafana/runtime/src/services/backendSrv';
|
||||||
|
|
||||||
|
import { AppChromeService } from '../components/AppChrome/AppChromeService';
|
||||||
|
|
||||||
|
export interface GrafanaContextType {
|
||||||
|
backend: BackendSrv;
|
||||||
|
location: LocationService;
|
||||||
|
config: GrafanaConfig;
|
||||||
|
chrome: AppChromeService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GrafanaContext = React.createContext<GrafanaContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function useGrafana(): GrafanaContextType {
|
||||||
|
const context = useContext(GrafanaContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('No GrafanaContext found');
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
return context as GrafanaContextType;
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||||
|
|
||||||
import { setEchoSrv } from '@grafana/runtime';
|
import { setEchoSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { GrafanaContext } from '../context/GrafanaContext';
|
||||||
import { Echo } from '../services/echo/Echo';
|
import { Echo } from '../services/echo/Echo';
|
||||||
|
|
||||||
import { GrafanaRoute } from './GrafanaRoute';
|
import { GrafanaRoute } from './GrafanaRoute';
|
||||||
@@ -24,7 +26,9 @@ describe('GrafanaRoute', () => {
|
|||||||
const match = {} as any;
|
const match = {} as any;
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<GrafanaRoute location={location} history={history} match={match} route={{ component: PageComponent } as any} />
|
<GrafanaContext.Provider value={getGrafanaContextMock()}>
|
||||||
|
<GrafanaRoute location={location} history={history} match={match} route={{ component: PageComponent } as any} />
|
||||||
|
</GrafanaContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(capturedProps.queryParams.query).toBe('hello');
|
expect(capturedProps.queryParams.query).toBe('hello');
|
||||||
|
|||||||
@@ -1,78 +1,76 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import Drop from 'tether-drop';
|
import Drop from 'tether-drop';
|
||||||
|
|
||||||
import { locationSearchToObject, navigationLogger, reportPageview } from '@grafana/runtime';
|
import { locationSearchToObject, navigationLogger, reportPageview } from '@grafana/runtime';
|
||||||
|
|
||||||
import { appChromeService } from '../components/AppChrome/AppChromeService';
|
import { useGrafana } from '../context/GrafanaContext';
|
||||||
import { keybindingSrv } from '../services/keybindingSrv';
|
import { keybindingSrv } from '../services/keybindingSrv';
|
||||||
|
|
||||||
import { GrafanaRouteComponentProps } from './types';
|
import { GrafanaRouteComponentProps, RouteDescriptor } from './types';
|
||||||
|
|
||||||
export interface Props extends Omit<GrafanaRouteComponentProps, 'queryParams'> {}
|
export interface Props extends Omit<GrafanaRouteComponentProps, 'queryParams'> {}
|
||||||
|
|
||||||
export class GrafanaRoute extends React.Component<Props> {
|
export function GrafanaRoute(props: Props) {
|
||||||
componentDidMount() {
|
const { chrome } = useGrafana();
|
||||||
appChromeService.routeMounted(this.props.route);
|
|
||||||
|
|
||||||
this.updateBodyClassNames();
|
useEffect(() => {
|
||||||
this.cleanupDOM();
|
chrome.routeMounted(props.route);
|
||||||
|
|
||||||
|
updateBodyClassNames(props.route);
|
||||||
|
cleanupDOM();
|
||||||
// unbinds all and re-bind global keybindins
|
// unbinds all and re-bind global keybindins
|
||||||
keybindingSrv.reset();
|
keybindingSrv.reset();
|
||||||
keybindingSrv.initGlobals();
|
keybindingSrv.initGlobals();
|
||||||
reportPageview();
|
reportPageview();
|
||||||
navigationLogger('GrafanaRoute', false, 'Mounted', this.props.match);
|
navigationLogger('GrafanaRoute', false, 'Mounted', props.match);
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
return () => {
|
||||||
this.cleanupDOM();
|
navigationLogger('GrafanaRoute', false, 'Unmounted', props.route);
|
||||||
|
updateBodyClassNames(props.route, true);
|
||||||
|
};
|
||||||
|
}, [chrome, props.route, props.match]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
cleanupDOM();
|
||||||
reportPageview();
|
reportPageview();
|
||||||
navigationLogger('GrafanaRoute', false, 'Updated', this.props, prevProps);
|
navigationLogger('GrafanaRoute', false, 'Updated', props);
|
||||||
}
|
});
|
||||||
|
|
||||||
componentWillUnmount() {
|
navigationLogger('GrafanaRoute', false, 'Rendered', props.route);
|
||||||
this.updateBodyClassNames(true);
|
|
||||||
navigationLogger('GrafanaRoute', false, 'Unmounted', this.props.route);
|
|
||||||
}
|
|
||||||
|
|
||||||
getPageClasses() {
|
return <props.route.component {...props} queryParams={locationSearchToObject(props.location.search)} />;
|
||||||
return this.props.route.pageClass ? this.props.route.pageClass.split(' ') : [];
|
}
|
||||||
}
|
|
||||||
|
|
||||||
updateBodyClassNames(clear = false) {
|
function getPageClasses(route: RouteDescriptor) {
|
||||||
for (const cls of this.getPageClasses()) {
|
return route.pageClass ? route.pageClass.split(' ') : [];
|
||||||
if (clear) {
|
}
|
||||||
document.body.classList.remove(cls);
|
|
||||||
} else {
|
function updateBodyClassNames(route: RouteDescriptor, clear = false) {
|
||||||
document.body.classList.add(cls);
|
for (const cls of getPageClasses(route)) {
|
||||||
}
|
if (clear) {
|
||||||
|
document.body.classList.remove(cls);
|
||||||
|
} else {
|
||||||
|
document.body.classList.add(cls);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
cleanupDOM() {
|
|
||||||
document.body.classList.remove('sidemenu-open--xs');
|
function cleanupDOM() {
|
||||||
|
document.body.classList.remove('sidemenu-open--xs');
|
||||||
// cleanup tooltips
|
|
||||||
const tooltipById = document.getElementById('tooltip');
|
// cleanup tooltips
|
||||||
tooltipById?.parentElement?.removeChild(tooltipById);
|
const tooltipById = document.getElementById('tooltip');
|
||||||
|
tooltipById?.parentElement?.removeChild(tooltipById);
|
||||||
const tooltipsByClass = document.querySelectorAll('.tooltip');
|
|
||||||
for (let i = 0; i < tooltipsByClass.length; i++) {
|
const tooltipsByClass = document.querySelectorAll('.tooltip');
|
||||||
const tooltip = tooltipsByClass[i];
|
for (let i = 0; i < tooltipsByClass.length; i++) {
|
||||||
tooltip.parentElement?.removeChild(tooltip);
|
const tooltip = tooltipsByClass[i];
|
||||||
}
|
tooltip.parentElement?.removeChild(tooltip);
|
||||||
|
}
|
||||||
// cleanup tether-drop
|
|
||||||
for (const drop of Drop.drops) {
|
// cleanup tether-drop
|
||||||
drop.destroy();
|
for (const drop of Drop.drops) {
|
||||||
}
|
drop.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
|
||||||
const { props } = this;
|
|
||||||
navigationLogger('GrafanaRoute', false, 'Rendered', props.route);
|
|
||||||
|
|
||||||
const RouteComponent = props.route.component;
|
|
||||||
return <RouteComponent {...props} queryParams={locationSearchToObject(props.location.search)} />;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,16 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { config, GrafanaBootConfig, ThemeChangedEvent } from '@grafana/runtime';
|
import { ThemeChangedEvent } from '@grafana/runtime';
|
||||||
import { ThemeContext } from '@grafana/ui';
|
import { ThemeContext } from '@grafana/ui';
|
||||||
|
|
||||||
import { appEvents } from '../core';
|
import { appEvents } from '../core';
|
||||||
|
|
||||||
export const ConfigContext = React.createContext<GrafanaBootConfig>(config);
|
|
||||||
export const ConfigConsumer = ConfigContext.Consumer;
|
|
||||||
|
|
||||||
export const provideConfig = (component: React.ComponentType<any>) => {
|
|
||||||
const ConfigProvider = (props: any) => (
|
|
||||||
<ConfigContext.Provider value={config}>{React.createElement(component, { ...props })}</ConfigContext.Provider>
|
|
||||||
);
|
|
||||||
return ConfigProvider;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ThemeProvider = ({ children, value }: { children: React.ReactNode; value: GrafanaTheme2 }) => {
|
export const ThemeProvider = ({ children, value }: { children: React.ReactNode; value: GrafanaTheme2 }) => {
|
||||||
const [theme, setTheme] = useState(value);
|
const [theme, setTheme] = useState(value);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const sub = appEvents.subscribe(ThemeChangedEvent, (event) => {
|
const sub = appEvents.subscribe(ThemeChangedEvent, (event) => {
|
||||||
//config.theme = event.payload;
|
|
||||||
setTheme(event.payload);
|
setTheme(event.payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -32,7 +21,7 @@ export const ThemeProvider = ({ children, value }: { children: React.ReactNode;
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const provideTheme = (component: React.ComponentType<any>, theme: GrafanaTheme2) => {
|
export const provideTheme = (component: React.ComponentType<any>, theme: GrafanaTheme2) => {
|
||||||
return provideConfig((props: any) => (
|
return function ThemeProviderWrapper(props: any) {
|
||||||
<ThemeProvider value={theme}>{React.createElement(component, { ...props })}</ThemeProvider>
|
return <ThemeProvider value={theme}>{React.createElement(component, { ...props })}</ThemeProvider>;
|
||||||
));
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import { fromPairs } from 'lodash';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { Route, Router } from 'react-router-dom';
|
import { Route, Router } from 'react-router-dom';
|
||||||
|
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||||
|
|
||||||
import { DataSourceApi, DataSourceInstanceSettings, DataSourceRef, QueryEditorProps, ScopedVars } from '@grafana/data';
|
import { DataSourceApi, DataSourceInstanceSettings, DataSourceRef, QueryEditorProps, ScopedVars } from '@grafana/data';
|
||||||
import { locationService, setDataSourceSrv, setEchoSrv } from '@grafana/runtime';
|
import { locationService, setDataSourceSrv, setEchoSrv } from '@grafana/runtime';
|
||||||
|
import { GrafanaContext } from 'app/core/context/GrafanaContext';
|
||||||
import { GrafanaRoute } from 'app/core/navigation/GrafanaRoute';
|
import { GrafanaRoute } from 'app/core/navigation/GrafanaRoute';
|
||||||
import { Echo } from 'app/core/services/echo/Echo';
|
import { Echo } from 'app/core/services/echo/Echo';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
@@ -92,9 +94,11 @@ export function setupExplore(options?: SetupOptions): {
|
|||||||
|
|
||||||
const { unmount, container } = render(
|
const { unmount, container } = render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Router history={locationService.getHistory()}>
|
<GrafanaContext.Provider value={getGrafanaContextMock()}>
|
||||||
<Route path="/explore" exact render={(props) => <GrafanaRoute {...props} route={route as any} />} />
|
<Router history={locationService.getHistory()}>
|
||||||
</Router>
|
<Route path="/explore" exact render={(props) => <GrafanaRoute {...props} route={route as any} />} />
|
||||||
|
</Router>
|
||||||
|
</GrafanaContext.Provider>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { act, render, screen } from '@testing-library/react';
|
import { act, render, screen } from '@testing-library/react';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Route, Router } from 'react-router-dom';
|
import { Route, Router } from 'react-router-dom';
|
||||||
|
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||||
|
|
||||||
import { AppPlugin, PluginType, AppRootProps, NavModelItem } from '@grafana/data';
|
import { AppPlugin, PluginType, AppRootProps, NavModelItem } from '@grafana/data';
|
||||||
import { locationService, setEchoSrv } from '@grafana/runtime';
|
import { locationService, setEchoSrv } from '@grafana/runtime';
|
||||||
|
import { GrafanaContext } from 'app/core/context/GrafanaContext';
|
||||||
import { GrafanaRoute } from 'app/core/navigation/GrafanaRoute';
|
import { GrafanaRoute } from 'app/core/navigation/GrafanaRoute';
|
||||||
import { Echo } from 'app/core/services/echo/Echo';
|
import { Echo } from 'app/core/services/echo/Echo';
|
||||||
|
|
||||||
@@ -66,7 +68,9 @@ function renderUnderRouter() {
|
|||||||
|
|
||||||
render(
|
render(
|
||||||
<Router history={locationService.getHistory()}>
|
<Router history={locationService.getHistory()}>
|
||||||
<Route path="/a/:pluginId" exact render={(props) => <GrafanaRoute {...props} route={route as any} />} />
|
<GrafanaContext.Provider value={getGrafanaContextMock()}>
|
||||||
|
<Route path="/a/:pluginId" exact render={(props) => <GrafanaRoute {...props} route={route as any} />} />
|
||||||
|
</GrafanaContext.Provider>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
18
public/test/mocks/getGrafanaContextMock.ts
Normal file
18
public/test/mocks/getGrafanaContextMock.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { GrafanaConfig } from '@grafana/data';
|
||||||
|
import { BackendSrv, LocationService } from '@grafana/runtime';
|
||||||
|
import { AppChromeService } from 'app/core/components/AppChrome/AppChromeService';
|
||||||
|
import { GrafanaContextType } from 'app/core/context/GrafanaContext';
|
||||||
|
|
||||||
|
/** Not sure what this should evolve into, just a starting point */
|
||||||
|
export function getGrafanaContextMock(overrides: Partial<GrafanaContextType> = {}): GrafanaContextType {
|
||||||
|
return {
|
||||||
|
chrome: new AppChromeService(),
|
||||||
|
// eslint-disable-next-line
|
||||||
|
backend: {} as BackendSrv,
|
||||||
|
// eslint-disable-next-line
|
||||||
|
location: {} as LocationService,
|
||||||
|
// eslint-disable-next-line
|
||||||
|
config: {} as GrafanaConfig,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user