diff --git a/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx b/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx
index 5e5116b3ada..63efd97922f 100644
--- a/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx
+++ b/packages/grafana-ui/src/components/Sidebar/Sidebar.test.tsx
@@ -23,13 +23,32 @@ describe('Sidebar', () => {
// Verify pane is closed
expect(screen.queryByTestId('sidebar-pane-header-title')).not.toBeInTheDocument();
});
+
+ it('Can persist docked state', async () => {
+ const { unmount } = render();
+
+ act(() => screen.getByLabelText('Settings').click());
+ act(() => screen.getByLabelText('Dock').click());
+
+ unmount();
+
+ render();
+
+ 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 contextValue = useSidebar({
position: 'right',
hasOpenPane: openPane !== '',
+ persistanceKey,
onClosePane: () => setOpenPane(''),
});
diff --git a/packages/grafana-ui/src/components/Sidebar/useSidebar.tsx b/packages/grafana-ui/src/components/Sidebar/useSidebar.tsx
index 82a577bf36c..8bbb0c2ac2a 100644
--- a/packages/grafana-ui/src/components/Sidebar/useSidebar.tsx
+++ b/packages/grafana-ui/src/components/Sidebar/useSidebar.tsx
@@ -1,6 +1,8 @@
import { clamp } from 'lodash';
import React, { useCallback } from 'react';
+import { store } from '@grafana/data';
+
import { useTheme2 } from '../../themes/ThemeContext';
export type SidebarPosition = 'left' | 'right';
@@ -30,7 +32,10 @@ export interface UseSideBarOptions {
hasOpenPane?: boolean;
position?: SidebarPosition;
tabsMode?: boolean;
- compactDefault?: boolean;
+ /** Initial state for compact mode */
+ defaultToCompact?: boolean;
+ /** Initial state for docked mode */
+ defaultToDocked?: boolean;
/** defaults to 2 grid units (16px) */
bottomMargin?: number;
/** defaults to 2 grid units (16px) */
@@ -39,6 +44,11 @@ export interface UseSideBarOptions {
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;
@@ -48,21 +58,30 @@ export function useSidebar({
hasOpenPane,
position = 'right',
tabsMode,
- compactDefault = true,
+ defaultToCompact = true,
+ defaultToDocked = false,
bottomMargin = 2,
edgeMargin = 2,
contentMargin = 2,
+ persistanceKey,
onClosePane,
}: UseSideBarOptions): SidebarContextValue {
const theme = useTheme2();
- const [isDocked, setIsDocked] = React.useState(false);
- const [paneWidth, setPaneWidth] = React.useState(280);
- const [compact, setCompact] = React.useState(compactDefault);
+
+ const [isDocked, setIsDocked] = useSidebarSavedState(persistanceKey, 'docked', defaultToDocked);
+ 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
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 toolbarWidth =
((compact ? SIDE_BAR_WIDTH_ICON_ONLY : SIDE_BAR_WIDTH_WITH_TEXT) + edgeMargin + contentMargin) *
@@ -82,10 +101,10 @@ export function useSidebar({
setCompactDrag((prevDrag) => {
const newDrag = prevDrag + diff;
if (newDrag < -20 && !compact) {
- setCompact(true);
+ setCompact(() => true);
return 0;
} else if (newDrag > 20 && compact) {
- setCompact(false);
+ setCompact(() => false);
return 0;
}
@@ -98,7 +117,7 @@ export function useSidebar({
return clamp(prevWidth + diff, 100, 500);
});
},
- [hasOpenPane, compact]
+ [hasOpenPane, setCompact, setPaneWidth, compact]
);
return {
@@ -117,3 +136,53 @@ export function useSidebar({
onClosePane,
};
}
+
+function useSidebarSavedState(
+ persistanceKey: string | undefined,
+ subKey: string,
+ defaultValue: T
+) {
+ const [state, setState] = React.useState(() => {
+ 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;
+}
diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx
index 59af972e3b0..091e4805bae 100644
--- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx
+++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx
@@ -69,6 +69,7 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
hasOpenPane: Boolean(openPane),
contentMargin: 1,
position: 'right',
+ persistanceKey: 'dashboard',
onClosePane: () => editPane.closePane(),
});