Extension Sidebar: Make sidebar's width resizeable (#103526)

* Extension Sidebar: Make sidebar's width resizeable

* Extension Sidebar: Fix css

* Extension Sidebar: Remove `!important`

* Extension Sidebar: Add new drag handle classes without drag-pill

* Extension Sidebar: Use `top`/`bottom` and move scrollbar
pull/103623/head^2
Sven Grossmann 3 months ago committed by GitHub
parent 9e28886372
commit c4a0eb396b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 45
      packages/grafana-ui/src/components/DragHandle/DragHandle.tsx
  2. 50
      public/app/core/components/AppChrome/AppChrome.tsx
  3. 8
      public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.tsx
  4. 20
      public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx

@ -58,6 +58,19 @@ export const getDragStyles = (theme: GrafanaTheme2, handlePosition?: DragHandleP
},
});
const beforeVertical = {
borderRight: '1px solid transparent',
height: '100%',
left: verticalOffset,
transform: 'translateX(-50%)',
};
const beforeHorizontal = {
borderTop: '1px solid transparent',
top: horizontalOffset,
transform: 'translateY(-50%)',
};
return {
dragHandleVertical: cx(
dragHandleBase,
@ -65,12 +78,7 @@ export const getDragStyles = (theme: GrafanaTheme2, handlePosition?: DragHandleP
cursor: 'col-resize',
width: clickTargetSize,
'&:before': {
borderRight: '1px solid transparent',
height: '100%',
left: verticalOffset,
transform: 'translateX(-50%)',
},
'&:before': beforeVertical,
'&:after': {
left: verticalOffset,
@ -86,12 +94,7 @@ export const getDragStyles = (theme: GrafanaTheme2, handlePosition?: DragHandleP
height: clickTargetSize,
cursor: 'row-resize',
'&:before': {
borderTop: '1px solid transparent',
top: horizontalOffset,
transform: 'translateY(-50%)',
width: '100%',
},
'&:before': beforeHorizontal,
'&:after': {
left: '50%',
@ -101,5 +104,23 @@ export const getDragStyles = (theme: GrafanaTheme2, handlePosition?: DragHandleP
},
})
),
dragHandleBaseVertical: cx(
dragHandleBase,
css({
cursor: 'col-resize',
width: clickTargetSize,
'&:before': beforeVertical,
})
),
dragHandleBaseHorizontal: cx(
dragHandleBase,
css({
cursor: 'row-resize',
height: clickTargetSize,
'&:before': beforeHorizontal,
})
),
};
};

@ -1,10 +1,11 @@
import { css, cx } from '@emotion/css';
import classNames from 'classnames';
import { Resizable } from 're-resizable';
import { PropsWithChildren, useEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { locationSearchToObject, locationService, useScopes } from '@grafana/runtime';
import { LinkButton, useStyles2 } from '@grafana/ui';
import { getDragStyles, LinkButton, useStyles2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { useMediaQueryMinWidth } from 'app/core/hooks/useMediaQueryMinWidth';
import { Trans } from 'app/core/internationalization';
@ -14,7 +15,11 @@ import { ScopesDashboards } from 'app/features/scopes/dashboards/ScopesDashboard
import { AppChromeMenu } from './AppChromeMenu';
import { AppChromeService, DOCKED_LOCAL_STORAGE_KEY } from './AppChromeService';
import { EXTENSION_SIDEBAR_WIDTH, ExtensionSidebar } from './ExtensionSidebar/ExtensionSidebar';
import {
ExtensionSidebar,
MAX_EXTENSION_SIDEBAR_WIDTH,
MIN_EXTENSION_SIDEBAR_WIDTH,
} from './ExtensionSidebar/ExtensionSidebar';
import { useExtensionSidebarContext } from './ExtensionSidebar/ExtensionSidebarProvider';
import { MegaMenu, MENU_WIDTH } from './MegaMenu/MegaMenu';
import { useMegaMenuFocusHelper } from './MegaMenu/utils';
@ -26,7 +31,12 @@ export interface Props extends PropsWithChildren<{}> {}
export function AppChrome({ children }: Props) {
const { chrome } = useGrafana();
const { isOpen: isExtensionSidebarOpen, isEnabled: isExtensionSidebarEnabled } = useExtensionSidebarContext();
const {
isOpen: isExtensionSidebarOpen,
isEnabled: isExtensionSidebarEnabled,
extensionSidebarWidth,
setExtensionSidebarWidth,
} = useExtensionSidebarContext();
const state = chrome.useState();
const scopes = useScopes();
@ -36,7 +46,10 @@ export function AppChrome({ children }: Props) {
);
const headerLevels = useChromeHeaderLevels();
const styles = useStyles2(getStyles, headerLevels * getChromeHeaderLevelHeight());
const headerHeight = headerLevels * getChromeHeaderLevelHeight();
const styles = useStyles2(getStyles, headerHeight);
const contentSizeStyles = useStyles2(getContentSizeStyles, extensionSidebarWidth);
const dragStyles = useStyles2(getDragStyles);
useResponsiveDockedMegaMenu(chrome);
useMegaMenuFocusHelper(state.megaMenuOpen, state.megaMenuDocked);
@ -117,15 +130,24 @@ export function AppChrome({ children }: Props) {
[styles.pageContainerMenuDocked]: menuDockedAndOpen || isScopesDashboardsOpen,
[styles.pageContainerMenuDockedScopes]: menuDockedAndOpen && isScopesDashboardsOpen,
[styles.pageContainerWithSidebar]: !state.chromeless && isExtensionSidebarOpen,
[contentSizeStyles.contentWidth]: !state.chromeless && isExtensionSidebarOpen,
})}
id="pageContent"
>
{children}
</main>
{!state.chromeless && isExtensionSidebarEnabled && isExtensionSidebarOpen && (
<div className={styles.sidebarContainer}>
<Resizable
className={styles.sidebarContainer}
defaultSize={{ width: extensionSidebarWidth }}
enable={{ left: true }}
onResize={(_evt, _direction, ref) => setExtensionSidebarWidth(ref.getBoundingClientRect().width)}
handleClasses={{ left: dragStyles.dragHandleBaseVertical }}
minWidth={MIN_EXTENSION_SIDEBAR_WIDTH}
maxWidth={MAX_EXTENSION_SIDEBAR_WIDTH}
>
<ExtensionSidebar />
</div>
</Resizable>
)}
</div>
</div>
@ -241,7 +263,6 @@ const getStyles = (theme: GrafanaTheme2, headerHeight: number) => {
overflow: 'auto',
height: '100%',
minHeight: 0,
maxWidth: `calc(100% - ${EXTENSION_SIDEBAR_WIDTH})`,
}),
skipLink: css({
position: 'fixed',
@ -254,10 +275,21 @@ const getStyles = (theme: GrafanaTheme2, headerHeight: number) => {
},
}),
sidebarContainer: css({
position: 'fixed',
height: `calc(100% - ${headerHeight}px)`,
// the `Resizeable` component overrides the needed `position` and `height`
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
position: 'fixed !important' as 'fixed',
top: headerHeight,
bottom: 0,
zIndex: 2,
right: 0,
}),
};
};
const getContentSizeStyles = (_: GrafanaTheme2, extensionSidebarWidth = 0) => {
return {
contentWidth: css({
maxWidth: `calc(100% - ${extensionSidebarWidth}px) !important`,
}),
};
};

@ -10,7 +10,9 @@ import {
useExtensionSidebarContext,
} from './ExtensionSidebarProvider';
export const EXTENSION_SIDEBAR_WIDTH = '300px';
export const DEFAULT_EXTENSION_SIDEBAR_WIDTH = 300;
export const MIN_EXTENSION_SIDEBAR_WIDTH = 100;
export const MAX_EXTENSION_SIDEBAR_WIDTH = 700;
export function ExtensionSidebar() {
const styles = getStyles(useTheme2());
@ -52,13 +54,13 @@ const getStyles = (theme: GrafanaTheme2) => {
flexDirection: 'column',
gap: theme.spacing(1),
padding: theme.spacing(1),
width: EXTENSION_SIDEBAR_WIDTH,
width: '100%',
height: '100%',
overflow: 'auto',
}),
content: css({
flex: 1,
minHeight: 0,
overflow: 'auto',
}),
};
};

@ -1,11 +1,15 @@
import { createContext, ReactNode, useContext, useEffect, useState } from 'react';
import { useLocalStorage } from 'react-use';
import { store, type ExtensionInfo } from '@grafana/data';
import { config } from '@grafana/runtime';
import { ExtensionPointPluginMeta, getExtensionPointPluginMeta } from 'app/features/plugins/extensions/utils';
import { DEFAULT_EXTENSION_SIDEBAR_WIDTH } from './ExtensionSidebar';
export const EXTENSION_SIDEBAR_EXTENSION_POINT_ID = 'grafana/extension-sidebar/v0-alpha';
export const EXTENSION_SIDEBAR_DOCKED_LOCAL_STORAGE_KEY = 'grafana.navigation.extensionSidebarDocked';
export const EXTENSION_SIDEBAR_WIDTH_LOCAL_STORAGE_KEY = 'grafana.navigation.extensionSidebarWidth';
const PERMITTED_EXTENSION_SIDEBAR_PLUGINS = [
'grafana-investigations-app',
'grafana-aiassistant-app',
@ -33,6 +37,14 @@ type ExtensionSidebarContextType = {
* A map of all components that are available for the extension point.
*/
availableComponents: ExtensionPointPluginMeta;
/**
* The width of the extension sidebar.
*/
extensionSidebarWidth: number;
/**
* Set the width of the extension sidebar.
*/
setExtensionSidebarWidth: (width: number) => void;
};
export const ExtensionSidebarContext = createContext<ExtensionSidebarContextType>({
@ -41,6 +53,8 @@ export const ExtensionSidebarContext = createContext<ExtensionSidebarContextType
dockedComponentId: undefined,
setDockedComponentId: () => {},
availableComponents: new Map(),
extensionSidebarWidth: DEFAULT_EXTENSION_SIDEBAR_WIDTH,
setExtensionSidebarWidth: () => {},
});
export function useExtensionSidebarContext() {
@ -53,6 +67,10 @@ interface ExtensionSidebarContextProps {
export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarContextProps) => {
const storedDockedPluginId = store.get(EXTENSION_SIDEBAR_DOCKED_LOCAL_STORAGE_KEY);
const [extensionSidebarWidth, setExtensionSidebarWidth] = useLocalStorage(
EXTENSION_SIDEBAR_WIDTH_LOCAL_STORAGE_KEY,
DEFAULT_EXTENSION_SIDEBAR_WIDTH
);
const isEnabled = !!config.featureToggles.extensionSidebar;
// get all components for this extension point, but only for the permitted plugins
// if the extension sidebar is not enabled, we will return an empty map
@ -103,6 +121,8 @@ export const ExtensionSidebarContextProvider = ({ children }: ExtensionSidebarCo
dockedComponentId,
setDockedComponentId,
availableComponents,
extensionSidebarWidth: extensionSidebarWidth ?? DEFAULT_EXTENSION_SIDEBAR_WIDTH,
setExtensionSidebarWidth,
}}
>
{children}

Loading…
Cancel
Save