Variables: Style tweaks to new variables menu (#110946)

This commit is contained in:
Torkel Ödegaard 2025-09-11 14:51:53 +02:00 committed by GitHub
parent d73308690c
commit 18673e6eef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 158 additions and 39 deletions

View File

@ -20,7 +20,7 @@ import { Box, Stack, useStyles2 } from '@grafana/ui';
import { PanelEditControls } from '../panel-edit/PanelEditControls';
import { getDashboardSceneFor } from '../utils/utils';
import { DashboardControlsMenu } from './DashboardControlsMenu';
import { DashboardControlsButton } from './DashboardControlsMenu';
import { DashboardLinksControls } from './DashboardLinksControls';
import { DashboardScene } from './DashboardScene';
import { VariableControls } from './VariableControls';
@ -159,7 +159,7 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
)}
{(hasControlMenuVariables || hasControlMenuLinks) && (
<Stack>
<DashboardControlsMenu dashboard={dashboard} />
<DashboardControlsButton dashboard={dashboard} />
</Stack>
)}
{showDebugger && <SceneDebugger scene={model} key={'scene-debugger'} />}

View File

@ -6,13 +6,13 @@ import { SceneVariableSet, TextBoxVariable, QueryVariable, CustomVariable, Scene
import {
DASHBOARD_CONTROLS_MENU_ARIA_LABEL,
DASHBOARD_CONTROLS_MENU_TITLE,
DashboardControlsMenu,
DashboardControlsButton,
} from './DashboardControlsMenu';
import { DashboardScene } from './DashboardScene';
describe('DashboardControlsMenu', () => {
it('should return null and not render anything when there are no variables', () => {
const { container } = render(<DashboardControlsMenu dashboard={getDashboard([])} />);
const { container } = render(<DashboardControlsButton dashboard={getDashboard([])} />);
expect(container.firstChild).toBeNull();
});
@ -24,7 +24,7 @@ describe('DashboardControlsMenu', () => {
showInControlsMenu: false,
}),
];
const { container } = render(<DashboardControlsMenu dashboard={getDashboard(variables)} />);
const { container } = render(<DashboardControlsButton dashboard={getDashboard(variables)} />);
expect(container.firstChild).toBeNull();
});
@ -37,7 +37,7 @@ describe('DashboardControlsMenu', () => {
}),
];
render(<DashboardControlsMenu dashboard={getDashboard(variables)} />);
render(<DashboardControlsButton dashboard={getDashboard(variables)} />);
// Should render the toolbar button
const button = screen.getByRole('button');
@ -66,7 +66,7 @@ describe('DashboardControlsMenu', () => {
];
act(() => {
render(<DashboardControlsMenu dashboard={getDashboard(variables)} />);
render(<DashboardControlsButton dashboard={getDashboard(variables)} />);
});
// Should have rendered a dropdown
@ -98,7 +98,7 @@ describe('DashboardControlsMenu', () => {
}),
];
render(<DashboardControlsMenu dashboard={getDashboard(variables)} />);
render(<DashboardControlsButton dashboard={getDashboard(variables)} />);
// Should still render dropdown since we have variables with showInControlsMenu=true
expect(screen.getByRole('button')).toBeInTheDocument();

View File

@ -2,8 +2,9 @@ import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { sceneGraph } from '@grafana/scenes';
import { Dropdown, Menu, ToolbarButton, useStyles2 } from '@grafana/ui';
import { sceneGraph, SceneVariable } from '@grafana/scenes';
import { DashboardLink } from '@grafana/schema';
import { Box, Dropdown, ToolbarButton, useStyles2 } from '@grafana/ui';
import { DashboardLinkRenderer } from './DashboardLinkRenderer';
import { DashboardScene } from './DashboardScene';
@ -12,8 +13,7 @@ import { VariableValueSelectWrapper } from './VariableControls';
export const DASHBOARD_CONTROLS_MENU_ARIA_LABEL = 'Dashboard controls menu';
export const DASHBOARD_CONTROLS_MENU_TITLE = 'Dashboard controls';
export function DashboardControlsMenu({ dashboard }: { dashboard: DashboardScene }) {
const styles = useStyles2(getStyles);
export function DashboardControlsButton({ dashboard }: { dashboard: DashboardScene }) {
const { links, uid } = dashboard.useState();
const filteredLinks = links.filter((link) => link.placement === 'inControlsMenu');
const variables = sceneGraph
@ -27,29 +27,8 @@ export function DashboardControlsMenu({ dashboard }: { dashboard: DashboardScene
return (
<Dropdown
overlay={
<Menu
onClick={(e) => {
e.stopPropagation();
}}
>
{/* Variables */}
{variables.map((variable) => (
<div className={styles.menuItem} key={variable.state.key}>
<VariableValueSelectWrapper variable={variable} />
</div>
))}
{variables.length > 0 && filteredLinks.length > 0 && <Menu.Divider />}
{/* Links */}
{filteredLinks.map((link, index) => (
<div className={styles.menuItem} key={`${link.title}-$${index}`}>
<DashboardLinkRenderer link={link} dashboardUID={uid} />
</div>
))}
</Menu>
}
placement="bottom-end"
overlay={<DashboardControlsMenu variables={variables} links={filteredLinks} dashboardUID={uid} />}
>
<ToolbarButton
aria-label={t('dashboard.controls.menu.aria-label', DASHBOARD_CONTROLS_MENU_ARIA_LABEL)}
@ -57,11 +36,54 @@ export function DashboardControlsMenu({ dashboard }: { dashboard: DashboardScene
icon="ellipsis-v"
iconSize="md"
narrow
variant="canvas"
/>
</Dropdown>
);
}
interface VariablesMenuProps {
variables: SceneVariable[];
links: DashboardLink[];
dashboardUID: string;
}
function DashboardControlsMenu({ variables, links, dashboardUID }: VariablesMenuProps) {
const styles = useStyles2(getStyles);
return (
<Box
minWidth={32}
borderColor={'weak'}
borderStyle={'solid'}
boxShadow={'z3'}
display={'flex'}
direction={'column'}
borderRadius={'default'}
backgroundColor={'primary'}
padding={1}
gap={0.5}
onClick={(e) => {
e.stopPropagation();
}}
>
{/* Variables */}
{variables.map((variable) => (
<div className={styles.menuItem} key={variable.state.key}>
<VariableValueSelectWrapper variable={variable} inMenu />
</div>
))}
{/* Links */}
{links.map((link, index) => (
<div className={styles.menuItem} key={`${link.title}-$${index}`}>
<DashboardLinkRenderer link={link} dashboardUID={dashboardUID} />
</div>
))}
</Box>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
menuItem: css({
padding: theme.spacing(0.5),

View File

@ -0,0 +1,68 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { sceneGraph } from '@grafana/scenes';
import { Box, Dropdown, ToolbarButton, useStyles2 } from '@grafana/ui';
import { DashboardScene } from './DashboardScene';
import { VariableValueSelectWrapper } from './VariableControls';
export const DROPDOWN_CONTROLS_ARIA_LABEL = 'Dashboard controls menu';
export const DROPDOWN_CONTROLS_TITLE = 'Dashboard controls';
export function DropdownVariableControls({ dashboard }: { dashboard: DashboardScene }) {
const styles = useStyles2(getStyles);
const variables = sceneGraph
.getVariables(dashboard)!
.useState()
.variables.filter((v) => v.state.showInControlsMenu !== true);
if (variables.length === 0) {
return null;
}
return (
<Dropdown
placement="bottom-end"
overlay={
<Box
minWidth={32}
borderColor={'weak'}
borderStyle={'solid'}
boxShadow={'z3'}
display={'flex'}
direction={'column'}
borderRadius={'default'}
backgroundColor={'primary'}
padding={1}
gap={0.5}
onClick={(e) => {
e.stopPropagation();
}}
>
{variables.map((variable) => (
<div className={styles.menuItem} key={variable.state.key}>
<VariableValueSelectWrapper variable={variable} inMenu />
</div>
))}
</Box>
}
>
<ToolbarButton
aria-label={t('dashboard.controls.menu.aria-label', DROPDOWN_CONTROLS_ARIA_LABEL)}
title={t('dashboard.controls.menu.title', DROPDOWN_CONTROLS_TITLE)}
icon="ellipsis-v"
iconSize="md"
narrow
variant="canvas"
/>
</Dropdown>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
menuItem: css({
padding: theme.spacing(0.5),
}),
});

View File

@ -2,7 +2,14 @@ import { css, cx } from '@emotion/css';
import { VariableHide, GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { sceneGraph, useSceneObjectState, SceneVariable, SceneVariableState, ControlsLabel } from '@grafana/scenes';
import {
sceneGraph,
useSceneObjectState,
SceneVariable,
SceneVariableState,
ControlsLabel,
ControlsLayout,
} from '@grafana/scenes';
import { useElementSelection, useStyles2 } from '@grafana/ui';
import { DashboardScene } from './DashboardScene';
@ -23,9 +30,10 @@ export function VariableControls({ dashboard }: { dashboard: DashboardScene }) {
interface VariableSelectProps {
variable: SceneVariable;
inMenu?: boolean;
}
export function VariableValueSelectWrapper({ variable }: VariableSelectProps) {
export function VariableValueSelectWrapper({ variable, inMenu }: VariableSelectProps) {
const state = useSceneObjectState<SceneVariableState>(variable, { shouldActivateOrKeepAlive: true });
const { isSelected, onSelect, isSelectable } = useElementSelection(variable.state.key);
const styles = useStyles2(getStyles);
@ -56,6 +64,15 @@ export function VariableValueSelectWrapper({ variable }: VariableSelectProps) {
}
};
if (inMenu) {
return (
<div className={styles.verticalContainer} data-testid={selectors.pages.Dashboard.SubMenu.submenuItem}>
<VariableLabel variable={variable} layout={'vertical'} />
<variable.Component model={variable} />
</div>
);
}
return (
<div
className={cx(
@ -72,7 +89,15 @@ export function VariableValueSelectWrapper({ variable }: VariableSelectProps) {
);
}
function VariableLabel({ variable, className }: { variable: SceneVariable; className?: string }) {
function VariableLabel({
variable,
className,
layout,
}: {
variable: SceneVariable;
className?: string;
layout?: ControlsLayout;
}) {
const { state } = variable;
if (variable.state.hide === VariableHide.hideLabel) {
@ -89,7 +114,7 @@ function VariableLabel({ variable, className }: { variable: SceneVariable; class
onCancel={() => variable.onCancel?.()}
label={labelOrName}
error={state.error}
layout={'horizontal'}
layout={layout ?? 'horizontal'}
description={state.description ?? undefined}
className={className}
/>
@ -105,6 +130,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
borderBottomLeftRadius: 'unset',
}),
}),
verticalContainer: css({
display: 'flex',
flexDirection: 'column',
}),
labelWrapper: css({
display: 'flex',
alignItems: 'center',