Make content outline visible and in expanded mode by default (#90283)

* Make content outline visible and in expanded mode by default

* Clean up unused args

* Save content outline visibility in local storage

* Add test

* Expanded state relies on local storage;
This commit is contained in:
Haris Rozajac 2024-07-16 07:15:30 -06:00 committed by GitHub
parent 6ff21726b7
commit 51afb2e484
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 52 additions and 17 deletions

View File

@ -1,6 +1,8 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { store } from '@grafana/data';
import { ContentOutline } from './ContentOutline'; import { ContentOutline } from './ContentOutline';
jest.mock('./ContentOutlineContext', () => ({ jest.mock('./ContentOutlineContext', () => ({
@ -73,15 +75,15 @@ const setup = (mergeSingleChild = false) => {
describe('<ContentOutline />', () => { describe('<ContentOutline />', () => {
it('toggles content on button click', async () => { it('toggles content on button click', async () => {
setup(); setup();
let showContentOutlineButton = screen.getByRole('button', { name: 'Expand outline' }); let showContentOutlineButton = screen.getByRole('button', { name: 'Collapse outline' });
expect(showContentOutlineButton).toBeInTheDocument(); expect(showContentOutlineButton).toBeInTheDocument();
await userEvent.click(showContentOutlineButton); await userEvent.click(showContentOutlineButton);
const hideContentOutlineButton = screen.getByRole('button', { name: 'Collapse outline' }); const hideContentOutlineButton = screen.getByRole('button', { name: 'Expand outline' });
expect(hideContentOutlineButton).toBeInTheDocument(); expect(hideContentOutlineButton).toBeInTheDocument();
await userEvent.click(hideContentOutlineButton); await userEvent.click(hideContentOutlineButton);
showContentOutlineButton = screen.getByRole('button', { name: 'Expand outline' }); showContentOutlineButton = screen.getByRole('button', { name: 'Collapse outline' });
expect(showContentOutlineButton).toBeInTheDocument(); expect(showContentOutlineButton).toBeInTheDocument();
}); });
@ -157,4 +159,15 @@ describe('<ContentOutline />', () => {
expect(unregisterMock).toHaveBeenCalledWith('item-2-1'); 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();
});
}); });

View File

@ -2,12 +2,11 @@ import { css, cx } from '@emotion/css';
import { Fragment, useEffect, useRef, useState } from 'react'; import { Fragment, useEffect, useRef, useState } from 'react';
import { useToggle, useScroll } from 'react-use'; import { useToggle, useScroll } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2, store } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime'; import { reportInteraction } from '@grafana/runtime';
import { useStyles2, PanelContainer, CustomScrollbar } from '@grafana/ui'; import { useStyles2, PanelContainer, CustomScrollbar } from '@grafana/ui';
import { ContentOutlineItemContextProps, useContentOutlineContext } from './ContentOutlineContext'; import { ContentOutlineItemContextProps, useContentOutlineContext } from './ContentOutlineContext';
import { ITEM_TYPES } from './ContentOutlineItem';
import { ContentOutlineItemButton } from './ContentOutlineItemButton'; import { ContentOutlineItemButton } from './ContentOutlineItemButton';
function scrollableChildren(item: ContentOutlineItemContextProps) { 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 }) { 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 styles = useStyles2(getStyles, contentOutlineExpanded);
const scrollerRef = useRef(scroller || null); const scrollerRef = useRef(scroller || null);
const { y: verticalScroll } = useScroll(scrollerRef); const { y: verticalScroll } = useScroll(scrollerRef);
@ -57,12 +63,7 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
}, {}); }, {});
}); });
const scrollIntoView = ( const scrollIntoView = (ref: HTMLElement | null, customOffsetTop = 0) => {
ref: HTMLElement | null,
itemPanelId: string,
itemType: ITEM_TYPES | undefined,
customOffsetTop = 0
) => {
let scrollValue = 0; let scrollValue = 0;
let el: HTMLElement | null | undefined = ref; let el: HTMLElement | null | undefined = ref;
@ -88,10 +89,10 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
}); });
if (activeParent) { if (activeParent) {
scrollIntoView(activeParent.ref, activeParent.panelId, activeParent.type, activeParent.customTopOffset); scrollIntoView(activeParent.ref, activeParent.customTopOffset);
} }
} else { } else {
scrollIntoView(item.ref, item.panelId, item.type, item.customTopOffset); scrollIntoView(item.ref, item.customTopOffset);
reportInteraction('explore_toolbar_contentoutline_clicked', { reportInteraction('explore_toolbar_contentoutline_clicked', {
item: 'select_section', item: 'select_section',
type: item.panelId, type: item.panelId,
@ -100,6 +101,7 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
}; };
const toggle = () => { const toggle = () => {
store.set(CONTENT_OUTLINE_LOCAL_STORAGE_KEYS.expanded, !contentOutlineExpanded);
toggleContentOutlineExpanded(); toggleContentOutlineExpanded();
reportInteraction('explore_toolbar_contentoutline_clicked', { reportInteraction('explore_toolbar_contentoutline_clicked', {
item: 'outline', item: 'outline',

View File

@ -2,7 +2,15 @@ import { render, screen } from '@testing-library/react';
import { Props as AutoSizerProps } from 'react-virtualized-auto-sizer'; import { Props as AutoSizerProps } from 'react-virtualized-auto-sizer';
import { TestProvider } from 'test/helpers/TestProvider'; 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 { selectors } from '@grafana/e2e-selectors';
import { usePluginLinkExtensions } from '@grafana/runtime'; import { usePluginLinkExtensions } from '@grafana/runtime';
import { configureStore } from 'app/store/configureStore'; import { configureStore } from 'app/store/configureStore';
@ -225,4 +233,14 @@ describe('Explore', () => {
expect(dataSourcePicker).toBeInTheDocument(); 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();
});
});
}); });

View File

@ -14,6 +14,7 @@ import {
QueryFixAction, QueryFixAction,
RawTimeRange, RawTimeRange,
SplitOpenOptions, SplitOpenOptions,
store,
SupplementaryQueryType, SupplementaryQueryType,
} from '@grafana/data'; } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
@ -34,7 +35,7 @@ import { StoreState } from 'app/types';
import { getTimeZone } from '../profile/state/selectors'; 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 { ContentOutlineContextProvider } from './ContentOutline/ContentOutlineContext';
import { ContentOutlineItem } from './ContentOutline/ContentOutlineItem'; import { ContentOutlineItem } from './ContentOutline/ContentOutlineItem';
import { CorrelationHelper } from './CorrelationHelper'; import { CorrelationHelper } from './CorrelationHelper';
@ -147,7 +148,7 @@ export class Explore extends PureComponent<Props, ExploreState> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
contentOutlineVisible: false, contentOutlineVisible: store.getBool(CONTENT_OUTLINE_LOCAL_STORAGE_KEYS.visible, true),
}; };
this.graphEventBus = props.eventBus.newScopedBus('graph', { onlyLocal: false }); this.graphEventBus = props.eventBus.newScopedBus('graph', { onlyLocal: false });
this.logsEventBus = props.eventBus.newScopedBus('logs', { onlyLocal: false }); this.logsEventBus = props.eventBus.newScopedBus('logs', { onlyLocal: false });
@ -175,6 +176,7 @@ export class Explore extends PureComponent<Props, ExploreState> {
}; };
onContentOutlineToogle = () => { onContentOutlineToogle = () => {
store.set(CONTENT_OUTLINE_LOCAL_STORAGE_KEYS.visible, !this.state.contentOutlineVisible);
this.setState((state) => { this.setState((state) => {
reportInteraction('explore_toolbar_contentoutline_clicked', { reportInteraction('explore_toolbar_contentoutline_clicked', {
item: 'outline', item: 'outline',