Dashboard: Edit pane UI updates (#102663)

* First pass

* Update

* Outline tweaks

* Update

* Updates

* Minor fixes

* fix

* Fixes

* Fixes

* Collapse add element pane when selecting
This commit is contained in:
Torkel Ödegaard 2025-03-26 12:26:55 +01:00 committed by GitHub
parent 91baf78071
commit 648e68387e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 296 additions and 155 deletions

View File

@ -170,6 +170,7 @@ export const availableIconsIndex = {
kubernetes: true,
'layer-group': true,
'layers-alt': true,
layers: true,
'legend-hide': true,
'legend-show': true,
'library-panel': true,

View File

@ -47,6 +47,7 @@ export class ConditionalRenderingData extends ConditionalRenderingBase<Condition
}
}
}
panelDataProviders.forEach((d) => {
this._subs.add(
d.subscribeToState(() => {

View File

@ -1,8 +1,9 @@
import { css } from '@emotion/css';
import { useEffect, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Box, Card, Icon, IconName, useStyles2 } from '@grafana/ui';
import { Box, Card, Icon, IconButton, IconName, useStyles2 } from '@grafana/ui';
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
import { t } from 'app/core/internationalization';
import store from 'app/core/store';
@ -84,22 +85,35 @@ export function DashboardAddPane({ editPane }: Props) {
];
return (
<Box display="flex" direction="column" gap={1} padding={2}>
{cards.map(({ icon, heading, title, testId, onClick, hide }) =>
hide ? null : (
<Card onClick={onClick} data-testid={testId} title={title} key={title}>
<Card.Heading>{heading}</Card.Heading>
<Card.Figure className={styles.figure}>
<Icon name={icon} size="xl" />
</Card.Figure>
</Card>
)
)}
</Box>
<>
<div className={styles.header}>
<IconButton name="arrow-left" size="lg" onClick={() => editPane.toggleAddPane()} aria-label="Close add pane" />
{t('dashboard.edit-pane.add.title', 'Add element')}
</div>
<Box display="flex" direction="column" gap={1} padding={2}>
{cards.map(({ icon, heading, title, testId, onClick, hide }) =>
hide ? null : (
<Card onClick={onClick} data-testid={testId} title={title} key={title}>
<Card.Heading>{heading}</Card.Heading>
<Card.Figure className={styles.figure}>
<Icon name={icon} size="xl" />
</Card.Figure>
</Card>
)
)}
</Box>
</>
);
}
const getStyles = () => ({
const getStyles = (theme: GrafanaTheme2) => ({
header: css({
display: 'flex',
alignItems: 'center',
padding: theme.spacing(1, 2),
gap: theme.spacing(1),
borderBottom: `1px solid ${theme.colors.border.weak}`,
}),
figure: css({
pointerEvents: 'none',
}),

View File

@ -1,16 +1,19 @@
import { css, cx } from '@emotion/css';
import { Resizable } from 're-resizable';
import { useEffect, useRef } from 'react';
import { useEffect } from 'react';
import { useLocalStorage } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneObjectState, SceneObjectBase, SceneObject, sceneGraph, useSceneObjectState } from '@grafana/scenes';
import {
ElementSelectionContextItem,
ElementSelectionContextState,
Tab,
TabsBar,
ScrollContainer,
ToolbarButton,
useSplitter,
useStyles2,
Text,
Icon,
} from '@grafana/ui';
import { t } from 'app/core/internationalization';
@ -27,7 +30,7 @@ import { useEditableElement } from './useEditableElement';
export interface DashboardEditPaneState extends SceneObjectState {
selection?: ElementSelection;
selectionContext: ElementSelectionContextState;
tab?: EditPaneTab;
showAddPane?: boolean;
}
export type EditPaneTab = 'add' | 'configure' | 'outline';
@ -63,9 +66,7 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
this._subs.add(
dashboard.subscribeToEvent(ObjectsReorderedOnCanvasEvent, ({ payload }) => {
if (this.state.tab === 'outline') {
this.forceRender();
}
this.forceRender();
})
);
}
@ -103,6 +104,10 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
return this.state.selection?.getSelection();
}
public toggleAddPane() {
this.setState({ showAddPane: !this.state.showAddPane });
}
public selectObject(obj: SceneObject, id: string, multi?: boolean) {
const prevItem = this.state.selection?.getFirstObject();
if (prevItem === obj && !multi) {
@ -125,6 +130,7 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
...this.state.selectionContext,
selected,
},
showAddPane: false,
});
}
@ -163,15 +169,11 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
});
}
public onChangeTab = (tab: EditPaneTab) => {
this.setState({ tab });
};
private newObjectAddedToCanvas(obj: SceneObject) {
this.selectObject(obj, obj.state.key!, false);
if (this.state.tab !== 'configure') {
this.onChangeTab('configure');
if (this.state.showAddPane) {
this.setState({ showAddPane: false });
}
}
}
@ -202,11 +204,27 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
}
}, [editPane, isCollapsed]);
const { selection, tab = 'configure' } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true });
const { selection, showAddPane } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true });
const styles = useStyles2(getStyles);
const paneRef = useRef<HTMLDivElement>(null);
const editableElement = useEditableElement(selection, editPane);
const selectedObject = selection?.getFirstObject();
const [outlineCollapsed, setOutlineCollapsed] = useLocalStorage(
'grafana.dashboard.edit-pane.outline.collapsed',
false
);
const [outlinePaneSize = 0.4, setOutlinePaneSize] = useLocalStorage('grafana.dashboard.edit-pane.outline.size', 0.4);
// splitter for template and payload editor
const splitter = useSplitter({
direction: 'column',
handleSize: 'sm',
// if Grafana Alertmanager, split 50/50, otherwise 100/0 because there is no payload editor
initialSize: 1 - outlinePaneSize,
dragPosition: 'middle',
onSizeChanged: (size) => {
setOutlinePaneSize(1 - size);
},
});
if (!editableElement) {
return null;
@ -221,43 +239,67 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
icon="arrow-to-right"
onClick={onToggleCollapse}
variant="canvas"
narrow={true}
className={styles.rotate180}
aria-label={t('dashboard.edit-pane.open', 'Open options pane')}
/>
</div>
{openOverlay && (
<Resizable className={cx(styles.fixed, styles.container)} defaultSize={{ height: '100%', width: '20vw' }}>
<ElementEditPane element={editableElement} key={selectedObject?.state.key} />
<Resizable className={styles.overlayWrapper} defaultSize={{ height: '100%', width: '300px' }}>
<ElementEditPane element={editableElement} key={selectedObject?.state.key} editPane={editPane} />
</Resizable>
)}
</>
);
}
if (outlineCollapsed) {
splitter.primaryProps.style.flexGrow = 1;
splitter.primaryProps.style.minHeight = 'unset';
splitter.secondaryProps.style.flexGrow = 0;
splitter.secondaryProps.style.minHeight = 'min-content';
} else {
splitter.primaryProps.style.minHeight = 'unset';
splitter.secondaryProps.style.minHeight = 'unset';
}
if (showAddPane) {
return (
<div className={styles.wrapper}>
<DashboardAddPane editPane={editPane} />
</div>
);
}
return (
<div className={styles.wrapper} ref={paneRef}>
<TabsBar className={styles.tabsbar}>
<Tab
active={tab === 'add'}
label={t('dashboard.editpane.add', 'Add')}
onChangeTab={() => editPane.onChangeTab('add')}
<div className={styles.wrapper}>
<div {...splitter.containerProps}>
<div {...splitter.primaryProps} className={cx(splitter.primaryProps.className, styles.paneContent)}>
<ElementEditPane element={editableElement} key={selectedObject?.state.key} editPane={editPane} />
</div>
<div
{...splitter.splitterProps}
className={cx(splitter.splitterProps.className, styles.splitter)}
data-edit-pane-splitter={true}
/>
<Tab
active={tab === 'configure'}
label={t('dashboard.editpane.configure', 'Configure')}
onChangeTab={() => editPane.onChangeTab('configure')}
/>
<Tab
active={tab === 'outline'}
label={t('dashboard.editpane.outline', 'Outline')}
onChangeTab={() => editPane.onChangeTab('outline')}
/>
</TabsBar>
<div className={styles.tabContent}>
{tab === 'add' && <DashboardAddPane editPane={editPane} />}
{tab === 'configure' && <ElementEditPane element={editableElement} key={selectedObject?.state.key} />}
{tab === 'outline' && <DashboardOutline editPane={editPane} />}
<div {...splitter.secondaryProps} className={cx(splitter.primaryProps.className, styles.paneContent)}>
<div
role="button"
onClick={() => setOutlineCollapsed(!outlineCollapsed)}
className={styles.outlineCollapseButton}
>
<Text weight="medium">Outline</Text>
<Icon name="angle-up" />
</div>
{!outlineCollapsed && (
<div className={styles.outlineContainer}>
<ScrollContainer showScrollIndicators={true}>
<DashboardOutline editPane={editPane} />
</ScrollContainer>
</div>
)}
</div>
</div>
</div>
);
@ -269,13 +311,27 @@ function getStyles(theme: GrafanaTheme2) {
display: 'flex',
flexDirection: 'column',
flex: '1 1 0',
marginTop: theme.spacing(2),
borderLeft: `1px solid ${theme.colors.border.weak}`,
borderTop: `1px solid ${theme.colors.border.weak}`,
background: theme.colors.background.primary,
}),
tabContent: css({
overlayWrapper: css({
right: 0,
bottom: 0,
top: theme.spacing(2),
position: 'absolute !important' as 'absolute',
background: theme.colors.background.primary,
borderLeft: `1px solid ${theme.colors.border.weak}`,
borderTop: `1px solid ${theme.colors.border.weak}`,
boxShadow: theme.shadows.z3,
zIndex: theme.zIndex.navbarFixed,
flexGrow: 1,
}),
paneContent: css({
overflow: 'hidden',
display: 'flex',
flex: '1 1 0',
flexDirection: 'column',
minHeight: 0,
overflow: 'auto',
}),
rotate180: css({
rotate: '180deg',
@ -287,20 +343,30 @@ function getStyles(theme: GrafanaTheme2) {
expandOptionsWrapper: css({
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(2, 1),
padding: theme.spacing(2, 1, 2, 0),
}),
// @ts-expect-error csstype doesn't allow !important. see https://github.com/frenic/csstype/issues/114
fixed: css({
position: 'absolute !important',
splitter: css({
'&:after': {
display: 'none',
},
}),
container: css({
right: 0,
background: theme.colors.background.primary,
borderLeft: `1px solid ${theme.colors.border.weak}`,
boxShadow: theme.shadows.z3,
zIndex: theme.zIndex.navbarFixed,
overflowX: 'hidden',
overflowY: 'scroll',
outlineCollapseButton: css({
display: 'flex',
padding: theme.spacing(0.5, 2),
gap: theme.spacing(1),
justifyContent: 'space-between',
alignItems: 'center',
background: theme.colors.background.secondary,
'&:hover': {
background: theme.colors.action.hover,
},
}),
outlineContainer: css({
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
overflow: 'hidden',
}),
};
}

View File

@ -48,7 +48,7 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
collapsed: isCollapsed,
paneOptions: {
collapseBelowPixels: 250,
collapseBelowPixels: 150,
snapOpenToPixels: 400,
},
});
@ -97,7 +97,11 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
</div>
{isEditing && (
<>
<div {...splitterProps} data-edit-pane-splitter={true} />
<div
{...splitterProps}
className={cx(splitterProps.className, styles.splitter)}
data-edit-pane-splitter={true}
/>
<div {...secondaryProps} className={cx(secondaryProps.className, styles.editPane)}>
<DashboardEditPaneRenderer
editPane={editPane}
@ -156,11 +160,18 @@ function getStyles(theme: GrafanaTheme2, headerHeight: number) {
scrollbarWidth: 'thin',
// The fixed controls headers is otherwise rendered over the selection outlinem, Maybe there is an other solution
paddingTop: '2px',
// Because the edit pane splitter handle area adds padding we can reduce it here
paddingRight: theme.spacing(1),
}),
editPane: css({
flexDirection: 'column',
borderLeft: `1px solid ${theme.colors.border.weak}`,
background: theme.colors.background.primary,
// borderLeft: `1px solid ${theme.colors.border.weak}`,
// background: theme.colors.background.primary,
}),
splitter: css({
'&:after': {
display: 'none',
},
}),
controlsWrapperSticky: css({
[theme.breakpoints.up('md')]: {

View File

@ -18,7 +18,7 @@ export class DashboardEditableElement implements EditableDashboardElement {
return {
typeName: t('dashboard.edit-pane.elements.dashboard', 'Dashboard'),
icon: 'apps',
instanceName: this.dashboard.state.title,
instanceName: t('dashboard.edit-pane.elements.dashboard', 'Dashboard'),
};
}
@ -55,6 +55,7 @@ export class DashboardEditableElement implements EditableDashboardElement {
return (
<Button
variant="secondary"
size="sm"
onClick={() => this.dashboard.onOpenSettings()}
tooltip={t('dashboard.toolbar.dashboard-settings.tooltip', 'Dashboard settings')}
>

View File

@ -3,8 +3,8 @@ import { useMemo, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneObject, VizPanel } from '@grafana/scenes';
import { Box, Icon, IconButton, Stack, Text, useElementSelection, useStyles2 } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { Box, Icon, Text, useElementSelection, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { isInCloneChain } from '../utils/clone';
@ -21,7 +21,7 @@ export function DashboardOutline({ editPane }: Props) {
const dashboard = getDashboardSceneFor(editPane);
return (
<Box padding={1} gap={0.5} display="flex" direction="column">
<Box padding={1} gap={0.25} display="flex" direction="column">
<DashboardOutlineNode sceneObject={dashboard} expandable />
</Box>
);
@ -40,34 +40,19 @@ function DashboardOutlineNode({ sceneObject, expandable }: { sceneObject: SceneO
return (
<>
<Stack
direction="row"
gap={0.5}
alignItems="center"
role="presentation"
aria-expanded={expandable ? isExpanded : undefined}
aria-owns={expandable ? key : undefined}
<button
role="treeitem"
className={cx(styles.nodeButton, isCloned && styles.nodeButtonClone, isSelected && styles.nodeButtonSelected)}
onPointerDown={(evt) => {
onSelect?.(evt);
setIsExpanded(!isExpanded);
}}
>
{expandable && (
<IconButton
name={isExpanded ? 'angle-down' : 'angle-right'}
onClick={() => setIsExpanded(!isExpanded)}
aria-label={
isExpanded
? t('dashboard.outline.tree.item.collapse', 'Collapse item')
: t('dashboard.outline.tree.item.expand', 'Expand item')
}
/>
)}
<button
role="treeitem"
className={cx(styles.nodeButton, isCloned && styles.nodeButtonClone, isSelected && styles.nodeButtonSelected)}
onPointerDown={(evt) => onSelect?.(evt)}
>
<Icon name={elementInfo.icon} />
<span>{elementInfo.instanceName}</span>
</button>
</Stack>
{expandable && <Icon name={isExpanded ? 'angle-down' : 'angle-right'} />}
<Icon size="sm" name={elementInfo.icon} />
<span>{elementInfo.instanceName}</span>
</button>
{expandable && isExpanded && (
<div className={styles.container} role="group">
{children.length > 0 ? (
@ -94,7 +79,7 @@ function getStyles(theme: GrafanaTheme2) {
container: css({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(1),
gap: theme.spacing(0.5),
marginLeft: theme.spacing(1),
paddingLeft: theme.spacing(1.5),
borderLeft: `1px solid ${theme.colors.border.medium}`,
@ -105,12 +90,16 @@ function getStyles(theme: GrafanaTheme2) {
background: 'transparent',
padding: theme.spacing(0.25, 1),
borderRadius: theme.shape.radius.default,
color: theme.colors.text.secondary,
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
gap: theme.spacing(0.5),
overflow: 'hidden',
'&:hover': {
backgroundColor: theme.colors.action.hover,
color: theme.colors.text.primary,
outline: `1px dashed ${theme.colors.border.strong}`,
outlineOffset: '0px',
backgroundColor: theme.colors.emphasize(theme.colors.background.canvas, 0.08),
},
'> span': {
whiteSpace: 'nowrap',
@ -119,7 +108,12 @@ function getStyles(theme: GrafanaTheme2) {
},
}),
nodeButtonSelected: css({
color: theme.colors.primary.text,
color: theme.colors.text.primary,
outline: `1px dashed ${theme.colors.primary.border}`,
outlineOffset: '0px',
'&:hover': {
outline: `1px dashed ${theme.colors.primary.border}`,
},
}),
nodeButtonClone: css({
color: theme.colors.text.secondary,

View File

@ -1,26 +1,43 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, Menu, Stack, Text, useStyles2, ConfirmButton, Dropdown, Icon } from '@grafana/ui';
import { Button, Menu, Stack, Text, useStyles2, ConfirmButton, Dropdown, Icon, IconButton } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { EditableDashboardElement } from '../scene/types/EditableDashboardElement';
import { DashboardEditPane } from './DashboardEditPane';
interface EditPaneHeaderProps {
element: EditableDashboardElement;
editPane: DashboardEditPane;
}
export function EditPaneHeader({ element }: EditPaneHeaderProps) {
export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) {
const elementInfo = element.getEditableElementInfo();
const styles = useStyles2(getStyles);
const onCopy = element.onCopy?.bind(element);
const onDuplicate = element.onDuplicate?.bind(element);
const onDelete = element.onDelete?.bind(element);
// temporary simple solution, should select parent element
const onGoBack = () => editPane.clearSelection();
const canGoBack = editPane.state.selection;
return (
<div className={styles.wrapper}>
<Text variant="h5">{elementInfo.typeName}</Text>
<Stack direction="row" gap={1}>
{canGoBack && (
<IconButton
name="arrow-left"
size="lg"
onClick={onGoBack}
tooltip={t('grafana.dashboard.edit-pane.go-back', 'Go back')}
aria-abel={t('grafana.dashboard.edit-pane.go-back', 'Go back')}
/>
)}
<Text>{elementInfo.typeName}</Text>
</Stack>
<Stack direction="row" gap={1}>
{element.renderActions && element.renderActions()}
{(onCopy || onDelete) && (
@ -80,7 +97,7 @@ function getStyles(theme: GrafanaTheme2) {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: theme.spacing(2),
padding: theme.spacing(1, 2),
borderBottom: `1px solid ${theme.colors.border.weak}`,
}),
};

View File

@ -1,20 +1,37 @@
import { Stack } from '@grafana/ui';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { ScrollContainer, useStyles2 } from '@grafana/ui';
import { EditableDashboardElement } from '../scene/types/EditableDashboardElement';
import { DashboardEditPane } from './DashboardEditPane';
import { EditPaneHeader } from './EditPaneHeader';
export interface Props {
element: EditableDashboardElement;
editPane: DashboardEditPane;
}
export function ElementEditPane({ element }: Props) {
export function ElementEditPane({ element, editPane }: Props) {
const categories = element.useEditPaneOptions ? element.useEditPaneOptions() : [];
const styles = useStyles2(getStyles);
return (
<Stack direction="column" gap={0}>
<EditPaneHeader element={element} />
{categories.map((cat) => cat.render())}
</Stack>
<div className={styles.wrapper}>
<EditPaneHeader element={element} editPane={editPane} />
<ScrollContainer showScrollIndicators={true}>{categories.map((cat) => cat.render())}</ScrollContainer>
</div>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
wrapper: css({
display: 'flex',
flexDirection: 'column',
flex: '1 1 0',
height: '100%',
}),
};
}

View File

@ -65,7 +65,7 @@ export class RowItem
return {
typeName: t('dashboard.edit-pane.elements.row', 'Row'),
instanceName: sceneGraph.interpolate(this, this.state.title, undefined, 'text'),
icon: 'line-alt',
icon: 'list-ul',
};
}

View File

@ -46,7 +46,7 @@ export class TabItem
return {
typeName: t('dashboard.edit-pane.elements.tab', 'Tab'),
instanceName: sceneGraph.interpolate(this, this.state.title, undefined, 'text'),
icon: 'tag-alt',
icon: 'layers',
};
}

View File

@ -8,6 +8,7 @@ import { isLibraryPanel } from '../../utils/utils';
import { DashboardScene } from '../DashboardScene';
import { BackToDashboardButton } from './actions/BackToDashboardButton';
import { DashboardAddButton } from './actions/DashboardAddButton';
import { DashboardSettingsButton } from './actions/DashboardSettingsButton';
import { DiscardLibraryPanelButton } from './actions/DiscardLibraryPanelButton';
import { DiscardPanelButton } from './actions/DiscardPanelButton';
@ -98,6 +99,12 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => {
group: 'panel',
condition: showPanelButtons && isEditingLibraryPanel,
},
{
key: 'dashboard-add-button',
component: DashboardAddButton,
group: 'dashboard',
condition: isEditingAndShowingDashboard && dashboard.canEditDashboard(),
},
{
key: 'edit-schema-v2-button',
component: EditSchemaV2Button,

View File

@ -0,0 +1,21 @@
import { selectors } from '@grafana/e2e-selectors';
import { Button } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { ToolbarActionProps } from '../types';
export function DashboardAddButton({ dashboard }: ToolbarActionProps) {
return (
<Button
tooltip={t('dashboard.toolbar.add-new.tooltip', 'Add panels and other elements')}
icon="plus"
onClick={() => dashboard.state.editPane.toggleAddPane()}
variant="primary"
size="sm"
fill="outline"
data-testid={selectors.components.PageToolbar.itemButton('Add button')}
>
<Trans i18nKey="dashboard.toolbar.add">Add</Trans>
</Button>
);
}

View File

@ -16,7 +16,7 @@ export interface OptionsPaneCategoryProps {
renderTitle?: (isExpanded: boolean) => React.ReactNode;
isOpenDefault?: boolean;
itemsCount?: number;
forceOpen?: number;
forceOpen?: boolean;
className?: string;
isNested?: boolean;
children: ReactNode;
@ -42,37 +42,24 @@ export const OptionsPaneCategory = React.memo(
isExpanded: isOpenDefault,
});
const [isExpanded, setIsExpanded] = useState(savedState?.isExpanded ?? isOpenDefault);
const manualClickTime = useRef(0);
const isExpandedInitialValue = forceOpen || (savedState?.isExpanded ?? isOpenDefault);
const [isExpanded, setIsExpanded] = useState(isExpandedInitialValue);
const ref = useRef<HTMLDivElement>(null);
const [queryParams, updateQueryParams] = useQueryParams();
const isOpenFromUrl = queryParams[CATEGORY_PARAM_NAME] === id;
// Handle opening by forceOpen param or from URL
useEffect(() => {
if (manualClickTime.current) {
// ignore changes since the click handled the expected behavior
if (Date.now() - manualClickTime.current < 200) {
return;
}
}
if (isOpenFromUrl || forceOpen) {
if (!isExpanded) {
setIsExpanded(true);
}
if (isOpenFromUrl) {
if ((forceOpen || isOpenFromUrl) && !isExpanded) {
setIsExpanded(true);
setTimeout(() => {
ref.current?.scrollIntoView();
}
}, 200);
}
}, [forceOpen, isExpanded, isOpenFromUrl]);
}, [isExpanded, isOpenFromUrl, forceOpen]);
const onToggle = useCallback(() => {
manualClickTime.current = Date.now();
updateQueryParams(
{
[CATEGORY_PARAM_NAME]: isExpanded ? undefined : id,
},
true
);
updateQueryParams({ [CATEGORY_PARAM_NAME]: isExpanded ? undefined : id }, true);
setSavedState({ isExpanded: !isExpanded });
setIsExpanded(!isExpanded);
}, [updateQueryParams, isExpanded, id, setSavedState]);

View File

@ -10,7 +10,7 @@ export interface OptionsPaneCategoryDescriptorProps {
title: string;
renderTitle?: (isExpanded: boolean) => React.ReactNode;
isOpenDefault?: boolean;
forceOpen?: number;
forceOpen?: boolean;
className?: string;
isNested?: boolean;
itemsCount?: number;

View File

@ -96,7 +96,7 @@ export const OptionsPaneOptions = (props: OptionPaneRenderProps) => {
break;
case OptionFilter.Recent:
mainBoxElements.push(
<OptionsPaneCategory id="Recent options" title="Recent options" key="Recent options" forceOpen={1}>
<OptionsPaneCategory id="Recent options" title="Recent options" key="Recent options" forceOpen={true}>
{getRecentOptions(allOptions).map((item) => item.render())}
</OptionsPaneCategory>
);
@ -163,7 +163,7 @@ export function renderSearchHits(
id="Found options"
title={`Matched ${optionHits.length}/${totalCount} options`}
key="Normal options"
forceOpen={1}
forceOpen={true}
>
{optionHits.map((hit) => hit.render(searchQuery))}
</OptionsPaneCategory>

View File

@ -85,7 +85,7 @@ export function getFieldOverrideCategories(
const configPropertiesOptions = getOverrideProperties(registry);
const isSystemOverride = isSystemOverrideGuard(override);
// A way to force open new override categories
const forceOpen = override.properties.length === 0 ? 1 : 0;
const forceOpen = override.properties.length === 0;
const category = new OptionsPaneCategoryDescriptor({
title: overrideName,

View File

@ -1395,7 +1395,8 @@
"tab": {
"heading": "Tab",
"title": "Break up your dashboard into different horizontal tabs"
}
},
"title": "Add element"
},
"elements": {
"dashboard": "Dashboard",
@ -1415,11 +1416,6 @@
}
}
},
"editpane": {
"add": "Add",
"configure": "Configure",
"outline": "Outline"
},
"empty": {
"add-library-panel-body": "Add visualizations that are shared with other dashboards.",
"add-library-panel-button": "Add library panel",
@ -1503,9 +1499,7 @@
"outline": {
"tree": {
"item": {
"collapse": "Collapse item",
"empty": "(empty)",
"expand": "Expand item"
"empty": "(empty)"
}
}
},
@ -1607,6 +1601,9 @@
},
"toolbar": {
"add": "Add",
"add-new": {
"tooltip": "Add panels and other elements"
},
"alert-rules": "Alert rules",
"back-to-dashboard": "Back to dashboard",
"dashboard-settings": {
@ -2409,6 +2406,13 @@
"requires-license": "Requires a Grafana Enterprise license",
"title": "Enterprise"
},
"grafana": {
"dashboard": {
"edit-pane": {
"go-back": "Go back"
}
}
},
"grafana-ui": {
"action-editor": {
"button": {