diff --git a/packages/grafana-ui/src/components/DragHandle/DragHandle.tsx b/packages/grafana-ui/src/components/DragHandle/DragHandle.tsx index 1c48a33992a..cf63b6626e5 100644 --- a/packages/grafana-ui/src/components/DragHandle/DragHandle.tsx +++ b/packages/grafana-ui/src/components/DragHandle/DragHandle.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, + }) + ), }; }; diff --git a/public/app/core/components/AppChrome/AppChrome.tsx b/public/app/core/components/AppChrome/AppChrome.tsx index 4cd95dd8d4e..c8b49ab0520 100644 --- a/public/app/core/components/AppChrome/AppChrome.tsx +++ b/public/app/core/components/AppChrome/AppChrome.tsx @@ -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} {!state.chromeless && isExtensionSidebarEnabled && isExtensionSidebarOpen && ( -
+ setExtensionSidebarWidth(ref.getBoundingClientRect().width)} + handleClasses={{ left: dragStyles.dragHandleBaseVertical }} + minWidth={MIN_EXTENSION_SIDEBAR_WIDTH} + maxWidth={MAX_EXTENSION_SIDEBAR_WIDTH} + > -
+ )} @@ -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`, + }), + }; +}; diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.tsx index d7ea37c0186..89e95a4815d 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebar.tsx @@ -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', }), }; }; diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx index ead8bd2c937..6346a561555 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider.tsx @@ -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({ @@ -41,6 +53,8 @@ export const ExtensionSidebarContext = createContext {}, 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}