Dashboards: Make it possible to render variables under a drop-down (#109225)

* feat: extend the variable models

* test(DropDownVariableControls): add tests

* refactor(VariableControls): filter in the render method
This commit is contained in:
Levente Balogh
2025-08-29 14:56:26 +02:00
committed by GitHub
parent 72eeefabd7
commit a746f6e121
15 changed files with 395 additions and 74 deletions

View File

@ -719,6 +719,7 @@ QueryVariableSpec: {
refresh: VariableRefresh refresh: VariableRefresh
skipUrlSync: bool | *false skipUrlSync: bool | *false
description?: string description?: string
showInControlsMenu?: bool
query: DataQueryKind query: DataQueryKind
regex: string | *"" regex: string | *""
sort: VariableSort sort: VariableSort
@ -731,6 +732,7 @@ QueryVariableSpec: {
allowCustomValue: bool | *true allowCustomValue: bool | *true
staticOptions?: [...VariableOption] staticOptions?: [...VariableOption]
staticOptionsOrder?: "before" | "after" | "sorted" staticOptionsOrder?: "before" | "after" | "sorted"
showInControlsMenu?: bool
} }
// Query variable kind // Query variable kind
@ -751,6 +753,7 @@ TextVariableSpec: {
hide: VariableHide hide: VariableHide
skipUrlSync: bool | *false skipUrlSync: bool | *false
description?: string description?: string
showInControlsMenu?: bool
} }
// Text variable kind // Text variable kind
@ -771,6 +774,7 @@ ConstantVariableSpec: {
hide: VariableHide hide: VariableHide
skipUrlSync: bool | *false skipUrlSync: bool | *false
description?: string description?: string
showInControlsMenu?: bool
} }
// Constant variable kind // Constant variable kind
@ -798,6 +802,7 @@ DatasourceVariableSpec: {
skipUrlSync: bool | *false skipUrlSync: bool | *false
description?: string description?: string
allowCustomValue: bool | *true allowCustomValue: bool | *true
showInControlsMenu?: bool
} }
// Datasource variable kind // Datasource variable kind
@ -823,6 +828,7 @@ IntervalVariableSpec: {
hide: VariableHide hide: VariableHide
skipUrlSync: bool | *false skipUrlSync: bool | *false
description?: string description?: string
showInControlsMenu?: bool
} }
// Interval variable kind // Interval variable kind
@ -845,6 +851,7 @@ CustomVariableSpec: {
skipUrlSync: bool | *false skipUrlSync: bool | *false
description?: string description?: string
allowCustomValue: bool | *true allowCustomValue: bool | *true
showInControlsMenu?: bool
} }
// Custom variable kind // Custom variable kind
@ -867,6 +874,7 @@ GroupByVariableSpec: {
hide: VariableHide hide: VariableHide
skipUrlSync: bool | *false skipUrlSync: bool | *false
description?: string description?: string
showInControlsMenu?: bool
} }
// Group variable kind // Group variable kind
@ -890,6 +898,7 @@ AdhocVariableSpec: {
skipUrlSync: bool | *false skipUrlSync: bool | *false
description?: string description?: string
allowCustomValue: bool | *true allowCustomValue: bool | *true
showInControlsMenu?: bool
} }
// Define the MetricFindValue type // Define the MetricFindValue type

View File

@ -723,6 +723,7 @@ QueryVariableSpec: {
refresh: VariableRefresh refresh: VariableRefresh
skipUrlSync: bool | *false skipUrlSync: bool | *false
description?: string description?: string
showInControlsMenu?: bool
query: DataQueryKind query: DataQueryKind
regex: string | *"" regex: string | *""
sort: VariableSort sort: VariableSort
@ -735,6 +736,7 @@ QueryVariableSpec: {
allowCustomValue: bool | *true allowCustomValue: bool | *true
staticOptions?: [...VariableOption] staticOptions?: [...VariableOption]
staticOptionsOrder?: "before" | "after" | "sorted" staticOptionsOrder?: "before" | "after" | "sorted"
showInControlsMenu?: bool
} }
// Query variable kind // Query variable kind
@ -755,6 +757,7 @@ TextVariableSpec: {
hide: VariableHide hide: VariableHide
skipUrlSync: bool | *false skipUrlSync: bool | *false
description?: string description?: string
showInControlsMenu?: bool
} }
// Text variable kind // Text variable kind
@ -775,6 +778,7 @@ ConstantVariableSpec: {
hide: VariableHide hide: VariableHide
skipUrlSync: bool | *false skipUrlSync: bool | *false
description?: string description?: string
showInControlsMenu?: bool
} }
// Constant variable kind // Constant variable kind
@ -802,6 +806,7 @@ DatasourceVariableSpec: {
skipUrlSync: bool | *false skipUrlSync: bool | *false
description?: string description?: string
allowCustomValue: bool | *true allowCustomValue: bool | *true
showInControlsMenu?: bool
} }
// Datasource variable kind // Datasource variable kind
@ -827,6 +832,7 @@ IntervalVariableSpec: {
hide: VariableHide hide: VariableHide
skipUrlSync: bool | *false skipUrlSync: bool | *false
description?: string description?: string
showInControlsMenu?: bool
} }
// Interval variable kind // Interval variable kind
@ -849,6 +855,7 @@ CustomVariableSpec: {
skipUrlSync: bool | *false skipUrlSync: bool | *false
description?: string description?: string
allowCustomValue: bool | *true allowCustomValue: bool | *true
showInControlsMenu?: bool
} }
// Custom variable kind // Custom variable kind
@ -871,6 +878,7 @@ GroupByVariableSpec: {
hide: VariableHide hide: VariableHide
skipUrlSync: bool | *false skipUrlSync: bool | *false
description?: string description?: string
showInControlsMenu?: bool
} }
// Group variable kind // Group variable kind
@ -894,6 +902,7 @@ AdhocVariableSpec: {
skipUrlSync: bool | *false skipUrlSync: bool | *false
description?: string description?: string
allowCustomValue: bool | *true allowCustomValue: bool | *true
showInControlsMenu?: bool
} }
// Define the MetricFindValue type // Define the MetricFindValue type

View File

@ -1223,6 +1223,7 @@ type DashboardQueryVariableSpec struct {
Refresh DashboardVariableRefresh `json:"refresh"` Refresh DashboardVariableRefresh `json:"refresh"`
SkipUrlSync bool `json:"skipUrlSync"` SkipUrlSync bool `json:"skipUrlSync"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
ShowInControlsMenu *bool `json:"showInControlsMenu,omitempty"`
Query DashboardDataQueryKind `json:"query"` Query DashboardDataQueryKind `json:"query"`
Regex string `json:"regex"` Regex string `json:"regex"`
Sort DashboardVariableSort `json:"sort"` Sort DashboardVariableSort `json:"sort"`
@ -1356,6 +1357,7 @@ type DashboardTextVariableSpec struct {
Hide DashboardVariableHide `json:"hide"` Hide DashboardVariableHide `json:"hide"`
SkipUrlSync bool `json:"skipUrlSync"` SkipUrlSync bool `json:"skipUrlSync"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
ShowInControlsMenu *bool `json:"showInControlsMenu,omitempty"`
} }
// NewDashboardTextVariableSpec creates a new DashboardTextVariableSpec object. // NewDashboardTextVariableSpec creates a new DashboardTextVariableSpec object.
@ -1401,6 +1403,7 @@ type DashboardConstantVariableSpec struct {
Hide DashboardVariableHide `json:"hide"` Hide DashboardVariableHide `json:"hide"`
SkipUrlSync bool `json:"skipUrlSync"` SkipUrlSync bool `json:"skipUrlSync"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
ShowInControlsMenu *bool `json:"showInControlsMenu,omitempty"`
} }
// NewDashboardConstantVariableSpec creates a new DashboardConstantVariableSpec object. // NewDashboardConstantVariableSpec creates a new DashboardConstantVariableSpec object.
@ -1453,6 +1456,7 @@ type DashboardDatasourceVariableSpec struct {
SkipUrlSync bool `json:"skipUrlSync"` SkipUrlSync bool `json:"skipUrlSync"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
AllowCustomValue bool `json:"allowCustomValue"` AllowCustomValue bool `json:"allowCustomValue"`
ShowInControlsMenu *bool `json:"showInControlsMenu,omitempty"`
} }
// NewDashboardDatasourceVariableSpec creates a new DashboardDatasourceVariableSpec object. // NewDashboardDatasourceVariableSpec creates a new DashboardDatasourceVariableSpec object.
@ -1509,6 +1513,7 @@ type DashboardIntervalVariableSpec struct {
Hide DashboardVariableHide `json:"hide"` Hide DashboardVariableHide `json:"hide"`
SkipUrlSync bool `json:"skipUrlSync"` SkipUrlSync bool `json:"skipUrlSync"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
ShowInControlsMenu *bool `json:"showInControlsMenu,omitempty"`
} }
// NewDashboardIntervalVariableSpec creates a new DashboardIntervalVariableSpec object. // NewDashboardIntervalVariableSpec creates a new DashboardIntervalVariableSpec object.
@ -1564,6 +1569,7 @@ type DashboardCustomVariableSpec struct {
SkipUrlSync bool `json:"skipUrlSync"` SkipUrlSync bool `json:"skipUrlSync"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
AllowCustomValue bool `json:"allowCustomValue"` AllowCustomValue bool `json:"allowCustomValue"`
ShowInControlsMenu *bool `json:"showInControlsMenu,omitempty"`
} }
// NewDashboardCustomVariableSpec creates a new DashboardCustomVariableSpec object. // NewDashboardCustomVariableSpec creates a new DashboardCustomVariableSpec object.
@ -1610,6 +1616,7 @@ type DashboardGroupByVariableSpec struct {
Hide DashboardVariableHide `json:"hide"` Hide DashboardVariableHide `json:"hide"`
SkipUrlSync bool `json:"skipUrlSync"` SkipUrlSync bool `json:"skipUrlSync"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
ShowInControlsMenu *bool `json:"showInControlsMenu,omitempty"`
} }
// NewDashboardGroupByVariableSpec creates a new DashboardGroupByVariableSpec object. // NewDashboardGroupByVariableSpec creates a new DashboardGroupByVariableSpec object.
@ -1660,6 +1667,7 @@ type DashboardAdhocVariableSpec struct {
SkipUrlSync bool `json:"skipUrlSync"` SkipUrlSync bool `json:"skipUrlSync"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
AllowCustomValue bool `json:"allowCustomValue"` AllowCustomValue bool `json:"allowCustomValue"`
ShowInControlsMenu *bool `json:"showInControlsMenu,omitempty"`
} }
// NewDashboardAdhocVariableSpec creates a new DashboardAdhocVariableSpec object. // NewDashboardAdhocVariableSpec creates a new DashboardAdhocVariableSpec object.

View File

@ -526,6 +526,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardAdhocVariableSpec(ref common.Ref
Format: "", Format: "",
}, },
}, },
"showInControlsMenu": {
SchemaProps: spec.SchemaProps{
Type: []string{"boolean"},
Format: "",
},
},
}, },
Required: []string{"name", "baseFilters", "filters", "defaultKeys", "hide", "skipUrlSync", "allowCustomValue"}, Required: []string{"name", "baseFilters", "filters", "defaultKeys", "hide", "skipUrlSync", "allowCustomValue"},
}, },
@ -1191,6 +1197,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardConstantVariableSpec(ref common.
Format: "", Format: "",
}, },
}, },
"showInControlsMenu": {
SchemaProps: spec.SchemaProps{
Type: []string{"boolean"},
Format: "",
},
},
}, },
Required: []string{"name", "query", "current", "hide", "skipUrlSync"}, Required: []string{"name", "query", "current", "hide", "skipUrlSync"},
}, },
@ -1358,6 +1370,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardCustomVariableSpec(ref common.Re
Format: "", Format: "",
}, },
}, },
"showInControlsMenu": {
SchemaProps: spec.SchemaProps{
Type: []string{"boolean"},
Format: "",
},
},
}, },
Required: []string{"name", "query", "current", "options", "multi", "includeAll", "hide", "skipUrlSync", "allowCustomValue"}, Required: []string{"name", "query", "current", "options", "multi", "includeAll", "hide", "skipUrlSync", "allowCustomValue"},
}, },
@ -1743,6 +1761,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardDatasourceVariableSpec(ref commo
Format: "", Format: "",
}, },
}, },
"showInControlsMenu": {
SchemaProps: spec.SchemaProps{
Type: []string{"boolean"},
Format: "",
},
},
}, },
Required: []string{"name", "pluginId", "refresh", "regex", "current", "options", "multi", "includeAll", "hide", "skipUrlSync", "allowCustomValue"}, Required: []string{"name", "pluginId", "refresh", "regex", "current", "options", "multi", "includeAll", "hide", "skipUrlSync", "allowCustomValue"},
}, },
@ -2343,6 +2367,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardGroupByVariableSpec(ref common.R
Format: "", Format: "",
}, },
}, },
"showInControlsMenu": {
SchemaProps: spec.SchemaProps{
Type: []string{"boolean"},
Format: "",
},
},
}, },
Required: []string{"name", "current", "options", "multi", "hide", "skipUrlSync"}, Required: []string{"name", "current", "options", "multi", "hide", "skipUrlSync"},
}, },
@ -2475,6 +2505,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardIntervalVariableSpec(ref common.
Format: "", Format: "",
}, },
}, },
"showInControlsMenu": {
SchemaProps: spec.SchemaProps{
Type: []string{"boolean"},
Format: "",
},
},
}, },
Required: []string{"name", "query", "current", "options", "auto", "auto_min", "auto_count", "refresh", "hide", "skipUrlSync"}, Required: []string{"name", "query", "current", "options", "auto", "auto_min", "auto_count", "refresh", "hide", "skipUrlSync"},
}, },
@ -3250,6 +3286,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardQueryVariableSpec(ref common.Ref
Format: "", Format: "",
}, },
}, },
"showInControlsMenu": {
SchemaProps: spec.SchemaProps{
Type: []string{"boolean"},
Format: "",
},
},
"query": { "query": {
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{}, Default: map[string]interface{}{},
@ -4094,6 +4136,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardTextVariableSpec(ref common.Refe
Format: "", Format: "",
}, },
}, },
"showInControlsMenu": {
SchemaProps: spec.SchemaProps{
Type: []string{"boolean"},
Format: "",
},
},
}, },
Required: []string{"name", "current", "query", "hide", "skipUrlSync"}, Required: []string{"name", "current", "query", "hide", "skipUrlSync"},
}, },

View File

@ -186,6 +186,7 @@ export interface BaseVariableModel {
error: any | null; error: any | null;
description: string | null; description: string | null;
usedInRepeat?: boolean; usedInRepeat?: boolean;
showInControlsMenu?: boolean;
} }
export interface SnapshotVariableModel extends VariableWithOptions { export interface SnapshotVariableModel extends VariableWithOptions {

View File

@ -993,6 +993,7 @@ export interface QueryVariableSpec {
refresh: VariableRefresh; refresh: VariableRefresh;
skipUrlSync: boolean; skipUrlSync: boolean;
description?: string; description?: string;
showInControlsMenu?: boolean;
query: DataQueryKind; query: DataQueryKind;
regex: string; regex: string;
sort: VariableSort; sort: VariableSort;
@ -1087,6 +1088,7 @@ export interface TextVariableSpec {
hide: VariableHide; hide: VariableHide;
skipUrlSync: boolean; skipUrlSync: boolean;
description?: string; description?: string;
showInControlsMenu?: boolean;
} }
export const defaultTextVariableSpec = (): TextVariableSpec => ({ export const defaultTextVariableSpec = (): TextVariableSpec => ({
@ -1117,6 +1119,7 @@ export interface ConstantVariableSpec {
hide: VariableHide; hide: VariableHide;
skipUrlSync: boolean; skipUrlSync: boolean;
description?: string; description?: string;
showInControlsMenu?: boolean;
} }
export const defaultConstantVariableSpec = (): ConstantVariableSpec => ({ export const defaultConstantVariableSpec = (): ConstantVariableSpec => ({
@ -1154,6 +1157,7 @@ export interface DatasourceVariableSpec {
skipUrlSync: boolean; skipUrlSync: boolean;
description?: string; description?: string;
allowCustomValue: boolean; allowCustomValue: boolean;
showInControlsMenu?: boolean;
} }
export const defaultDatasourceVariableSpec = (): DatasourceVariableSpec => ({ export const defaultDatasourceVariableSpec = (): DatasourceVariableSpec => ({
@ -1195,6 +1199,7 @@ export interface IntervalVariableSpec {
hide: VariableHide; hide: VariableHide;
skipUrlSync: boolean; skipUrlSync: boolean;
description?: string; description?: string;
showInControlsMenu?: boolean;
} }
export const defaultIntervalVariableSpec = (): IntervalVariableSpec => ({ export const defaultIntervalVariableSpec = (): IntervalVariableSpec => ({
@ -1235,6 +1240,7 @@ export interface CustomVariableSpec {
skipUrlSync: boolean; skipUrlSync: boolean;
description?: string; description?: string;
allowCustomValue: boolean; allowCustomValue: boolean;
showInControlsMenu?: boolean;
} }
export const defaultCustomVariableSpec = (): CustomVariableSpec => ({ export const defaultCustomVariableSpec = (): CustomVariableSpec => ({
@ -1276,6 +1282,7 @@ export interface GroupByVariableSpec {
hide: VariableHide; hide: VariableHide;
skipUrlSync: boolean; skipUrlSync: boolean;
description?: string; description?: string;
showInControlsMenu?: boolean;
} }
export const defaultGroupByVariableSpec = (): GroupByVariableSpec => ({ export const defaultGroupByVariableSpec = (): GroupByVariableSpec => ({
@ -1314,6 +1321,7 @@ export interface AdhocVariableSpec {
skipUrlSync: boolean; skipUrlSync: boolean;
description?: string; description?: string;
allowCustomValue: boolean; allowCustomValue: boolean;
showInControlsMenu?: boolean;
} }
export const defaultAdhocVariableSpec = (): AdhocVariableSpec => ({ export const defaultAdhocVariableSpec = (): AdhocVariableSpec => ({

View File

@ -22,6 +22,7 @@ import { getDashboardSceneFor } from '../utils/utils';
import { DashboardLinksControls } from './DashboardLinksControls'; import { DashboardLinksControls } from './DashboardLinksControls';
import { DashboardScene } from './DashboardScene'; import { DashboardScene } from './DashboardScene';
import { DropdownVariableControls } from './DropdownVariableControls';
import { VariableControls } from './VariableControls'; import { VariableControls } from './VariableControls';
export interface DashboardControlsState extends SceneObjectState { export interface DashboardControlsState extends SceneObjectState {
@ -151,6 +152,9 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
<refreshPicker.Component model={refreshPicker} /> <refreshPicker.Component model={refreshPicker} />
</Stack> </Stack>
)} )}
<Stack>
<DropdownVariableControls dashboard={dashboard} />
</Stack>
{showDebugger && <SceneDebugger scene={model} key={'scene-debugger'} />} {showDebugger && <SceneDebugger scene={model} key={'scene-debugger'} />}
</div> </div>
); );

View File

@ -0,0 +1,120 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { SceneVariableSet, TextBoxVariable, QueryVariable, CustomVariable, SceneVariable } from '@grafana/scenes';
import { DashboardScene } from './DashboardScene';
import {
DROPDOWN_CONTROLS_ARIA_LABEL,
DROPDOWN_CONTROLS_TITLE,
DropdownVariableControls,
} from './DropdownVariableControls';
describe('DropdownVariableControls', () => {
it('should return null and not render anything when there are no variables', () => {
const { container } = render(<DropdownVariableControls dashboard={getDashboard([])} />);
expect(container.firstChild).toBeNull();
});
it('should return null when variables exist but none of them is meant to be shown in the controls menu', () => {
const variables = [
new TextBoxVariable({
name: 'textVar',
value: 'test',
showInControlsMenu: false,
}),
];
const { container } = render(<DropdownVariableControls dashboard={getDashboard(variables)} />);
expect(container.firstChild).toBeNull();
});
it('should render a dropdown with a toolbar-button when there are any variables that are set to be shown under the controls menu', () => {
const variables = [
new TextBoxVariable({
name: 'textVar',
value: 'test',
showInControlsMenu: true,
}),
];
render(<DropdownVariableControls dashboard={getDashboard(variables)} />);
// Should render the toolbar button
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
expect(button).toHaveAttribute('aria-label', DROPDOWN_CONTROLS_ARIA_LABEL);
expect(button).toHaveAttribute('title', DROPDOWN_CONTROLS_TITLE);
});
it('should render multiple variables in dropdown menu', async () => {
const variables = [
new TextBoxVariable({
name: 'textVar1',
value: 'test1',
showInControlsMenu: true,
}),
new TextBoxVariable({
name: 'textVar2',
value: 'test2',
showInControlsMenu: true,
}),
new QueryVariable({
name: 'queryVar',
query: 'test query',
showInControlsMenu: true,
}),
];
render(<DropdownVariableControls dashboard={getDashboard(variables)} />);
// Should have rendered a dropdown
expect(screen.getByRole('button')).toBeInTheDocument();
// Open the dropdown
userEvent.click(screen.getByRole('button'));
expect(await screen.findByText('textVar1')).toBeInTheDocument();
expect(await screen.findByText('textVar2')).toBeInTheDocument();
expect(await screen.findByText('queryVar')).toBeInTheDocument();
});
it('should filter out variables with showInControlsMenu=false', async () => {
const variables = [
new TextBoxVariable({
name: 'textVar1',
value: 'test1',
showInControlsMenu: true,
}),
new TextBoxVariable({
name: 'textVar2',
value: 'test2',
showInControlsMenu: false, // This should be filtered out
}),
new CustomVariable({
name: 'customVar',
query: 'option1,option2',
showInControlsMenu: true,
}),
];
render(<DropdownVariableControls dashboard={getDashboard(variables)} />);
// Should still render dropdown since we have variables with showInControlsMenu=true
expect(screen.getByRole('button')).toBeInTheDocument();
// Open the dropdown
userEvent.click(screen.getByRole('button'));
expect(await screen.findByText('textVar1')).toBeInTheDocument();
expect(await screen.findByText('customVar')).toBeInTheDocument();
expect(screen.queryByText('textVar2')).not.toBeInTheDocument();
});
});
function getDashboard(variables: SceneVariable[]): DashboardScene {
return new DashboardScene({
uid: 'test-dashboard',
title: 'Test Dashboard',
$variables: new SceneVariableSet({
variables,
}),
});
}

View File

@ -0,0 +1,56 @@
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 { 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
overlay={
<Menu
onClick={(e) => {
e.stopPropagation();
}}
>
{variables.map((variable) => (
<div className={styles.menuItem} key={variable.state.key}>
<VariableValueSelectWrapper variable={variable} />
</div>
))}
</Menu>
}
>
<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
/>
</Dropdown>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
menuItem: css({
padding: theme.spacing(0.5),
}),
});

View File

@ -8,11 +8,13 @@ import { useElementSelection, useStyles2 } from '@grafana/ui';
import { DashboardScene } from './DashboardScene'; import { DashboardScene } from './DashboardScene';
export function VariableControls({ dashboard }: { dashboard: DashboardScene }) { export function VariableControls({ dashboard }: { dashboard: DashboardScene }) {
const variables = sceneGraph.getVariables(dashboard)!.useState(); const { variables } = sceneGraph.getVariables(dashboard)!.useState();
return ( return (
<> <>
{variables.variables.map((variable) => ( {variables
.filter((v) => !v.state.showInControlsMenu)
.map((variable) => (
<VariableValueSelectWrapper key={variable.state.key} variable={variable} /> <VariableValueSelectWrapper key={variable.state.key} variable={variable} />
))} ))}
</> </>

View File

@ -93,6 +93,7 @@ describe('sceneVariablesSetToVariables', () => {
name: 'test', name: 'test',
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
showInControlsMenu: true,
value: ['selected-value'], value: ['selected-value'],
text: ['selected-value-text'], text: ['selected-value-text'],
datasource: { uid: 'fake-uid', type: 'fake-type' }, datasource: { uid: 'fake-uid', type: 'fake-type' },
@ -138,6 +139,7 @@ describe('sceneVariablesSetToVariables', () => {
"query": "query", "query": "query",
"refresh": 1, "refresh": 1,
"regex": "", "regex": "",
"showInControlsMenu": true,
"staticOptions": [ "staticOptions": [
{ {
"text": "test", "text": "test",
@ -155,6 +157,7 @@ describe('sceneVariablesSetToVariables', () => {
name: 'test', name: 'test',
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
showInControlsMenu: true,
value: ['selected-value'], value: ['selected-value'],
text: ['selected-value-text'], text: ['selected-value-text'],
datasource: { uid: 'fake-uid', type: 'fake-type' }, datasource: { uid: 'fake-uid', type: 'fake-type' },
@ -200,6 +203,7 @@ describe('sceneVariablesSetToVariables', () => {
"query": "query", "query": "query",
"refresh": 1, "refresh": 1,
"regex": "", "regex": "",
"showInControlsMenu": true,
"staticOptions": [ "staticOptions": [
{ {
"text": "test", "text": "test",
@ -217,6 +221,7 @@ describe('sceneVariablesSetToVariables', () => {
name: 'test', name: 'test',
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
showInControlsMenu: true,
value: ['selected-value'], value: ['selected-value'],
text: ['selected-value-text'], text: ['selected-value-text'],
datasource: { uid: 'fake-uid', type: 'fake-type' }, datasource: { uid: 'fake-uid', type: 'fake-type' },
@ -244,6 +249,7 @@ describe('sceneVariablesSetToVariables', () => {
name: 'test', name: 'test',
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
showInControlsMenu: true,
value: ['test'], value: ['test'],
text: ['test'], text: ['test'],
datasource: { uid: 'fake-uid', type: 'fake-type' }, datasource: { uid: 'fake-uid', type: 'fake-type' },
@ -273,6 +279,7 @@ describe('sceneVariablesSetToVariables', () => {
name: 'test', name: 'test',
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
showInControlsMenu: true,
value: ['selected-ds-1', 'selected-ds-2'], value: ['selected-ds-1', 'selected-ds-2'],
text: ['selected-ds-1-text', 'selected-ds-2-text'], text: ['selected-ds-1-text', 'selected-ds-2-text'],
pluginId: 'fake-std', pluginId: 'fake-std',
@ -311,6 +318,7 @@ describe('sceneVariablesSetToVariables', () => {
"query": "fake-std", "query": "fake-std",
"refresh": 1, "refresh": 1,
"regex": "", "regex": "",
"showInControlsMenu": true,
"type": "datasource", "type": "datasource",
} }
`); `);
@ -321,6 +329,7 @@ describe('sceneVariablesSetToVariables', () => {
name: 'test', name: 'test',
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
showInControlsMenu: true,
value: ['test', 'test2'], value: ['test', 'test2'],
text: ['test', 'test2'], text: ['test', 'test2'],
query: 'test,test1,test2', query: 'test,test1,test2',
@ -378,6 +387,7 @@ describe('sceneVariablesSetToVariables', () => {
}, },
], ],
"query": "test,test1,test2", "query": "test,test1,test2",
"showInControlsMenu": true,
"type": "custom", "type": "custom",
} }
`); `);
@ -388,6 +398,7 @@ describe('sceneVariablesSetToVariables', () => {
name: 'test', name: 'test',
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
showInControlsMenu: true,
value: 'constant value', value: 'constant value',
skipUrlSync: true, skipUrlSync: true,
}); });
@ -409,6 +420,7 @@ describe('sceneVariablesSetToVariables', () => {
"label": "test-label", "label": "test-label",
"name": "test", "name": "test",
"query": "constant value", "query": "constant value",
"showInControlsMenu": true,
"skipUrlSync": true, "skipUrlSync": true,
"type": "constant", "type": "constant",
} }
@ -420,6 +432,7 @@ describe('sceneVariablesSetToVariables', () => {
name: 'test', name: 'test',
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
showInControlsMenu: true,
value: 'text value', value: 'text value',
skipUrlSync: true, skipUrlSync: true,
}); });
@ -447,6 +460,7 @@ describe('sceneVariablesSetToVariables', () => {
}, },
], ],
"query": "text value", "query": "text value",
"showInControlsMenu": true,
"skipUrlSync": true, "skipUrlSync": true,
"type": "textbox", "type": "textbox",
} }
@ -458,6 +472,7 @@ describe('sceneVariablesSetToVariables', () => {
intervals: ['1m', '2m', '3m', '1h', '1d'], intervals: ['1m', '2m', '3m', '1h', '1d'],
value: '1m', value: '1m',
refresh: VariableRefresh.onDashboardLoad, refresh: VariableRefresh.onDashboardLoad,
showInControlsMenu: true,
}); });
const set = new SceneVariableSet({ const set = new SceneVariableSet({
variables: [variable], variables: [variable],
@ -506,6 +521,7 @@ describe('sceneVariablesSetToVariables', () => {
], ],
"query": "1m,2m,3m,1h,1d", "query": "1m,2m,3m,1h,1d",
"refresh": 1, "refresh": 1,
"showInControlsMenu": true,
"type": "interval", "type": "interval",
} }
`); `);
@ -517,6 +533,7 @@ describe('sceneVariablesSetToVariables', () => {
allowCustomValue: true, allowCustomValue: true,
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
showInControlsMenu: true,
datasource: { uid: 'fake-uid', type: 'fake-type' }, datasource: { uid: 'fake-uid', type: 'fake-type' },
filters: [ filters: [
{ {
@ -565,6 +582,7 @@ describe('sceneVariablesSetToVariables', () => {
], ],
"label": "test-label", "label": "test-label",
"name": "test", "name": "test",
"showInControlsMenu": true,
"type": "adhoc", "type": "adhoc",
} }
`); `);
@ -577,6 +595,7 @@ describe('sceneVariablesSetToVariables', () => {
allowCustomValue: true, allowCustomValue: true,
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
showInControlsMenu: true,
datasource: { uid: 'fake-std', type: 'fake-std' }, datasource: { uid: 'fake-std', type: 'fake-std' },
originFilters: [ originFilters: [
{ {
@ -608,6 +627,7 @@ describe('sceneVariablesSetToVariables', () => {
"filters": [], "filters": [],
"label": "test-label", "label": "test-label",
"name": "test", "name": "test",
"showInControlsMenu": true,
"type": "adhoc", "type": "adhoc",
} }
`); `);
@ -619,6 +639,7 @@ describe('sceneVariablesSetToVariables', () => {
allowCustomValue: true, allowCustomValue: true,
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
showInControlsMenu: true,
datasource: { uid: 'fake-std', type: 'fake-std' }, datasource: { uid: 'fake-std', type: 'fake-std' },
originFilters: [ originFilters: [
{ {
@ -668,6 +689,7 @@ describe('sceneVariablesSetToVariables', () => {
], ],
"label": "test-label", "label": "test-label",
"name": "test", "name": "test",
"showInControlsMenu": true,
"type": "adhoc", "type": "adhoc",
} }
`); `);
@ -680,6 +702,7 @@ describe('sceneVariablesSetToVariables', () => {
allowCustomValue: true, allowCustomValue: true,
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
showInControlsMenu: true,
datasource: { uid: 'fake-uid', type: 'fake-type' }, datasource: { uid: 'fake-uid', type: 'fake-type' },
defaultKeys: [ defaultKeys: [
{ {
@ -755,6 +778,7 @@ describe('sceneVariablesSetToVariables', () => {
], ],
"label": "test-label", "label": "test-label",
"name": "test", "name": "test",
"showInControlsMenu": true,
"type": "adhoc", "type": "adhoc",
} }
`); `);
@ -775,6 +799,7 @@ describe('sceneVariablesSetToVariables', () => {
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
allowCustomValue: true, allowCustomValue: true,
showInControlsMenu: true,
datasource: { uid: 'fake-uid', type: 'fake-type' }, datasource: { uid: 'fake-uid', type: 'fake-type' },
defaultOptions: [ defaultOptions: [
{ {
@ -819,6 +844,7 @@ describe('sceneVariablesSetToVariables', () => {
"value": "bar", "value": "bar",
}, },
], ],
"showInControlsMenu": true,
"type": "groupby", "type": "groupby",
} }
`); `);
@ -831,6 +857,7 @@ describe('sceneVariablesSetToVariables', () => {
name: 'test', name: 'test',
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
showInControlsMenu: true,
datasource: { uid: 'fake-uid', type: 'fake-type' }, datasource: { uid: 'fake-uid', type: 'fake-type' },
defaultOptions: [ defaultOptions: [
{ {
@ -866,6 +893,7 @@ describe('sceneVariablesSetToVariables', () => {
isMulti: true, isMulti: true,
staticOptions: [{ label: 'test', value: 'test' }], staticOptions: [{ label: 'test', value: 'test' }],
staticOptionsOrder: 'after', staticOptionsOrder: 'after',
showInControlsMenu: true,
}); });
const set = new SceneVariableSet({ const set = new SceneVariableSet({
@ -910,6 +938,7 @@ describe('sceneVariablesSetToVariables', () => {
}, },
"refresh": "onDashboardLoad", "refresh": "onDashboardLoad",
"regex": "", "regex": "",
"showInControlsMenu": true,
"skipUrlSync": false, "skipUrlSync": false,
"sort": "disabled", "sort": "disabled",
"staticOptions": [ "staticOptions": [
@ -932,6 +961,7 @@ describe('sceneVariablesSetToVariables', () => {
value: ['test', 'test2'], value: ['test', 'test2'],
text: ['test', 'test2'], text: ['test', 'test2'],
query: 'test,test1,test2', query: 'test,test1,test2',
showInControlsMenu: true,
options: [ options: [
{ label: 'test', value: 'test' }, { label: 'test', value: 'test' },
{ label: 'test1', value: 'test1' }, { label: 'test1', value: 'test1' },
@ -988,6 +1018,7 @@ describe('sceneVariablesSetToVariables', () => {
}, },
], ],
"query": "test,test1,test2", "query": "test,test1,test2",
"showInControlsMenu": true,
"skipUrlSync": false, "skipUrlSync": false,
}, },
} }
@ -1005,6 +1036,7 @@ describe('sceneVariablesSetToVariables', () => {
includeAll: true, includeAll: true,
allValue: 'test-all', allValue: 'test-all',
isMulti: true, isMulti: true,
showInControlsMenu: true,
}); });
const set = new SceneVariableSet({ const set = new SceneVariableSet({
variables: [variable], variables: [variable],
@ -1039,6 +1071,7 @@ describe('sceneVariablesSetToVariables', () => {
"pluginId": "fake-std", "pluginId": "fake-std",
"refresh": "onDashboardLoad", "refresh": "onDashboardLoad",
"regex": "", "regex": "",
"showInControlsMenu": true,
"skipUrlSync": false, "skipUrlSync": false,
}, },
} }
@ -1051,6 +1084,7 @@ describe('sceneVariablesSetToVariables', () => {
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
value: 'constant value', value: 'constant value',
showInControlsMenu: true,
skipUrlSync: true, skipUrlSync: true,
}); });
const set = new SceneVariableSet({ const set = new SceneVariableSet({
@ -1073,6 +1107,7 @@ describe('sceneVariablesSetToVariables', () => {
"label": "test-label", "label": "test-label",
"name": "test", "name": "test",
"query": "constant value", "query": "constant value",
"showInControlsMenu": true,
"skipUrlSync": true, "skipUrlSync": true,
}, },
} }
@ -1085,6 +1120,7 @@ describe('sceneVariablesSetToVariables', () => {
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
value: 'text value', value: 'text value',
showInControlsMenu: true,
skipUrlSync: true, skipUrlSync: true,
}); });
const set = new SceneVariableSet({ const set = new SceneVariableSet({
@ -1107,6 +1143,7 @@ describe('sceneVariablesSetToVariables', () => {
"label": "test-label", "label": "test-label",
"name": "test", "name": "test",
"query": "text value", "query": "text value",
"showInControlsMenu": true,
"skipUrlSync": true, "skipUrlSync": true,
}, },
} }
@ -1117,6 +1154,7 @@ describe('sceneVariablesSetToVariables', () => {
const variable = new IntervalVariable({ const variable = new IntervalVariable({
intervals: ['1m', '2m', '3m', '1h', '1d'], intervals: ['1m', '2m', '3m', '1h', '1d'],
value: '1m', value: '1m',
showInControlsMenu: true,
refresh: VariableRefresh.onDashboardLoad, refresh: VariableRefresh.onDashboardLoad,
}); });
const set = new SceneVariableSet({ const set = new SceneVariableSet({
@ -1169,6 +1207,7 @@ describe('sceneVariablesSetToVariables', () => {
], ],
"query": "1m,2m,3m,1h,1d", "query": "1m,2m,3m,1h,1d",
"refresh": "onTimeRangeChanged", "refresh": "onTimeRangeChanged",
"showInControlsMenu": true,
"skipUrlSync": false, "skipUrlSync": false,
}, },
} }
@ -1180,6 +1219,7 @@ describe('sceneVariablesSetToVariables', () => {
name: 'test', name: 'test',
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
showInControlsMenu: true,
datasource: { uid: 'fake-uid', type: 'fake-type' }, datasource: { uid: 'fake-uid', type: 'fake-type' },
filters: [ filters: [
{ {
@ -1231,6 +1271,7 @@ describe('sceneVariablesSetToVariables', () => {
"hide": "dontHide", "hide": "dontHide",
"label": "test-label", "label": "test-label",
"name": "test", "name": "test",
"showInControlsMenu": true,
"skipUrlSync": false, "skipUrlSync": false,
}, },
} }
@ -1242,6 +1283,7 @@ describe('sceneVariablesSetToVariables', () => {
name: 'test', name: 'test',
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
showInControlsMenu: true,
datasource: { uid: 'fake-uid', type: 'fake-type' }, datasource: { uid: 'fake-uid', type: 'fake-type' },
defaultKeys: [ defaultKeys: [
{ {
@ -1320,6 +1362,7 @@ describe('sceneVariablesSetToVariables', () => {
"hide": "dontHide", "hide": "dontHide",
"label": "test-label", "label": "test-label",
"name": "test", "name": "test",
"showInControlsMenu": true,
"skipUrlSync": false, "skipUrlSync": false,
}, },
} }
@ -1340,6 +1383,7 @@ describe('sceneVariablesSetToVariables', () => {
name: 'test', name: 'test',
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
showInControlsMenu: true,
datasource: { uid: 'fake-uid', type: 'fake-type' }, datasource: { uid: 'fake-uid', type: 'fake-type' },
defaultOptions: [ defaultOptions: [
{ {
@ -1387,6 +1431,7 @@ describe('sceneVariablesSetToVariables', () => {
"value": "bar", "value": "bar",
}, },
], ],
"showInControlsMenu": true,
"skipUrlSync": false, "skipUrlSync": false,
}, },
} }
@ -1400,6 +1445,7 @@ describe('sceneVariablesSetToVariables', () => {
name: 'test', name: 'test',
label: 'test-label', label: 'test-label',
description: 'test-desc', description: 'test-desc',
showInControlsMenu: true,
datasource: { uid: 'fake-uid', type: 'fake-type' }, datasource: { uid: 'fake-uid', type: 'fake-type' },
defaultOptions: [ defaultOptions: [
{ {

View File

@ -60,6 +60,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
skipUrlSync: Boolean(variable.state.skipUrlSync), skipUrlSync: Boolean(variable.state.skipUrlSync),
hide: variable.state.hide || OldVariableHide.dontHide, hide: variable.state.hide || OldVariableHide.dontHide,
type: variable.state.type, type: variable.state.type,
showInControlsMenu: variable.state.showInControlsMenu,
}; };
if (sceneUtils.isQueryVariable(variable)) { if (sceneUtils.isQueryVariable(variable)) {
@ -283,6 +284,7 @@ export function sceneVariablesSetToSchemaV2Variables(
description: variable.state.description ?? undefined, description: variable.state.description ?? undefined,
skipUrlSync: Boolean(variable.state.skipUrlSync), skipUrlSync: Boolean(variable.state.skipUrlSync),
hide: transformVariableHideToEnum(variable.state.hide) || defaultVariableHide(), hide: transformVariableHideToEnum(variable.state.hide) || defaultVariableHide(),
showInControlsMenu: variable.state.showInControlsMenu,
}; };
// current: VariableOption; // current: VariableOption;

View File

@ -273,6 +273,7 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S
name: variable.spec.name, name: variable.spec.name,
label: variable.spec.label, label: variable.spec.label,
description: variable.spec.description, description: variable.spec.description,
showInControlsMenu: variable.spec.showInControlsMenu,
}; };
if (variable.kind === defaultAdhocVariableKind().kind) { if (variable.kind === defaultAdhocVariableKind().kind) {
const ds = getDataSourceForQuery( const ds = getDataSourceForQuery(

View File

@ -131,6 +131,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
name: variable.name, name: variable.name,
label: variable.label, label: variable.label,
description: variable.description, description: variable.description,
showInControlsMenu: variable.showInControlsMenu,
}; };
if (variable.type === 'adhoc') { if (variable.type === 'adhoc') {
const originFilters: AdHocVariableFilter[] = []; const originFilters: AdHocVariableFilter[] = [];

View File

@ -4562,6 +4562,12 @@
"overwrite": "Overwrite", "overwrite": "Overwrite",
"title-plugin-dashboard": "Plugin dashboard" "title-plugin-dashboard": "Plugin dashboard"
}, },
"controls": {
"menu": {
"aria-label": "",
"title": ""
}
},
"dash-nav": { "dash-nav": {
"on-open-snapshot-original": { "on-open-snapshot-original": {
"confirmText": { "confirmText": {