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:
Ashley Harrison 2024-07-17 13:48:47 +01:00 committed by GitHub
parent ad6cf2ce4d
commit 334657e1cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 338 additions and 132 deletions

View File

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

View File

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

View 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?.();
};

View File

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

View File

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

View File

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

View File

@ -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': {

View File

@ -11,6 +11,7 @@ jest.mock('@grafana/runtime', () => ({
post: postMock,
}),
config: {
...jest.requireActual('@grafana/runtime').config,
loginError: false,
buildInfo: {
version: 'v1.0',

View File

@ -9,6 +9,7 @@ jest.mock('@grafana/runtime', () => ({
post: postMock,
}),
config: {
...jest.requireActual('@grafana/runtime').config,
buildInfo: {
version: 'v1.0',
commit: '1',

View File

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

View File

@ -13,6 +13,7 @@ jest.mock('@grafana/runtime', () => ({
post: postMock,
}),
config: {
...jest.requireActual('@grafana/runtime').config,
auth: {
disableLogin: false,
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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