mirror of
https://github.com/grafana/grafana.git
synced 2025-12-21 20:24:41 +08:00
Compare commits
11 Commits
zoltan/pos
...
sidebar-cl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
994374277b | ||
|
|
abf49d213f | ||
|
|
4b6a6cc454 | ||
|
|
a956ee05ec | ||
|
|
1da57371dd | ||
|
|
504eae15e8 | ||
|
|
15422d9e4c | ||
|
|
b160514317 | ||
|
|
635d9cc1ab | ||
|
|
2bbdaa634d | ||
|
|
55c447088b |
@@ -50,6 +50,7 @@ export const flows = {
|
|||||||
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
|
||||||
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click();
|
await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.outlineButton).click();
|
||||||
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click();
|
await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.Outline.item('Variables')).click();
|
||||||
|
await dashboardPage.getByGrafanaSelector(selectors.components.Sidebar.dockToggle).click();
|
||||||
await dashboardPage
|
await dashboardPage
|
||||||
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton)
|
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.addVariableButton)
|
||||||
.click();
|
.click();
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ export const versionedComponents = {
|
|||||||
closePane: {
|
closePane: {
|
||||||
'12.4.0': 'data-testid Sidebar close pane',
|
'12.4.0': 'data-testid Sidebar close pane',
|
||||||
},
|
},
|
||||||
|
dockToggle: {
|
||||||
|
'12.4.0': 'data-testid sidebar-dock-toggle',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
EditPaneHeader: {
|
EditPaneHeader: {
|
||||||
deleteButton: {
|
deleteButton: {
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export const Example: StoryFn<StoryProps> = (args) => {
|
|||||||
position: args.position,
|
position: args.position,
|
||||||
bottomMargin: 0,
|
bottomMargin: 0,
|
||||||
edgeMargin: 0,
|
edgeMargin: 0,
|
||||||
|
onClosePane: () => setOpenPane(''),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -79,7 +80,7 @@ export const Example: StoryFn<StoryProps> = (args) => {
|
|||||||
<Sidebar contextValue={contextValue}>
|
<Sidebar contextValue={contextValue}>
|
||||||
{openPane === 'settings' && (
|
{openPane === 'settings' && (
|
||||||
<Sidebar.OpenPane>
|
<Sidebar.OpenPane>
|
||||||
<Sidebar.PaneHeader title="Settings" onClose={() => togglePane('')}>
|
<Sidebar.PaneHeader title="Settings">
|
||||||
<Button variant="secondary" size="sm">
|
<Button variant="secondary" size="sm">
|
||||||
Action
|
Action
|
||||||
</Button>
|
</Button>
|
||||||
@@ -88,12 +89,12 @@ export const Example: StoryFn<StoryProps> = (args) => {
|
|||||||
)}
|
)}
|
||||||
{openPane === 'outline' && (
|
{openPane === 'outline' && (
|
||||||
<Sidebar.OpenPane>
|
<Sidebar.OpenPane>
|
||||||
<Sidebar.PaneHeader title="Outline" onClose={() => togglePane('')} />
|
<Sidebar.PaneHeader title="Outline" />
|
||||||
</Sidebar.OpenPane>
|
</Sidebar.OpenPane>
|
||||||
)}
|
)}
|
||||||
{openPane === 'add' && (
|
{openPane === 'add' && (
|
||||||
<Sidebar.OpenPane>
|
<Sidebar.OpenPane>
|
||||||
<Sidebar.PaneHeader title="Add element" onClose={() => togglePane('')} />
|
<Sidebar.PaneHeader title="Add element" />
|
||||||
</Sidebar.OpenPane>
|
</Sidebar.OpenPane>
|
||||||
)}
|
)}
|
||||||
<Sidebar.Toolbar>
|
<Sidebar.Toolbar>
|
||||||
|
|||||||
@@ -23,13 +23,33 @@ describe('Sidebar', () => {
|
|||||||
// Verify pane is closed
|
// Verify pane is closed
|
||||||
expect(screen.queryByTestId('sidebar-pane-header-title')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('sidebar-pane-header-title')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Can persist docked state', async () => {
|
||||||
|
const { unmount } = render(<TestSetup persistanceKey="test" />);
|
||||||
|
|
||||||
|
act(() => screen.getByLabelText('Settings').click());
|
||||||
|
act(() => screen.getByLabelText('Dock').click());
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
|
||||||
|
render(<TestSetup persistanceKey="test" />);
|
||||||
|
|
||||||
|
act(() => screen.getByLabelText('Settings').click());
|
||||||
|
expect(screen.getByLabelText('Undock')).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function TestSetup() {
|
interface TestSetupProps {
|
||||||
|
persistanceKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TestSetup({ persistanceKey }: TestSetupProps) {
|
||||||
const [openPane, setOpenPane] = React.useState('');
|
const [openPane, setOpenPane] = React.useState('');
|
||||||
const contextValue = useSidebar({
|
const contextValue = useSidebar({
|
||||||
position: 'right',
|
position: 'right',
|
||||||
hasOpenPane: openPane !== '',
|
hasOpenPane: openPane !== '',
|
||||||
|
persistanceKey,
|
||||||
|
onClosePane: () => setOpenPane(''),
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -37,7 +57,7 @@ function TestSetup() {
|
|||||||
<Sidebar contextValue={contextValue}>
|
<Sidebar contextValue={contextValue}>
|
||||||
{openPane === 'settings' && (
|
{openPane === 'settings' && (
|
||||||
<Sidebar.OpenPane>
|
<Sidebar.OpenPane>
|
||||||
<Sidebar.PaneHeader title="Settings" onClose={() => setOpenPane('')} />
|
<Sidebar.PaneHeader title="Settings" />
|
||||||
</Sidebar.OpenPane>
|
</Sidebar.OpenPane>
|
||||||
)}
|
)}
|
||||||
<Sidebar.Toolbar>
|
<Sidebar.Toolbar>
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ import { css, cx } from '@emotion/css';
|
|||||||
import { ReactNode, useContext } from 'react';
|
import { ReactNode, useContext } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { t } from '@grafana/i18n';
|
import { t } from '@grafana/i18n';
|
||||||
|
|
||||||
import { useStyles2, useTheme2 } from '../../themes/ThemeContext';
|
import { useStyles2, useTheme2 } from '../../themes/ThemeContext';
|
||||||
|
import { getPortalContainer } from '../Portal/Portal';
|
||||||
|
|
||||||
import { SidebarButton } from './SidebarButton';
|
import { SidebarButton } from './SidebarButton';
|
||||||
import { SidebarPaneHeader } from './SidebarPaneHeader';
|
import { SidebarPaneHeader } from './SidebarPaneHeader';
|
||||||
import { SidebarResizer } from './SidebarResizer';
|
import { SidebarResizer } from './SidebarResizer';
|
||||||
import { SIDE_BAR_WIDTH_ICON_ONLY, SIDE_BAR_WIDTH_WITH_TEXT, SidebarContext, SidebarContextValue } from './useSidebar';
|
import { SIDE_BAR_WIDTH_ICON_ONLY, SIDE_BAR_WIDTH_WITH_TEXT, SidebarContext, SidebarContextValue } from './useSidebar';
|
||||||
|
import { useCustomClickAway } from './useSidebarClickAway';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
@@ -30,9 +33,20 @@ export function SidebarComp({ children, contextValue }: Props) {
|
|||||||
|
|
||||||
const style = { [position]: theme.spacing(edgeMargin), bottom: theme.spacing(bottomMargin) };
|
const style = { [position]: theme.spacing(edgeMargin), bottom: theme.spacing(bottomMargin) };
|
||||||
|
|
||||||
|
const ref = useCustomClickAway((evt) => {
|
||||||
|
const portalContainer = getPortalContainer();
|
||||||
|
// ignore clicks inside portal container
|
||||||
|
if (evt.target instanceof Node && portalContainer && portalContainer.contains(evt.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isDocked && hasOpenPane) {
|
||||||
|
contextValue.onClosePane?.();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarContext.Provider value={contextValue}>
|
<SidebarContext.Provider value={contextValue}>
|
||||||
<div className={className} style={style}>
|
<div ref={ref} className={className} style={style}>
|
||||||
{!tabsMode && <SidebarResizer />}
|
{!tabsMode && <SidebarResizer />}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
@@ -61,7 +75,7 @@ export function SiderbarToolbar({ children }: SiderbarToolbarProps) {
|
|||||||
icon={'web-section-alt'}
|
icon={'web-section-alt'}
|
||||||
onClick={context.onToggleDock}
|
onClick={context.onToggleDock}
|
||||||
title={context.isDocked ? t('grafana-ui.sidebar.undock', 'Undock') : t('grafana-ui.sidebar.dock', 'Dock')}
|
title={context.isDocked ? t('grafana-ui.sidebar.undock', 'Undock') : t('grafana-ui.sidebar.dock', 'Dock')}
|
||||||
data-testid="sidebar-dock-toggle"
|
data-testid={selectors.components.Sidebar.dockToggle}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode, useContext } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
@@ -9,23 +9,29 @@ import { useStyles2 } from '../../themes/ThemeContext';
|
|||||||
import { IconButton } from '../IconButton/IconButton';
|
import { IconButton } from '../IconButton/IconButton';
|
||||||
import { Text } from '../Text/Text';
|
import { Text } from '../Text/Text';
|
||||||
|
|
||||||
|
import { SidebarContext } from './useSidebar';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
onClose?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SidebarPaneHeader({ children, onClose, title }: Props) {
|
export function SidebarPaneHeader({ children, title }: Props) {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
const context = useContext(SidebarContext);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('SidebarPaneHeader must be used within a Sidebar');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
{onClose && (
|
{context.onClosePane && (
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="lg"
|
size="lg"
|
||||||
name="times"
|
name="times"
|
||||||
onClick={onClose}
|
onClick={context.onClosePane}
|
||||||
aria-label={t('grafana-ui.sidebar.close', 'Close')}
|
aria-label={t('grafana-ui.sidebar.close', 'Close')}
|
||||||
tooltip={t('grafana-ui.sidebar.close', 'Close')}
|
tooltip={t('grafana-ui.sidebar.close', 'Close')}
|
||||||
data-testid={selectors.components.Sidebar.closePane}
|
data-testid={selectors.components.Sidebar.closePane}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { clamp } from 'lodash';
|
import { clamp } from 'lodash';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { store } from '@grafana/data';
|
||||||
|
|
||||||
import { useTheme2 } from '../../themes/ThemeContext';
|
import { useTheme2 } from '../../themes/ThemeContext';
|
||||||
|
|
||||||
export type SidebarPosition = 'left' | 'right';
|
export type SidebarPosition = 'left' | 'right';
|
||||||
@@ -18,6 +20,8 @@ export interface SidebarContextValue {
|
|||||||
contentMargin: number;
|
contentMargin: number;
|
||||||
onToggleDock: () => void;
|
onToggleDock: () => void;
|
||||||
onResize: (diff: number) => void;
|
onResize: (diff: number) => void;
|
||||||
|
/** Called when pane is closed or clicked outside of (in undocked mode) */
|
||||||
|
onClosePane?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SidebarContext: React.Context<SidebarContextValue | undefined> = React.createContext<
|
export const SidebarContext: React.Context<SidebarContextValue | undefined> = React.createContext<
|
||||||
@@ -28,13 +32,23 @@ export interface UseSideBarOptions {
|
|||||||
hasOpenPane?: boolean;
|
hasOpenPane?: boolean;
|
||||||
position?: SidebarPosition;
|
position?: SidebarPosition;
|
||||||
tabsMode?: boolean;
|
tabsMode?: boolean;
|
||||||
compactDefault?: boolean;
|
/** Initial state for compact mode */
|
||||||
|
defaultToCompact?: boolean;
|
||||||
|
/** Initial state for docked mode */
|
||||||
|
defaultToDocked?: boolean;
|
||||||
/** defaults to 2 grid units (16px) */
|
/** defaults to 2 grid units (16px) */
|
||||||
bottomMargin?: number;
|
bottomMargin?: number;
|
||||||
/** defaults to 2 grid units (16px) */
|
/** defaults to 2 grid units (16px) */
|
||||||
edgeMargin?: number;
|
edgeMargin?: number;
|
||||||
/** defaults to 2 grid units (16px) */
|
/** defaults to 2 grid units (16px) */
|
||||||
contentMargin?: number;
|
contentMargin?: number;
|
||||||
|
/** Called when pane is closed or clicked outside of (in undocked mode) */
|
||||||
|
onClosePane?: () => void;
|
||||||
|
/**
|
||||||
|
* Optional key to use for persisting sidebar state (docked / compact / size)
|
||||||
|
* Can only be app name as the final local storag key will be `grafana.ui.sidebar.{persistanceKey}.{docked|compact|size}`
|
||||||
|
*/
|
||||||
|
persistanceKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SIDE_BAR_WIDTH_ICON_ONLY = 5;
|
export const SIDE_BAR_WIDTH_ICON_ONLY = 5;
|
||||||
@@ -44,20 +58,30 @@ export function useSidebar({
|
|||||||
hasOpenPane,
|
hasOpenPane,
|
||||||
position = 'right',
|
position = 'right',
|
||||||
tabsMode,
|
tabsMode,
|
||||||
compactDefault = true,
|
defaultToCompact = true,
|
||||||
|
defaultToDocked = false,
|
||||||
bottomMargin = 2,
|
bottomMargin = 2,
|
||||||
edgeMargin = 2,
|
edgeMargin = 2,
|
||||||
contentMargin = 2,
|
contentMargin = 2,
|
||||||
|
persistanceKey,
|
||||||
|
onClosePane,
|
||||||
}: UseSideBarOptions): SidebarContextValue {
|
}: UseSideBarOptions): SidebarContextValue {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const [isDocked, setIsDocked] = React.useState(false);
|
|
||||||
const [paneWidth, setPaneWidth] = React.useState(280);
|
const [isDocked, setIsDocked] = useSidebarSavedState(persistanceKey, 'docked', defaultToDocked);
|
||||||
const [compact, setCompact] = React.useState(compactDefault);
|
const [compact, setCompact] = useSidebarSavedState(persistanceKey, 'compact', defaultToCompact);
|
||||||
|
const [paneWidth, setPaneWidth] = useSidebarSavedState(persistanceKey, 'size', 280);
|
||||||
|
|
||||||
// Used to accumulate drag distance to know when to change compact mode
|
// Used to accumulate drag distance to know when to change compact mode
|
||||||
const [_, setCompactDrag] = React.useState(0);
|
const [_, setCompactDrag] = React.useState(0);
|
||||||
|
|
||||||
const onToggleDock = useCallback(() => setIsDocked((prev) => !prev), []);
|
const onToggleDock = useCallback(() => {
|
||||||
|
setIsDocked((prev) => {
|
||||||
|
return !prev;
|
||||||
|
});
|
||||||
|
}, [setIsDocked]);
|
||||||
|
|
||||||
|
// Calculate how much space the outer wrapper needs to reserve for the sidebar toolbar + pane (if docked)
|
||||||
const prop = position === 'right' ? 'paddingRight' : 'paddingLeft';
|
const prop = position === 'right' ? 'paddingRight' : 'paddingLeft';
|
||||||
const toolbarWidth =
|
const toolbarWidth =
|
||||||
((compact ? SIDE_BAR_WIDTH_ICON_ONLY : SIDE_BAR_WIDTH_WITH_TEXT) + edgeMargin + contentMargin) *
|
((compact ? SIDE_BAR_WIDTH_ICON_ONLY : SIDE_BAR_WIDTH_WITH_TEXT) + edgeMargin + contentMargin) *
|
||||||
@@ -77,10 +101,10 @@ export function useSidebar({
|
|||||||
setCompactDrag((prevDrag) => {
|
setCompactDrag((prevDrag) => {
|
||||||
const newDrag = prevDrag + diff;
|
const newDrag = prevDrag + diff;
|
||||||
if (newDrag < -20 && !compact) {
|
if (newDrag < -20 && !compact) {
|
||||||
setCompact(true);
|
setCompact(() => true);
|
||||||
return 0;
|
return 0;
|
||||||
} else if (newDrag > 20 && compact) {
|
} else if (newDrag > 20 && compact) {
|
||||||
setCompact(false);
|
setCompact(() => false);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +117,7 @@ export function useSidebar({
|
|||||||
return clamp(prevWidth + diff, 100, 500);
|
return clamp(prevWidth + diff, 100, 500);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[hasOpenPane, compact]
|
[hasOpenPane, setCompact, setPaneWidth, compact]
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -109,5 +133,56 @@ export function useSidebar({
|
|||||||
edgeMargin,
|
edgeMargin,
|
||||||
bottomMargin,
|
bottomMargin,
|
||||||
contentMargin,
|
contentMargin,
|
||||||
|
onClosePane,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useSidebarSavedState<T = number | boolean>(
|
||||||
|
persistanceKey: string | undefined,
|
||||||
|
subKey: string,
|
||||||
|
defaultValue: T
|
||||||
|
) {
|
||||||
|
const [state, setState] = React.useState<T>(() => {
|
||||||
|
if (!persistanceKey) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof defaultValue === 'boolean') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
return store.getBool(`grafana.ui.sidebar.${persistanceKey}.${subKey}`, defaultValue) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof defaultValue === 'number') {
|
||||||
|
const value = Number.parseInt(store.get(`grafana.ui.sidebar.${persistanceKey}.${subKey}`), 10);
|
||||||
|
if (Number.isNaN(value)) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
return value as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
const setPersisted = useCallback(
|
||||||
|
(cb: (prevState: T) => T) => {
|
||||||
|
setState((prevState) => {
|
||||||
|
const newState = cb(prevState);
|
||||||
|
|
||||||
|
if (!persistanceKey) {
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (persistanceKey) {
|
||||||
|
store.set(`grafana.ui.sidebar.${persistanceKey}.${subKey}`, String(newState));
|
||||||
|
}
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[persistanceKey, subKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
return [state, setPersisted] as const;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cannot use the react-use useClickAway directly as it relies on mousedown event which is not ideal as the element selection uses pointerdown
|
||||||
|
* @param ref
|
||||||
|
* @param onClickAway
|
||||||
|
*/
|
||||||
|
export function useCustomClickAway(onClickAway: (evt: MouseEvent | TouchEvent) => void) {
|
||||||
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
|
const refCb = React.useRef(onClickAway);
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
refCb.current = onClickAway;
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handler = (e: MouseEvent | TouchEvent) => {
|
||||||
|
const element = ref.current;
|
||||||
|
if (element && e.target instanceof Node && !element.contains(e.target)) {
|
||||||
|
refCb.current(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('pointerdown', handler);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('pointerdown', handler);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
@@ -63,9 +63,9 @@ describe('DashboardEditPaneRenderer', () => {
|
|||||||
|
|
||||||
act(() => screen.getByLabelText('Outline').click());
|
act(() => screen.getByLabelText('Outline').click());
|
||||||
|
|
||||||
expect(await screen.findByTestId('sidebar-dock-toggle')).toBeInTheDocument();
|
expect(await screen.findByTestId(selectors.components.Sidebar.dockToggle)).toBeInTheDocument();
|
||||||
|
|
||||||
act(() => screen.getByTestId('sidebar-dock-toggle').click());
|
act(() => screen.getByTestId(selectors.components.Sidebar.dockToggle).click());
|
||||||
|
|
||||||
expect(scene.state.editPane.state.isDocked).toBe(true);
|
expect(scene.state.editPane.state.isDocked).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
|
|||||||
hasOpenPane: Boolean(openPane),
|
hasOpenPane: Boolean(openPane),
|
||||||
contentMargin: 1,
|
contentMargin: 1,
|
||||||
position: 'right',
|
position: 'right',
|
||||||
|
persistanceKey: 'dashboard',
|
||||||
|
onClosePane: () => editPane.closePane(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { getPanelPlugin } from '@grafana/data/test';
|
|||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { setPluginImportUtils } from '@grafana/runtime';
|
import { setPluginImportUtils } from '@grafana/runtime';
|
||||||
import { SceneVariableSet, VizPanel } from '@grafana/scenes';
|
import { SceneVariableSet, VizPanel } from '@grafana/scenes';
|
||||||
import { ElementSelectionContext } from '@grafana/ui';
|
import { ElementSelectionContext, Sidebar, useSidebar } from '@grafana/ui';
|
||||||
|
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { AutoGridItem } from '../scene/layout-auto-grid/AutoGridItem';
|
import { AutoGridItem } from '../scene/layout-auto-grid/AutoGridItem';
|
||||||
@@ -86,6 +86,12 @@ function buildTestScene() {
|
|||||||
return testScene;
|
return testScene;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function WrapSidebar({ children }: { children: React.ReactElement }) {
|
||||||
|
const sidebarContext = useSidebar({});
|
||||||
|
|
||||||
|
return <Sidebar contextValue={sidebarContext}>{children}</Sidebar>;
|
||||||
|
}
|
||||||
|
|
||||||
describe('DashboardOutline', () => {
|
describe('DashboardOutline', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
@@ -101,7 +107,9 @@ describe('DashboardOutline', () => {
|
|||||||
|
|
||||||
render(
|
render(
|
||||||
<ElementSelectionContext.Provider value={scene.state.editPane.state.selectionContext}>
|
<ElementSelectionContext.Provider value={scene.state.editPane.state.selectionContext}>
|
||||||
|
<WrapSidebar>
|
||||||
<DashboardOutline editPane={scene.state.editPane} isEditing={true} />
|
<DashboardOutline editPane={scene.state.editPane} isEditing={true} />
|
||||||
|
</WrapSidebar>
|
||||||
</ElementSelectionContext.Provider>
|
</ElementSelectionContext.Provider>
|
||||||
);
|
);
|
||||||
// select Row lvl 1
|
// select Row lvl 1
|
||||||
|
|||||||
@@ -25,10 +25,7 @@ export function DashboardOutline({ editPane, isEditing }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Sidebar.PaneHeader
|
<Sidebar.PaneHeader title={t('dashboard.outline.pane-header', 'Content outline')} />
|
||||||
title={t('dashboard.outline.pane-header', 'Content outline')}
|
|
||||||
onClose={() => editPane.closePane()}
|
|
||||||
/>
|
|
||||||
<Box padding={1} gap={0} display="flex" direction="column" element="ul" role="tree" position="relative">
|
<Box padding={1} gap={0} display="flex" direction="column" element="ul" role="tree" position="relative">
|
||||||
<DashboardOutlineNode sceneObject={dashboard} isEditing={isEditing} editPane={editPane} depth={0} index={0} />
|
<DashboardOutlineNode sceneObject={dashboard} isEditing={isEditing} editPane={editPane} depth={0} index={0} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
|
|||||||
|
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { SceneTimeRange } from '@grafana/scenes';
|
import { SceneTimeRange } from '@grafana/scenes';
|
||||||
|
import { Sidebar, useSidebar } from '@grafana/ui';
|
||||||
|
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { RowItem } from '../scene/layout-rows/RowItem';
|
import { RowItem } from '../scene/layout-rows/RowItem';
|
||||||
@@ -53,6 +54,12 @@ const buildTestScene = (scene: DashboardScene) => {
|
|||||||
return scene;
|
return scene;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function WrapSidebar({ children }: { children: React.ReactElement }) {
|
||||||
|
const sidebarContext = useSidebar({});
|
||||||
|
|
||||||
|
return <Sidebar contextValue={sidebarContext}>{children}</Sidebar>;
|
||||||
|
}
|
||||||
|
|
||||||
describe('EditPaneHeader', () => {
|
describe('EditPaneHeader', () => {
|
||||||
const mockEditPane = {
|
const mockEditPane = {
|
||||||
state: { selection: null },
|
state: { selection: null },
|
||||||
@@ -71,7 +78,11 @@ describe('EditPaneHeader', () => {
|
|||||||
const elementSelection = new ElementSelection([['row-test', row.getRef()]]);
|
const elementSelection = new ElementSelection([['row-test', row.getRef()]]);
|
||||||
const editableElement = elementSelection.createSelectionElement()!;
|
const editableElement = elementSelection.createSelectionElement()!;
|
||||||
|
|
||||||
render(<EditPaneHeader element={editableElement} editPane={mockEditPane} />);
|
render(
|
||||||
|
<WrapSidebar>
|
||||||
|
<EditPaneHeader element={editableElement} editPane={mockEditPane} />
|
||||||
|
</WrapSidebar>
|
||||||
|
);
|
||||||
|
|
||||||
await user.click(screen.getByTestId(selectors.components.EditPaneHeader.deleteButton));
|
await user.click(screen.getByTestId(selectors.components.EditPaneHeader.deleteButton));
|
||||||
expect(DashboardInteractions.trackRemoveRowClick).toHaveBeenCalled();
|
expect(DashboardInteractions.trackRemoveRowClick).toHaveBeenCalled();
|
||||||
@@ -84,7 +95,11 @@ describe('EditPaneHeader', () => {
|
|||||||
const elementSelection = new ElementSelection([['tab-test', tab.getRef()]]);
|
const elementSelection = new ElementSelection([['tab-test', tab.getRef()]]);
|
||||||
const editableElement = elementSelection.createSelectionElement()!;
|
const editableElement = elementSelection.createSelectionElement()!;
|
||||||
|
|
||||||
render(<EditPaneHeader element={editableElement} editPane={mockEditPane} />);
|
render(
|
||||||
|
<WrapSidebar>
|
||||||
|
<EditPaneHeader element={editableElement} editPane={mockEditPane} />
|
||||||
|
</WrapSidebar>
|
||||||
|
);
|
||||||
|
|
||||||
await user.click(screen.getByTestId(selectors.components.EditPaneHeader.deleteButton));
|
await user.click(screen.getByTestId(selectors.components.EditPaneHeader.deleteButton));
|
||||||
expect(DashboardInteractions.trackRemoveTabClick).toHaveBeenCalled();
|
expect(DashboardInteractions.trackRemoveTabClick).toHaveBeenCalled();
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sidebar.PaneHeader title={elementInfo.typeName} onClose={() => editPane.closePane()}>
|
<Sidebar.PaneHeader title={elementInfo.typeName}>
|
||||||
<Stack direction="row" gap={1}>
|
<Stack direction="row" gap={1}>
|
||||||
{element.renderActions && element.renderActions()}
|
{element.renderActions && element.renderActions()}
|
||||||
{(onCopy || onDuplicate) && (
|
{(onCopy || onDuplicate) && (
|
||||||
|
|||||||
Reference in New Issue
Block a user