The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/routes/RoutesWrapper.tsx

177 lines
6.6 KiB

import { css } from '@emotion/css';
import { ComponentType, ReactNode } from 'react';
// eslint-disable-next-line no-restricted-imports
import { Router } from 'react-router-dom';
import { CompatRouter } from 'react-router-dom-v5-compat';
import { GrafanaTheme2 } from '@grafana/data/';
import {
locationService,
LocationServiceProvider,
useChromeHeaderHeight,
useSidecar_EXPERIMENTAL,
} from '@grafana/runtime';
import { GlobalStyles, IconButton, ModalRoot, Stack, useSplitter, useStyles2 } from '@grafana/ui';
import { AngularRoot } from '../angular/AngularRoot';
import { AppChrome } from '../core/components/AppChrome/AppChrome';
import { AppNotificationList } from '../core/components/AppNotifications/AppNotificationList';
import { ModalsContextProvider } from '../core/context/ModalsContextProvider';
import { QueriesDrawerContextProvider } from '../features/explore/QueriesDrawer/QueriesDrawerContext';
function ExtraProviders(props: { children: ReactNode; providers: Array<ComponentType<{ children: ReactNode }>> }) {
return props.providers.reduce((tree, Provider): ReactNode => {
return <Provider>{tree}</Provider>;
}, props.children);
}
type RouterWrapperProps = {
routes?: JSX.Element | false;
bodyRenderHooks: ComponentType[];
pageBanners: ComponentType[];
providers: Array<ComponentType<{ children: ReactNode }>>;
};
export function RouterWrapper(props: RouterWrapperProps) {
return (
<Router history={locationService.getHistory()}>
<LocationServiceProvider service={locationService}>
<CompatRouter>
<QueriesDrawerContextProvider>
<ExtraProviders providers={props.providers}>
<ModalsContextProvider>
<AppChrome>
<AngularRoot />
<AppNotificationList />
<Stack gap={0} grow={1} direction="column">
{props.pageBanners.map((Banner, index) => (
<Banner key={index.toString()} />
))}
{props.routes}
</Stack>
{props.bodyRenderHooks.map((Hook, index) => (
<Hook key={index.toString()} />
))}
</AppChrome>
<ModalRoot />
</ModalsContextProvider>
</ExtraProviders>
</QueriesDrawerContextProvider>
</CompatRouter>
</LocationServiceProvider>
</Router>
);
}
/**
* Renders both the main app tree and a secondary sidecar app tree to show 2 apps at the same time in a resizable split
* view.
* @param props
* @constructor
*/
export function ExperimentalSplitPaneRouterWrapper(props: RouterWrapperProps) {
const { closeApp, locationService, activePluginId } = useSidecar_EXPERIMENTAL();
let { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({
direction: 'row',
initialSize: 0.6,
dragPosition: 'end',
handleSize: 'sm',
});
// The style changes allow the resizing to be more flexible and not constrained by the content dimensions. In the
// future this could be a switch in the useSplitter but for now it's here until this feature is more final.
function alterStyles<T extends { style: React.CSSProperties }>(props: T): T {
return {
...props,
style: { ...props.style, overflow: 'auto', minWidth: 'unset', minHeight: 'unset' },
};
}
primaryProps = alterStyles(primaryProps);
secondaryProps = alterStyles(secondaryProps);
const headerHeight = useChromeHeaderHeight();
const styles = useStyles2(getStyles, headerHeight);
// Right now we consider only app plugin to be opened here but in the future we might want to just open any kind
// of url and so this should check whether there is a location in the sidecar locationService.
const sidecarOpen = Boolean(activePluginId);
return (
// Why do we need these 2 wrappers here? We want for one app case to render very similar as if there was no split
// wrapper at all but the split wrapper needs to have these wrappers to attach its container props to. At the same
// time we don't want to rerender the main app when going from 2 apps render to single app render which would happen
// if we removed the wrappers. So the solution is to keep those 2 divs but make them no actually do anything in
// case we are rendering a single app.
<div {...(sidecarOpen ? containerProps : { className: styles.dummyWrapper })}>
<div {...(sidecarOpen ? primaryProps : { className: styles.dummyWrapper })}>
<RouterWrapper {...props} />
</div>
{/* Sidecar */}
{sidecarOpen && (
<>
<div {...splitterProps} />
<div {...secondaryProps}>
<Router history={locationService.getHistory()}>
<LocationServiceProvider service={locationService}>
<CompatRouter>
<GlobalStyles />
<div className={styles.secondAppChrome}>
<div className={styles.secondAppToolbar}>
<IconButton
size={'lg'}
style={{ margin: '8px' }}
name={'times'}
aria-label={'close'}
onClick={() => closeApp()}
/>
</div>
<div className={styles.secondAppWrapper}>
{/*We don't render anything other than app plugin but we want to keep the same routing layout so*/}
{/*there are is no difference with matching relative routes between main and sidecar view.*/}
{props.routes}
</div>
</div>
</CompatRouter>
</LocationServiceProvider>
</Router>
</div>
</>
)}
</div>
);
}
const getStyles = (theme: GrafanaTheme2, headerHeight: number | undefined) => {
return {
secondAppChrome: css({
label: 'secondAppChrome',
display: 'flex',
height: '100%',
width: '100%',
paddingTop: headerHeight || 0,
flexGrow: 1,
flexDirection: 'column',
}),
secondAppToolbar: css({
label: 'secondAppToolbar',
display: 'flex',
justifyContent: 'flex-end',
}),
secondAppWrapper: css({
label: 'secondAppWrapper',
overflow: 'auto',
flex: '1',
}),
// This is basically the same as grafana-app class. This means the 2 additional wrapper divs that are in between
// grafana-app div and the main app component don't actually change anything in the layout.
dummyWrapper: css({
label: 'dummyWrapper',
display: 'flex',
height: '100vh',
flexDirection: 'column',
}),
};
};