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
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user