mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Navigation: Move scroll behaviour to body (#89921)
* initial attempt at body scrolling * fix login layout * minor fixes * "fix" some fixed position stuff * remember scroll position in dashboard page * fix unit tests * expose chrome header height in runtime and fix connections sticky header * fix panel edit in scenes * fix unit tests * make useChromeHeaderHeight backwards compatible, fix plugin details double scrollbar * fix sticky behaviour in explore metrics * handle when undefined * deprecate scrollRef/scrollTop * fix extra overflow on firefox
This commit is contained in:
parent
ad6cf2ce4d
commit
334657e1cb
@ -4568,10 +4568,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Styles should be written using objects.", "4"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "5"]
|
||||
],
|
||||
"public/app/features/live/LiveConnectionWarning.tsx:5381": [
|
||||
[0, 0, 0, "Styles should be written using objects.", "0"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "1"]
|
||||
],
|
||||
"public/app/features/live/centrifuge/LiveDataStream.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
@ -4965,13 +4961,10 @@ exports[`better eslint`] = {
|
||||
"public/app/features/plugins/admin/components/PluginDetailsPage.tsx:5381": [
|
||||
[0, 0, 0, "\'Layout\' import from \'@grafana/ui/src/components/Layout/Layout\' is restricted from being used by a pattern. Use Stack component instead.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "2"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "3"],
|
||||
[0, 0, 0, "Styles should be written using objects.", "4"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"]
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "2"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "3"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
|
||||
],
|
||||
"public/app/features/plugins/admin/components/PluginDetailsSignature.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
|
@ -51,5 +51,6 @@ export {
|
||||
} from './analytics/plugins/eventProperties';
|
||||
export { usePluginInteractionReporter } from './analytics/plugins/usePluginInteractionReporter';
|
||||
export { setReturnToPreviousHook, useReturnToPrevious } from './utils/returnToPrevious';
|
||||
export { setChromeHeaderHeightHook, useChromeHeaderHeight } from './utils/chromeHeaderHeight';
|
||||
export { type EmbeddedDashboardProps, EmbeddedDashboard, setEmbeddedDashboard } from './components/EmbeddedDashboard';
|
||||
export { hasPermission, hasPermissionInMetadata, hasAllPermissions, hasAnyPermission } from './utils/rbac';
|
||||
|
18
packages/grafana-runtime/src/utils/chromeHeaderHeight.ts
Normal file
18
packages/grafana-runtime/src/utils/chromeHeaderHeight.ts
Normal file
@ -0,0 +1,18 @@
|
||||
type ChromeHeaderHeightHook = () => number;
|
||||
|
||||
let chromeHeaderHeightHook: ChromeHeaderHeightHook | undefined = undefined;
|
||||
|
||||
export const setChromeHeaderHeightHook = (hook: ChromeHeaderHeightHook) => {
|
||||
chromeHeaderHeightHook = hook;
|
||||
};
|
||||
|
||||
export const useChromeHeaderHeight = () => {
|
||||
if (!chromeHeaderHeightHook) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
throw new Error('useChromeHeaderHeight hook not found in @grafana/runtime');
|
||||
}
|
||||
console.error('useChromeHeaderHeight hook not found');
|
||||
}
|
||||
|
||||
return chromeHeaderHeightHook?.();
|
||||
};
|
@ -5,6 +5,9 @@ import { GrafanaTheme2, ThemeTypographyVariant } from '@grafana/data';
|
||||
import { getFocusStyles } from '../mixins';
|
||||
|
||||
export function getElementStyles(theme: GrafanaTheme2) {
|
||||
// TODO can we get the feature toggle in a better way?
|
||||
const isBodyScrolling = window.grafanaBootData?.settings.featureToggles.bodyScrolling;
|
||||
|
||||
return css({
|
||||
html: {
|
||||
MsOverflowStyle: 'scrollbar',
|
||||
@ -23,10 +26,26 @@ export function getElementStyles(theme: GrafanaTheme2) {
|
||||
body: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
position: isBodyScrolling ? 'unset' : 'absolute',
|
||||
color: theme.colors.text.primary,
|
||||
backgroundColor: theme.colors.background.canvas,
|
||||
...getVariantStyles(theme.typography.body),
|
||||
...theme.typography.body,
|
||||
...(isBodyScrolling && {
|
||||
// react select tries prevent scrolling by setting overflow/padding-right on the body
|
||||
// Need type assertion here due to the use of !important
|
||||
// see https://github.com/frenic/csstype/issues/114#issuecomment-697201978
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
overflowY: 'scroll !important' as 'scroll',
|
||||
paddingRight: '0 !important',
|
||||
'@media print': {
|
||||
overflow: 'visible',
|
||||
},
|
||||
'@page': {
|
||||
margin: 0,
|
||||
size: 'auto',
|
||||
padding: 0,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
'h1, .h1': getVariantStyles(theme.typography.h1),
|
||||
|
@ -5,27 +5,42 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
export function getPageStyles(theme: GrafanaTheme2) {
|
||||
const maxWidthBreakpoint =
|
||||
theme.breakpoints.values.xxl + theme.spacing.gridSize * 2 + theme.components.sidemenu.width;
|
||||
const isBodyScrolling = window.grafanaBootData?.settings.featureToggles.bodyScrolling;
|
||||
|
||||
return css({
|
||||
'.grafana-app': {
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
top: 0,
|
||||
left: 0,
|
||||
},
|
||||
'.grafana-app': isBodyScrolling
|
||||
? {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: '100svh',
|
||||
}
|
||||
: {
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
top: 0,
|
||||
left: 0,
|
||||
},
|
||||
|
||||
'.main-view': {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
flex: '1 1 0',
|
||||
minWidth: 0,
|
||||
},
|
||||
'.main-view': isBodyScrolling
|
||||
? {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
position: 'relative',
|
||||
minWidth: 0,
|
||||
}
|
||||
: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
flex: '1 1 0',
|
||||
minWidth: 0,
|
||||
},
|
||||
|
||||
'.page-scrollbar-content': {
|
||||
display: 'flex',
|
||||
|
@ -39,6 +39,7 @@ import {
|
||||
setPluginExtensionsHook,
|
||||
setPluginComponentHook,
|
||||
setCurrentUser,
|
||||
setChromeHeaderHeightHook,
|
||||
} from '@grafana/runtime';
|
||||
import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelDataErrorView';
|
||||
import { setPanelRenderer } from '@grafana/runtime/src/components/PanelRenderer';
|
||||
@ -54,7 +55,7 @@ import appEvents from './core/app_events';
|
||||
import { AppChromeService } from './core/components/AppChrome/AppChromeService';
|
||||
import { getAllOptionEditors, getAllStandardFieldConfigs } from './core/components/OptionsUI/registry';
|
||||
import { PluginPage } from './core/components/Page/PluginPage';
|
||||
import { GrafanaContextType, useReturnToPreviousInternal } from './core/context/GrafanaContext';
|
||||
import { GrafanaContextType, useChromeHeaderHeight, useReturnToPreviousInternal } from './core/context/GrafanaContext';
|
||||
import { initIconCache } from './core/icons/iconBundle';
|
||||
import { initializeI18n } from './core/internationalization';
|
||||
import { setMonacoEnv } from './core/monacoEnv';
|
||||
@ -255,6 +256,7 @@ export class GrafanaApp {
|
||||
};
|
||||
|
||||
setReturnToPreviousHook(useReturnToPreviousInternal);
|
||||
setChromeHeaderHeightHook(useChromeHeaderHeight);
|
||||
|
||||
const root = createRoot(document.getElementById('reactRoot')!);
|
||||
root.render(
|
||||
|
@ -3,7 +3,7 @@ import classNames from 'classnames';
|
||||
import { PropsWithChildren, useEffect } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { locationSearchToObject, locationService } from '@grafana/runtime';
|
||||
import { config, locationSearchToObject, locationService } from '@grafana/runtime';
|
||||
import { useStyles2, LinkButton, useTheme2 } from '@grafana/ui';
|
||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||
import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange';
|
||||
@ -26,10 +26,11 @@ export function AppChrome({ children }: Props) {
|
||||
const state = chrome.useState();
|
||||
const searchBarHidden = state.searchBarHidden || state.kioskMode === KioskMode.TV;
|
||||
const theme = useTheme2();
|
||||
const styles = useStyles2(getStyles);
|
||||
const styles = useStyles2(getStyles, searchBarHidden);
|
||||
|
||||
const dockedMenuBreakpoint = theme.breakpoints.values.xl;
|
||||
const dockedMenuLocalStorageState = store.getBool(DOCKED_LOCAL_STORAGE_KEY, true);
|
||||
const menuDockedAndOpen = !state.chromeless && state.megaMenuDocked && state.megaMenuOpen;
|
||||
useMediaQueryChange({
|
||||
breakpoint: dockedMenuBreakpoint,
|
||||
onChange: (e) => {
|
||||
@ -102,10 +103,15 @@ export function AppChrome({ children }: Props) {
|
||||
)}
|
||||
<div className={contentClass}>
|
||||
<div className={styles.panes}>
|
||||
{!state.chromeless && state.megaMenuDocked && state.megaMenuOpen && (
|
||||
{menuDockedAndOpen && (
|
||||
<MegaMenu className={styles.dockedMegaMenu} onClose={() => chrome.setMegaMenuOpen(false)} />
|
||||
)}
|
||||
<main className={styles.pageContainer} id="pageContent">
|
||||
<main
|
||||
className={cx(styles.pageContainer, {
|
||||
[styles.pageContainerMenuDocked]: config.featureToggles.bodyScrolling && menuDockedAndOpen,
|
||||
})}
|
||||
id="pageContent"
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
@ -119,14 +125,14 @@ export function AppChrome({ children }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
const getStyles = (theme: GrafanaTheme2, searchBarHidden: boolean) => {
|
||||
return {
|
||||
content: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
paddingTop: TOP_BAR_LEVEL_HEIGHT * 2,
|
||||
flexGrow: 1,
|
||||
height: '100%',
|
||||
height: config.featureToggles.bodyScrolling ? 'auto' : '100%',
|
||||
}),
|
||||
contentNoSearchBar: css({
|
||||
paddingTop: TOP_BAR_LEVEL_HEIGHT,
|
||||
@ -134,16 +140,31 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
contentChromeless: css({
|
||||
paddingTop: 0,
|
||||
}),
|
||||
dockedMegaMenu: css({
|
||||
background: theme.colors.background.primary,
|
||||
borderRight: `1px solid ${theme.colors.border.weak}`,
|
||||
display: 'none',
|
||||
zIndex: theme.zIndex.navbarFixed,
|
||||
dockedMegaMenu: css(
|
||||
config.featureToggles.bodyScrolling
|
||||
? {
|
||||
background: theme.colors.background.primary,
|
||||
borderRight: `1px solid ${theme.colors.border.weak}`,
|
||||
display: 'none',
|
||||
position: 'fixed',
|
||||
height: `calc(100% - ${searchBarHidden ? TOP_BAR_LEVEL_HEIGHT : TOP_BAR_LEVEL_HEIGHT * 2}px)`,
|
||||
zIndex: theme.zIndex.navbarFixed,
|
||||
|
||||
[theme.breakpoints.up('xl')]: {
|
||||
display: 'block',
|
||||
},
|
||||
}),
|
||||
[theme.breakpoints.up('xl')]: {
|
||||
display: 'block',
|
||||
},
|
||||
}
|
||||
: {
|
||||
background: theme.colors.background.primary,
|
||||
borderRight: `1px solid ${theme.colors.border.weak}`,
|
||||
display: 'none',
|
||||
zIndex: theme.zIndex.navbarFixed,
|
||||
|
||||
[theme.breakpoints.up('xl')]: {
|
||||
display: 'block',
|
||||
},
|
||||
}
|
||||
),
|
||||
topNav: css({
|
||||
display: 'flex',
|
||||
position: 'fixed',
|
||||
@ -153,37 +174,58 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
background: theme.colors.background.primary,
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
panes: css({
|
||||
label: 'page-panes',
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
flexGrow: 1,
|
||||
minHeight: 0,
|
||||
flexDirection: 'column',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
}),
|
||||
pageContainer: css({
|
||||
label: 'page-container',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
overflow: 'auto',
|
||||
'@media print': {
|
||||
overflow: 'visible',
|
||||
},
|
||||
'@page': {
|
||||
margin: 0,
|
||||
size: 'auto',
|
||||
padding: 0,
|
||||
},
|
||||
panes: css(
|
||||
config.featureToggles.bodyScrolling
|
||||
? {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
label: 'page-panes',
|
||||
}
|
||||
: {
|
||||
label: 'page-panes',
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
flexGrow: 1,
|
||||
minHeight: 0,
|
||||
flexDirection: 'column',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
}
|
||||
),
|
||||
pageContainerMenuDocked: css({
|
||||
paddingLeft: '300px',
|
||||
}),
|
||||
pageContainer: css(
|
||||
config.featureToggles.bodyScrolling
|
||||
? {
|
||||
label: 'page-container',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
}
|
||||
: {
|
||||
label: 'page-container',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
overflow: 'auto',
|
||||
'@media print': {
|
||||
overflow: 'visible',
|
||||
},
|
||||
'@page': {
|
||||
margin: 0,
|
||||
size: 'auto',
|
||||
padding: 0,
|
||||
},
|
||||
}
|
||||
),
|
||||
skipLink: css({
|
||||
position: 'absolute',
|
||||
position: 'fixed',
|
||||
top: -1000,
|
||||
|
||||
':focus': {
|
||||
|
@ -11,6 +11,7 @@ jest.mock('@grafana/runtime', () => ({
|
||||
post: postMock,
|
||||
}),
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
loginError: false,
|
||||
buildInfo: {
|
||||
version: 'v1.0',
|
||||
|
@ -9,6 +9,7 @@ jest.mock('@grafana/runtime', () => ({
|
||||
post: postMock,
|
||||
}),
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
buildInfo: {
|
||||
version: 'v1.0',
|
||||
commit: '1',
|
||||
|
@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { Branding } from '../Branding/Branding';
|
||||
@ -36,7 +37,9 @@ export const LoginLayout = ({ children, branding, isChangingPassword }: React.Pr
|
||||
|
||||
return (
|
||||
<Branding.LoginBackground
|
||||
className={cx(loginStyles.container, startAnim && loginStyles.loginAnim, branding?.loginBackground)}
|
||||
className={cx(loginStyles.container, startAnim && loginStyles.loginAnim, branding?.loginBackground, {
|
||||
[loginStyles.containerBodyScrolling]: config.featureToggles.bodyScrolling,
|
||||
})}
|
||||
>
|
||||
<div className={loginStyles.loginMain}>
|
||||
<div className={cx(loginStyles.loginContent, loginBoxBackground, 'login-content-box')}>
|
||||
@ -93,6 +96,9 @@ export const getLoginStyles = (theme: GrafanaTheme2) => {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}),
|
||||
containerBodyScrolling: css({
|
||||
flex: 1,
|
||||
}),
|
||||
loginAnim: css({
|
||||
['&:before']: {
|
||||
opacity: 1,
|
||||
|
@ -13,6 +13,7 @@ jest.mock('@grafana/runtime', () => ({
|
||||
post: postMock,
|
||||
}),
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
auth: {
|
||||
disableLogin: false,
|
||||
},
|
||||
|
@ -1,28 +1,33 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
|
||||
|
||||
type Props = Parameters<typeof CustomScrollbar>[0];
|
||||
|
||||
// Shim to provide API-compatibility for Page's scroll-related props
|
||||
// when bodyScrolling is enabled, this is a no-op
|
||||
// TODO remove this shim completely when bodyScrolling is enabled
|
||||
export default function NativeScrollbar({ children, scrollRefCallback, scrollTop, divId }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && scrollRefCallback) {
|
||||
if (!config.featureToggles.bodyScrolling && ref.current && scrollRefCallback) {
|
||||
scrollRefCallback(ref.current);
|
||||
}
|
||||
}, [ref, scrollRefCallback]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && scrollTop != null) {
|
||||
if (!config.featureToggles.bodyScrolling && ref.current && scrollTop != null) {
|
||||
ref.current?.scrollTo(0, scrollTop);
|
||||
}
|
||||
}, [scrollTop]);
|
||||
|
||||
return (
|
||||
return config.featureToggles.bodyScrolling ? (
|
||||
children
|
||||
) : (
|
||||
// Set the .scrollbar-view class to help e2e tests find this, like in CustomScrollbar
|
||||
<div ref={ref} className={cx(styles.nativeScrollbars, 'scrollbar-view')} id={divId}>
|
||||
{children}
|
||||
|
@ -2,6 +2,7 @@ import { css, cx } from '@emotion/css';
|
||||
import { useLayoutEffect } from 'react';
|
||||
|
||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||
|
||||
@ -98,14 +99,24 @@ Page.Contents = PageContents;
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
wrapper: css({
|
||||
label: 'page-wrapper',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flex: '1 1 0',
|
||||
flexDirection: 'column',
|
||||
minHeight: 0,
|
||||
}),
|
||||
wrapper: css(
|
||||
config.featureToggles.bodyScrolling
|
||||
? {
|
||||
label: 'page-wrapper',
|
||||
display: 'flex',
|
||||
flex: '1 1 0',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
}
|
||||
: {
|
||||
label: 'page-wrapper',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flex: '1 1 0',
|
||||
flexDirection: 'column',
|
||||
minHeight: 0,
|
||||
}
|
||||
),
|
||||
pageContent: css({
|
||||
label: 'page-content',
|
||||
flexGrow: 1,
|
||||
|
@ -22,14 +22,14 @@ export interface PageProps extends HTMLAttributes<HTMLDivElement> {
|
||||
/** Control the page layout. */
|
||||
layout?: PageLayoutType;
|
||||
/**
|
||||
* @deprecated this will be removed when bodyScrolling is enabled by default
|
||||
* Can be used to get the scroll container element to access scroll position
|
||||
* */
|
||||
// Probably will deprecate this in the future in favor of just scrolling document.body directly
|
||||
scrollRef?: RefCallback<HTMLDivElement>;
|
||||
/**
|
||||
* @deprecated this will be removed when bodyScrolling is enabled by default
|
||||
* Can be used to update the current scroll position
|
||||
* */
|
||||
// Probably will deprecate this in the future in favor of just scrolling document.body directly
|
||||
scrollTop?: number;
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { createContext, useCallback, useContext } from 'react';
|
||||
|
||||
import { GrafanaConfig } from '@grafana/data';
|
||||
import { LocationService, locationService, BackendSrv } from '@grafana/runtime';
|
||||
import { LocationService, locationService, BackendSrv, config } from '@grafana/runtime';
|
||||
|
||||
import { AppChromeService } from '../components/AppChrome/AppChromeService';
|
||||
import { NewFrontendAssetsChecker } from '../services/NewFrontendAssetsChecker';
|
||||
@ -41,3 +41,18 @@ export function useReturnToPreviousInternal() {
|
||||
[chrome]
|
||||
);
|
||||
}
|
||||
|
||||
const SINGLE_HEADER_BAR_HEIGHT = 40;
|
||||
|
||||
export function useChromeHeaderHeight() {
|
||||
const { chrome } = useGrafana();
|
||||
const { kioskMode, searchBarHidden, chromeless } = chrome.useState();
|
||||
|
||||
if (kioskMode || chromeless || !config.featureToggles.bodyScrolling) {
|
||||
return 0;
|
||||
} else if (searchBarHidden) {
|
||||
return SINGLE_HEADER_BAR_HEIGHT;
|
||||
} else {
|
||||
return SINGLE_HEADER_BAR_HEIGHT * 2;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { BouncingLoader } from '../components/BouncingLoader/BouncingLoader';
|
||||
@ -9,7 +10,12 @@ export function GrafanaRouteLoading() {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.loadingPage}>
|
||||
<div
|
||||
className={cx({
|
||||
[styles.loadingPage]: !config.featureToggles.bodyScrolling,
|
||||
[styles.loadingPageBodyScrolling]: config.featureToggles.bodyScrolling,
|
||||
})}
|
||||
>
|
||||
<BouncingLoader />
|
||||
</div>
|
||||
);
|
||||
@ -24,4 +30,12 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
loadingPageBodyScrolling: css({
|
||||
backgroundColor: theme.colors.background.primary,
|
||||
flex: 1,
|
||||
flexDrection: 'column',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
});
|
||||
|
@ -15,6 +15,10 @@ import { ROUTE_BASE_ID, ROUTES } from './constants';
|
||||
|
||||
jest.mock('app/core/services/context_srv');
|
||||
jest.mock('app/features/datasources/api');
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
useChromeHeaderHeight: jest.fn(),
|
||||
}));
|
||||
|
||||
const renderPage = (
|
||||
path = `/${ROUTE_BASE_ID}`,
|
||||
|
@ -12,6 +12,11 @@ import { AddNewConnection } from './ConnectData';
|
||||
|
||||
jest.mock('app/features/datasources/api');
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
useChromeHeaderHeight: jest.fn(),
|
||||
}));
|
||||
|
||||
const renderPage = (plugins: CatalogPlugin[] = []): RenderResult => {
|
||||
return render(
|
||||
<TestProvider storeState={{ plugins: getPluginsStateMock(plugins) }}>
|
||||
|
@ -2,16 +2,17 @@ import { css } from '@emotion/css';
|
||||
import * as React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { config, useChromeHeaderHeight } from '@grafana/runtime';
|
||||
import { Icon, Input, useStyles2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
const getStyles = (theme: GrafanaTheme2, headerHeight: number) => ({
|
||||
searchContainer: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
top: headerHeight,
|
||||
backgroundColor: theme.colors.background.primary,
|
||||
zIndex: 2,
|
||||
padding: theme.spacing(2, 0),
|
||||
@ -26,7 +27,8 @@ export interface Props {
|
||||
}
|
||||
|
||||
export const Search = ({ onChange, value }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const chromeHeaderHeight = useChromeHeaderHeight();
|
||||
const styles = useStyles2(getStyles, config.featureToggles.bodyScrolling ? chromeHeaderHeight ?? 0 : 0);
|
||||
|
||||
return (
|
||||
<div className={styles.searchContainer}>
|
||||
|
@ -2,6 +2,7 @@ import { css, cx } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { SceneComponentProps } from '@grafana/scenes';
|
||||
import { Button, ToolbarButton, useStyles2 } from '@grafana/ui';
|
||||
|
||||
@ -32,7 +33,13 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
|
||||
return (
|
||||
<>
|
||||
<NavToolbarActions dashboard={dashboard} />
|
||||
<div {...containerProps} data-testid={selectors.components.PanelEditor.General.content}>
|
||||
<div
|
||||
{...containerProps}
|
||||
className={cx(containerProps.className, {
|
||||
[styles.content]: config.featureToggles.bodyScrolling,
|
||||
})}
|
||||
data-testid={selectors.components.PanelEditor.General.content}
|
||||
>
|
||||
<div {...primaryProps} className={cx(primaryProps.className, styles.body)}>
|
||||
<VizAndDataPane model={model} />
|
||||
</div>
|
||||
@ -176,6 +183,11 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
minHeight: 0,
|
||||
width: '100%',
|
||||
}),
|
||||
content: css({
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}),
|
||||
body: css({
|
||||
label: 'body',
|
||||
flexGrow: 1,
|
||||
|
@ -68,6 +68,7 @@ export type Props = Themeable2 &
|
||||
export interface State {
|
||||
editPanel: PanelModel | null;
|
||||
viewPanel: PanelModel | null;
|
||||
editView: string | null;
|
||||
updateScrollTop?: number;
|
||||
rememberScrollTop?: number;
|
||||
showLoadingState: boolean;
|
||||
@ -87,6 +88,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
|
||||
getCleanState(): State {
|
||||
return {
|
||||
editView: null,
|
||||
editPanel: null,
|
||||
viewPanel: null,
|
||||
showLoadingState: false,
|
||||
@ -194,6 +196,13 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
this.props.notifyApp(createErrorNotification(`Panel not found`));
|
||||
locationService.partial({ editPanel: null, viewPanel: null });
|
||||
}
|
||||
|
||||
if (config.featureToggles.bodyScrolling) {
|
||||
// Update window scroll position
|
||||
if (this.state.updateScrollTop !== undefined && this.state.updateScrollTop !== prevState.updateScrollTop) {
|
||||
window.scrollTo(0, this.state.updateScrollTop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateLiveTimer = () => {
|
||||
@ -209,6 +218,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
|
||||
const urlEditPanelId = queryParams.editPanel;
|
||||
const urlViewPanelId = queryParams.viewPanel;
|
||||
const urlEditView = queryParams.editview;
|
||||
|
||||
if (!dashboard) {
|
||||
return state;
|
||||
@ -216,13 +226,33 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
|
||||
const updatedState = { ...state };
|
||||
|
||||
if (config.featureToggles.bodyScrolling) {
|
||||
// Entering settings view
|
||||
if (!state.editView && urlEditView) {
|
||||
updatedState.editView = urlEditView;
|
||||
updatedState.rememberScrollTop = window.scrollY;
|
||||
updatedState.updateScrollTop = 0;
|
||||
}
|
||||
|
||||
// Leaving settings view
|
||||
else if (state.editView && !urlEditView) {
|
||||
updatedState.updateScrollTop = state.rememberScrollTop;
|
||||
updatedState.editView = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Entering edit mode
|
||||
if (!state.editPanel && urlEditPanelId) {
|
||||
const panel = dashboard.getPanelByUrlId(urlEditPanelId);
|
||||
if (panel) {
|
||||
if (dashboard.canEditPanel(panel)) {
|
||||
updatedState.editPanel = panel;
|
||||
updatedState.rememberScrollTop = state.scrollElement?.scrollTop;
|
||||
updatedState.rememberScrollTop = config.featureToggles.bodyScrolling
|
||||
? window.scrollY
|
||||
: state.scrollElement?.scrollTop;
|
||||
if (config.featureToggles.bodyScrolling) {
|
||||
updatedState.updateScrollTop = 0;
|
||||
}
|
||||
} else {
|
||||
updatedState.editPanelAccessDenied = true;
|
||||
}
|
||||
@ -244,7 +274,9 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
// Should move this state out of dashboard in the future
|
||||
dashboard.initViewPanel(panel);
|
||||
updatedState.viewPanel = panel;
|
||||
updatedState.rememberScrollTop = state.scrollElement?.scrollTop;
|
||||
updatedState.rememberScrollTop = config.featureToggles.bodyScrolling
|
||||
? window.scrollY
|
||||
: state.scrollElement?.scrollTop;
|
||||
updatedState.updateScrollTop = 0;
|
||||
} else {
|
||||
updatedState.panelNotFound = true;
|
||||
|
@ -62,18 +62,18 @@ export class LiveConnectionWarning extends PureComponent<Props, State> {
|
||||
|
||||
const getStyle = stylesFactory((theme: GrafanaTheme2) => {
|
||||
return {
|
||||
foot: css`
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
z-index: 10000;
|
||||
cursor: wait;
|
||||
margin: 16px;
|
||||
`,
|
||||
warn: css`
|
||||
max-width: 400px;
|
||||
margin: auto;
|
||||
`,
|
||||
foot: css({
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 10000,
|
||||
cursor: 'wait',
|
||||
margin: theme.spacing(2),
|
||||
}),
|
||||
warn: css({
|
||||
maxWidth: '400px',
|
||||
margin: 'auto',
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
@ -131,9 +131,11 @@ export function PluginDetailsBody({ plugin, queryParams, pageId }: Props): JSX.E
|
||||
}
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css({
|
||||
height: '100%',
|
||||
}),
|
||||
container: config.featureToggles.bodyScrolling
|
||||
? css({})
|
||||
: css({
|
||||
height: '100%',
|
||||
}),
|
||||
readme: css({
|
||||
'& img': {
|
||||
maxWidth: '100%',
|
||||
|
@ -99,20 +99,24 @@ export function PluginDetailsPage({
|
||||
|
||||
export const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
alert: css`
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
`,
|
||||
subtitle: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${theme.spacing(1)};
|
||||
`,
|
||||
alert: css({
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
subtitle: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
// Needed due to block formatting context
|
||||
tabContent: css`
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
padding-left: 5px;
|
||||
`,
|
||||
tabContent: config.featureToggles.bodyScrolling
|
||||
? css({
|
||||
paddingLeft: '5px',
|
||||
})
|
||||
: css({
|
||||
overflow: 'auto',
|
||||
height: '100%',
|
||||
paddingLeft: '5px',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { AdHocVariableFilter, GrafanaTheme2, PageLayoutType, VariableHide, urlUtil } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { locationService, useChromeHeaderHeight } from '@grafana/runtime';
|
||||
import {
|
||||
AdHocFiltersVariable,
|
||||
DataSourceVariable,
|
||||
@ -212,7 +212,8 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
|
||||
|
||||
static Component = ({ model }: SceneComponentProps<DataTrail>) => {
|
||||
const { controls, topScene, history, settings, metric } = model.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
const chromeHeaderHeight = useChromeHeaderHeight();
|
||||
const styles = useStyles2(getStyles, chromeHeaderHeight ?? 0);
|
||||
const showHeaderForFirstTimeUsers = getTrailStore().recent.length < 2;
|
||||
|
||||
return (
|
||||
@ -266,7 +267,7 @@ function getVariableSet(initialDS?: string, metric?: string, initialFilters?: Ad
|
||||
});
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
function getStyles(theme: GrafanaTheme2, chromeHeaderHeight: number) {
|
||||
return {
|
||||
container: css({
|
||||
flexGrow: 1,
|
||||
@ -290,7 +291,7 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
position: 'sticky',
|
||||
background: theme.isDark ? theme.colors.background.canvas : theme.colors.background.primary,
|
||||
zIndex: theme.zIndex.navbarFixed,
|
||||
top: 0,
|
||||
top: chromeHeaderHeight,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user