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:
Torkel Ödegaard
2022-07-23 17:09:03 +02:00
committed by GitHub
parent 4eb0a8a98e
commit b782d9aa12
15 changed files with 165 additions and 101 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}>
<GrafanaContext.Provider value={context}>
<Page {...props}> <Page {...props}>
<div data-testid="page-children">Children</div> <div data-testid="page-children">Children</div>
</Page> </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 });

View File

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

View 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;
}

View File

@@ -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(
<GrafanaContext.Provider value={getGrafanaContextMock()}>
<GrafanaRoute location={location} history={history} match={match} route={{ component: PageComponent } as any} /> <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');

View File

@@ -1,55 +1,62 @@
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(' ') : [];
}
function updateBodyClassNames(route: RouteDescriptor, clear = false) {
for (const cls of getPageClasses(route)) {
if (clear) { if (clear) {
document.body.classList.remove(cls); document.body.classList.remove(cls);
} else { } else {
document.body.classList.add(cls); document.body.classList.add(cls);
} }
} }
} }
cleanupDOM() { function cleanupDOM() {
document.body.classList.remove('sidemenu-open--xs'); document.body.classList.remove('sidemenu-open--xs');
// cleanup tooltips // cleanup tooltips
@@ -66,13 +73,4 @@ export class GrafanaRoute extends React.Component<Props> {
for (const drop of Drop.drops) { for (const drop of Drop.drops) {
drop.destroy(); 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)} />;
}
} }

View File

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

View File

@@ -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}>
<GrafanaContext.Provider value={getGrafanaContextMock()}>
<Router history={locationService.getHistory()}> <Router history={locationService.getHistory()}>
<Route path="/explore" exact render={(props) => <GrafanaRoute {...props} route={route as any} />} /> <Route path="/explore" exact render={(props) => <GrafanaRoute {...props} route={route as any} />} />
</Router> </Router>
</GrafanaContext.Provider>
</Provider> </Provider>
); );

View File

@@ -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()}>
<GrafanaContext.Provider value={getGrafanaContextMock()}>
<Route path="/a/:pluginId" exact render={(props) => <GrafanaRoute {...props} route={route as any} />} /> <Route path="/a/:pluginId" exact render={(props) => <GrafanaRoute {...props} route={route as any} />} />
</GrafanaContext.Provider>
</Router> </Router>
); );
} }

View 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,
};
}