diff --git a/public/app/features/explore/ContentOutline/ContentOutline.test.tsx b/public/app/features/explore/ContentOutline/ContentOutline.test.tsx index 537755195bf..6b4107b6d3a 100644 --- a/public/app/features/explore/ContentOutline/ContentOutline.test.tsx +++ b/public/app/features/explore/ContentOutline/ContentOutline.test.tsx @@ -1,6 +1,8 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { store } from '@grafana/data'; + import { ContentOutline } from './ContentOutline'; jest.mock('./ContentOutlineContext', () => ({ @@ -73,15 +75,15 @@ const setup = (mergeSingleChild = false) => { describe('', () => { it('toggles content on button click', async () => { setup(); - let showContentOutlineButton = screen.getByRole('button', { name: 'Expand outline' }); + let showContentOutlineButton = screen.getByRole('button', { name: 'Collapse outline' }); expect(showContentOutlineButton).toBeInTheDocument(); await userEvent.click(showContentOutlineButton); - const hideContentOutlineButton = screen.getByRole('button', { name: 'Collapse outline' }); + const hideContentOutlineButton = screen.getByRole('button', { name: 'Expand outline' }); expect(hideContentOutlineButton).toBeInTheDocument(); await userEvent.click(hideContentOutlineButton); - showContentOutlineButton = screen.getByRole('button', { name: 'Expand outline' }); + showContentOutlineButton = screen.getByRole('button', { name: 'Collapse outline' }); expect(showContentOutlineButton).toBeInTheDocument(); }); @@ -157,4 +159,15 @@ describe('', () => { expect(unregisterMock).toHaveBeenCalledWith('item-2-1'); }); + + it('should retrieve the last expanded state from local storage', async () => { + const getBoolMock = jest.spyOn(store, 'getBool').mockReturnValue(false); + setup(); + const collapseContentOutlineButton = screen.queryByRole('button', { name: 'Collapse outline' }); + const expandContentOutlineButton = screen.queryByRole('button', { name: 'Expand outline' }); + expect(collapseContentOutlineButton).not.toBeInTheDocument(); + expect(expandContentOutlineButton).toBeInTheDocument(); + + getBoolMock.mockRestore(); + }); }); diff --git a/public/app/features/explore/ContentOutline/ContentOutline.tsx b/public/app/features/explore/ContentOutline/ContentOutline.tsx index 415f34f3e52..ddd82ce01a3 100644 --- a/public/app/features/explore/ContentOutline/ContentOutline.tsx +++ b/public/app/features/explore/ContentOutline/ContentOutline.tsx @@ -2,12 +2,11 @@ import { css, cx } from '@emotion/css'; import { Fragment, useEffect, useRef, useState } from 'react'; import { useToggle, useScroll } from 'react-use'; -import { GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2, store } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { useStyles2, PanelContainer, CustomScrollbar } from '@grafana/ui'; import { ContentOutlineItemContextProps, useContentOutlineContext } from './ContentOutlineContext'; -import { ITEM_TYPES } from './ContentOutlineItem'; import { ContentOutlineItemButton } from './ContentOutlineItemButton'; function scrollableChildren(item: ContentOutlineItemContextProps) { @@ -35,8 +34,15 @@ function shouldBeActive( } } +export const CONTENT_OUTLINE_LOCAL_STORAGE_KEYS = { + visible: 'grafana.explore.contentOutline.visible', + expanded: 'grafana.explore.contentOutline.expanded', +}; + export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement | undefined; panelId: string }) { - const [contentOutlineExpanded, toggleContentOutlineExpanded] = useToggle(false); + const [contentOutlineExpanded, toggleContentOutlineExpanded] = useToggle( + store.getBool(CONTENT_OUTLINE_LOCAL_STORAGE_KEYS.expanded, true) + ); const styles = useStyles2(getStyles, contentOutlineExpanded); const scrollerRef = useRef(scroller || null); const { y: verticalScroll } = useScroll(scrollerRef); @@ -57,12 +63,7 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement | }, {}); }); - const scrollIntoView = ( - ref: HTMLElement | null, - itemPanelId: string, - itemType: ITEM_TYPES | undefined, - customOffsetTop = 0 - ) => { + const scrollIntoView = (ref: HTMLElement | null, customOffsetTop = 0) => { let scrollValue = 0; let el: HTMLElement | null | undefined = ref; @@ -88,10 +89,10 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement | }); if (activeParent) { - scrollIntoView(activeParent.ref, activeParent.panelId, activeParent.type, activeParent.customTopOffset); + scrollIntoView(activeParent.ref, activeParent.customTopOffset); } } else { - scrollIntoView(item.ref, item.panelId, item.type, item.customTopOffset); + scrollIntoView(item.ref, item.customTopOffset); reportInteraction('explore_toolbar_contentoutline_clicked', { item: 'select_section', type: item.panelId, @@ -100,6 +101,7 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement | }; const toggle = () => { + store.set(CONTENT_OUTLINE_LOCAL_STORAGE_KEYS.expanded, !contentOutlineExpanded); toggleContentOutlineExpanded(); reportInteraction('explore_toolbar_contentoutline_clicked', { item: 'outline', diff --git a/public/app/features/explore/Explore.test.tsx b/public/app/features/explore/Explore.test.tsx index 4ea74b48fc4..8cf7d3382e7 100644 --- a/public/app/features/explore/Explore.test.tsx +++ b/public/app/features/explore/Explore.test.tsx @@ -2,7 +2,15 @@ import { render, screen } from '@testing-library/react'; import { Props as AutoSizerProps } from 'react-virtualized-auto-sizer'; import { TestProvider } from 'test/helpers/TestProvider'; -import { CoreApp, createTheme, DataSourceApi, EventBusSrv, LoadingState, PluginExtensionTypes } from '@grafana/data'; +import { + CoreApp, + createTheme, + DataSourceApi, + EventBusSrv, + LoadingState, + PluginExtensionTypes, + store, +} from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { usePluginLinkExtensions } from '@grafana/runtime'; import { configureStore } from 'app/store/configureStore'; @@ -225,4 +233,14 @@ describe('Explore', () => { expect(dataSourcePicker).toBeInTheDocument(); }); }); + + describe('Content Outline', () => { + it('should retrieve the last visible state from local storage', async () => { + const getBoolMock = jest.spyOn(store, 'getBool').mockReturnValue(false); + setup(); + const showContentOutlineButton = screen.queryByRole('button', { name: 'Collapse outline' }); + expect(showContentOutlineButton).not.toBeInTheDocument(); + getBoolMock.mockRestore(); + }); + }); }); diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 22ce2d7688b..7bf13532be5 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -14,6 +14,7 @@ import { QueryFixAction, RawTimeRange, SplitOpenOptions, + store, SupplementaryQueryType, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; @@ -34,7 +35,7 @@ import { StoreState } from 'app/types'; import { getTimeZone } from '../profile/state/selectors'; -import { ContentOutline } from './ContentOutline/ContentOutline'; +import { CONTENT_OUTLINE_LOCAL_STORAGE_KEYS, ContentOutline } from './ContentOutline/ContentOutline'; import { ContentOutlineContextProvider } from './ContentOutline/ContentOutlineContext'; import { ContentOutlineItem } from './ContentOutline/ContentOutlineItem'; import { CorrelationHelper } from './CorrelationHelper'; @@ -147,7 +148,7 @@ export class Explore extends PureComponent { constructor(props: Props) { super(props); this.state = { - contentOutlineVisible: false, + contentOutlineVisible: store.getBool(CONTENT_OUTLINE_LOCAL_STORAGE_KEYS.visible, true), }; this.graphEventBus = props.eventBus.newScopedBus('graph', { onlyLocal: false }); this.logsEventBus = props.eventBus.newScopedBus('logs', { onlyLocal: false }); @@ -175,6 +176,7 @@ export class Explore extends PureComponent { }; onContentOutlineToogle = () => { + store.set(CONTENT_OUTLINE_LOCAL_STORAGE_KEYS.visible, !this.state.contentOutlineVisible); this.setState((state) => { reportInteraction('explore_toolbar_contentoutline_clicked', { item: 'outline',