From a746f6e1211a6719a62fd0d94e54368c84b1fd03 Mon Sep 17 00:00:00 2001 From: Levente Balogh Date: Fri, 29 Aug 2025 14:56:26 +0200 Subject: [PATCH] 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 --- .../kinds/v2beta1/dashboard_spec.cue | 9 ++ .../apis/dashboard/v2beta1/dashboard_spec.cue | 9 ++ .../dashboard/v2beta1/dashboard_spec_gen.go | 148 +++++++++--------- .../dashboard/v2beta1/zz_generated.openapi.go | 48 ++++++ .../grafana-data/src/types/templateVars.ts | 1 + .../dashboard/v2beta1/types.spec.gen.ts | 8 + .../scene/DashboardControls.tsx | 4 + .../scene/DropdownVariableControls.test.tsx | 120 ++++++++++++++ .../scene/DropdownVariableControls.tsx | 56 +++++++ .../scene/VariableControls.tsx | 10 +- .../sceneVariablesSetToVariables.test.ts | 46 ++++++ .../sceneVariablesSetToVariables.ts | 2 + .../transformSaveModelSchemaV2ToScene.ts | 1 + .../dashboard-scene/utils/variables.ts | 1 + public/locales/en-US/grafana.json | 6 + 15 files changed, 395 insertions(+), 74 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/DropdownVariableControls.test.tsx create mode 100644 public/app/features/dashboard-scene/scene/DropdownVariableControls.tsx diff --git a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue index 1c7ca148a08..4618bc87f86 100644 --- a/apps/dashboard/kinds/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2beta1/dashboard_spec.cue @@ -719,6 +719,7 @@ QueryVariableSpec: { refresh: VariableRefresh skipUrlSync: bool | *false description?: string + showInControlsMenu?: bool query: DataQueryKind regex: string | *"" sort: VariableSort @@ -731,6 +732,7 @@ QueryVariableSpec: { allowCustomValue: bool | *true staticOptions?: [...VariableOption] staticOptionsOrder?: "before" | "after" | "sorted" + showInControlsMenu?: bool } // Query variable kind @@ -751,6 +753,7 @@ TextVariableSpec: { hide: VariableHide skipUrlSync: bool | *false description?: string + showInControlsMenu?: bool } // Text variable kind @@ -771,6 +774,7 @@ ConstantVariableSpec: { hide: VariableHide skipUrlSync: bool | *false description?: string + showInControlsMenu?: bool } // Constant variable kind @@ -798,6 +802,7 @@ DatasourceVariableSpec: { skipUrlSync: bool | *false description?: string allowCustomValue: bool | *true + showInControlsMenu?: bool } // Datasource variable kind @@ -823,6 +828,7 @@ IntervalVariableSpec: { hide: VariableHide skipUrlSync: bool | *false description?: string + showInControlsMenu?: bool } // Interval variable kind @@ -845,6 +851,7 @@ CustomVariableSpec: { skipUrlSync: bool | *false description?: string allowCustomValue: bool | *true + showInControlsMenu?: bool } // Custom variable kind @@ -867,6 +874,7 @@ GroupByVariableSpec: { hide: VariableHide skipUrlSync: bool | *false description?: string + showInControlsMenu?: bool } // Group variable kind @@ -890,6 +898,7 @@ AdhocVariableSpec: { skipUrlSync: bool | *false description?: string allowCustomValue: bool | *true + showInControlsMenu?: bool } // Define the MetricFindValue type diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue index d110a33fa01..071a920ba19 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec.cue @@ -723,6 +723,7 @@ QueryVariableSpec: { refresh: VariableRefresh skipUrlSync: bool | *false description?: string + showInControlsMenu?: bool query: DataQueryKind regex: string | *"" sort: VariableSort @@ -735,6 +736,7 @@ QueryVariableSpec: { allowCustomValue: bool | *true staticOptions?: [...VariableOption] staticOptionsOrder?: "before" | "after" | "sorted" + showInControlsMenu?: bool } // Query variable kind @@ -755,6 +757,7 @@ TextVariableSpec: { hide: VariableHide skipUrlSync: bool | *false description?: string + showInControlsMenu?: bool } // Text variable kind @@ -775,6 +778,7 @@ ConstantVariableSpec: { hide: VariableHide skipUrlSync: bool | *false description?: string + showInControlsMenu?: bool } // Constant variable kind @@ -802,6 +806,7 @@ DatasourceVariableSpec: { skipUrlSync: bool | *false description?: string allowCustomValue: bool | *true + showInControlsMenu?: bool } // Datasource variable kind @@ -827,6 +832,7 @@ IntervalVariableSpec: { hide: VariableHide skipUrlSync: bool | *false description?: string + showInControlsMenu?: bool } // Interval variable kind @@ -849,6 +855,7 @@ CustomVariableSpec: { skipUrlSync: bool | *false description?: string allowCustomValue: bool | *true + showInControlsMenu?: bool } // Custom variable kind @@ -871,6 +878,7 @@ GroupByVariableSpec: { hide: VariableHide skipUrlSync: bool | *false description?: string + showInControlsMenu?: bool } // Group variable kind @@ -894,6 +902,7 @@ AdhocVariableSpec: { skipUrlSync: bool | *false description?: string allowCustomValue: bool | *true + showInControlsMenu?: bool } // Define the MetricFindValue type diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go index 10e17f2c5e2..7096c8b2cc1 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/dashboard_spec_gen.go @@ -1223,6 +1223,7 @@ type DashboardQueryVariableSpec struct { Refresh DashboardVariableRefresh `json:"refresh"` SkipUrlSync bool `json:"skipUrlSync"` Description *string `json:"description,omitempty"` + ShowInControlsMenu *bool `json:"showInControlsMenu,omitempty"` Query DashboardDataQueryKind `json:"query"` Regex string `json:"regex"` Sort DashboardVariableSort `json:"sort"` @@ -1349,13 +1350,14 @@ func NewDashboardTextVariableKind() *DashboardTextVariableKind { // Text variable specification // +k8s:openapi-gen=true type DashboardTextVariableSpec struct { - Name string `json:"name"` - Current DashboardVariableOption `json:"current"` - Query string `json:"query"` - Label *string `json:"label,omitempty"` - Hide DashboardVariableHide `json:"hide"` - SkipUrlSync bool `json:"skipUrlSync"` - Description *string `json:"description,omitempty"` + Name string `json:"name"` + Current DashboardVariableOption `json:"current"` + Query string `json:"query"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` + ShowInControlsMenu *bool `json:"showInControlsMenu,omitempty"` } // NewDashboardTextVariableSpec creates a new DashboardTextVariableSpec object. @@ -1394,13 +1396,14 @@ func NewDashboardConstantVariableKind() *DashboardConstantVariableKind { // Constant variable specification // +k8s:openapi-gen=true type DashboardConstantVariableSpec struct { - Name string `json:"name"` - Query string `json:"query"` - Current DashboardVariableOption `json:"current"` - Label *string `json:"label,omitempty"` - Hide DashboardVariableHide `json:"hide"` - SkipUrlSync bool `json:"skipUrlSync"` - Description *string `json:"description,omitempty"` + Name string `json:"name"` + Query string `json:"query"` + Current DashboardVariableOption `json:"current"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` + ShowInControlsMenu *bool `json:"showInControlsMenu,omitempty"` } // NewDashboardConstantVariableSpec creates a new DashboardConstantVariableSpec object. @@ -1439,20 +1442,21 @@ func NewDashboardDatasourceVariableKind() *DashboardDatasourceVariableKind { // Datasource variable specification // +k8s:openapi-gen=true type DashboardDatasourceVariableSpec struct { - Name string `json:"name"` - PluginId string `json:"pluginId"` - Refresh DashboardVariableRefresh `json:"refresh"` - Regex string `json:"regex"` - Current DashboardVariableOption `json:"current"` - Options []DashboardVariableOption `json:"options"` - Multi bool `json:"multi"` - IncludeAll bool `json:"includeAll"` - AllValue *string `json:"allValue,omitempty"` - Label *string `json:"label,omitempty"` - Hide DashboardVariableHide `json:"hide"` - SkipUrlSync bool `json:"skipUrlSync"` - Description *string `json:"description,omitempty"` - AllowCustomValue bool `json:"allowCustomValue"` + Name string `json:"name"` + PluginId string `json:"pluginId"` + Refresh DashboardVariableRefresh `json:"refresh"` + Regex string `json:"regex"` + Current DashboardVariableOption `json:"current"` + Options []DashboardVariableOption `json:"options"` + Multi bool `json:"multi"` + IncludeAll bool `json:"includeAll"` + AllValue *string `json:"allValue,omitempty"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` + AllowCustomValue bool `json:"allowCustomValue"` + ShowInControlsMenu *bool `json:"showInControlsMenu,omitempty"` } // NewDashboardDatasourceVariableSpec creates a new DashboardDatasourceVariableSpec object. @@ -1497,18 +1501,19 @@ func NewDashboardIntervalVariableKind() *DashboardIntervalVariableKind { // Interval variable specification // +k8s:openapi-gen=true type DashboardIntervalVariableSpec struct { - Name string `json:"name"` - Query string `json:"query"` - Current DashboardVariableOption `json:"current"` - Options []DashboardVariableOption `json:"options"` - Auto bool `json:"auto"` - AutoMin string `json:"auto_min"` - AutoCount int64 `json:"auto_count"` - Refresh DashboardVariableRefresh `json:"refresh"` - Label *string `json:"label,omitempty"` - Hide DashboardVariableHide `json:"hide"` - SkipUrlSync bool `json:"skipUrlSync"` - Description *string `json:"description,omitempty"` + Name string `json:"name"` + Query string `json:"query"` + Current DashboardVariableOption `json:"current"` + Options []DashboardVariableOption `json:"options"` + Auto bool `json:"auto"` + AutoMin string `json:"auto_min"` + AutoCount int64 `json:"auto_count"` + Refresh DashboardVariableRefresh `json:"refresh"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` + ShowInControlsMenu *bool `json:"showInControlsMenu,omitempty"` } // NewDashboardIntervalVariableSpec creates a new DashboardIntervalVariableSpec object. @@ -1552,18 +1557,19 @@ func NewDashboardCustomVariableKind() *DashboardCustomVariableKind { // Custom variable specification // +k8s:openapi-gen=true type DashboardCustomVariableSpec struct { - Name string `json:"name"` - Query string `json:"query"` - Current DashboardVariableOption `json:"current"` - Options []DashboardVariableOption `json:"options"` - Multi bool `json:"multi"` - IncludeAll bool `json:"includeAll"` - AllValue *string `json:"allValue,omitempty"` - Label *string `json:"label,omitempty"` - Hide DashboardVariableHide `json:"hide"` - SkipUrlSync bool `json:"skipUrlSync"` - Description *string `json:"description,omitempty"` - AllowCustomValue bool `json:"allowCustomValue"` + Name string `json:"name"` + Query string `json:"query"` + Current DashboardVariableOption `json:"current"` + Options []DashboardVariableOption `json:"options"` + Multi bool `json:"multi"` + IncludeAll bool `json:"includeAll"` + AllValue *string `json:"allValue,omitempty"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` + AllowCustomValue bool `json:"allowCustomValue"` + ShowInControlsMenu *bool `json:"showInControlsMenu,omitempty"` } // NewDashboardCustomVariableSpec creates a new DashboardCustomVariableSpec object. @@ -1601,15 +1607,16 @@ func NewDashboardGroupByVariableKind() *DashboardGroupByVariableKind { // GroupBy variable specification // +k8s:openapi-gen=true type DashboardGroupByVariableSpec struct { - Name string `json:"name"` - DefaultValue *DashboardVariableOption `json:"defaultValue,omitempty"` - Current DashboardVariableOption `json:"current"` - Options []DashboardVariableOption `json:"options"` - Multi bool `json:"multi"` - Label *string `json:"label,omitempty"` - Hide DashboardVariableHide `json:"hide"` - SkipUrlSync bool `json:"skipUrlSync"` - Description *string `json:"description,omitempty"` + Name string `json:"name"` + DefaultValue *DashboardVariableOption `json:"defaultValue,omitempty"` + Current DashboardVariableOption `json:"current"` + Options []DashboardVariableOption `json:"options"` + Multi bool `json:"multi"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` + ShowInControlsMenu *bool `json:"showInControlsMenu,omitempty"` } // NewDashboardGroupByVariableSpec creates a new DashboardGroupByVariableSpec object. @@ -1651,15 +1658,16 @@ func NewDashboardAdhocVariableKind() *DashboardAdhocVariableKind { // Adhoc variable specification // +k8s:openapi-gen=true type DashboardAdhocVariableSpec struct { - Name string `json:"name"` - BaseFilters []DashboardAdHocFilterWithLabels `json:"baseFilters"` - Filters []DashboardAdHocFilterWithLabels `json:"filters"` - DefaultKeys []DashboardMetricFindValue `json:"defaultKeys"` - Label *string `json:"label,omitempty"` - Hide DashboardVariableHide `json:"hide"` - SkipUrlSync bool `json:"skipUrlSync"` - Description *string `json:"description,omitempty"` - AllowCustomValue bool `json:"allowCustomValue"` + Name string `json:"name"` + BaseFilters []DashboardAdHocFilterWithLabels `json:"baseFilters"` + Filters []DashboardAdHocFilterWithLabels `json:"filters"` + DefaultKeys []DashboardMetricFindValue `json:"defaultKeys"` + Label *string `json:"label,omitempty"` + Hide DashboardVariableHide `json:"hide"` + SkipUrlSync bool `json:"skipUrlSync"` + Description *string `json:"description,omitempty"` + AllowCustomValue bool `json:"allowCustomValue"` + ShowInControlsMenu *bool `json:"showInControlsMenu,omitempty"` } // NewDashboardAdhocVariableSpec creates a new DashboardAdhocVariableSpec object. diff --git a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go index 8f555e11ee2..ef920f197e0 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2beta1/zz_generated.openapi.go @@ -526,6 +526,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardAdhocVariableSpec(ref common.Ref Format: "", }, }, + "showInControlsMenu": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, }, Required: []string{"name", "baseFilters", "filters", "defaultKeys", "hide", "skipUrlSync", "allowCustomValue"}, }, @@ -1191,6 +1197,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardConstantVariableSpec(ref common. Format: "", }, }, + "showInControlsMenu": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, }, Required: []string{"name", "query", "current", "hide", "skipUrlSync"}, }, @@ -1358,6 +1370,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardCustomVariableSpec(ref common.Re Format: "", }, }, + "showInControlsMenu": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, }, 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: "", }, }, + "showInControlsMenu": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, }, 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: "", }, }, + "showInControlsMenu": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, }, Required: []string{"name", "current", "options", "multi", "hide", "skipUrlSync"}, }, @@ -2475,6 +2505,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardIntervalVariableSpec(ref common. Format: "", }, }, + "showInControlsMenu": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, }, 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: "", }, }, + "showInControlsMenu": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, "query": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, @@ -4094,6 +4136,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardTextVariableSpec(ref common.Refe Format: "", }, }, + "showInControlsMenu": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, }, Required: []string{"name", "current", "query", "hide", "skipUrlSync"}, }, diff --git a/packages/grafana-data/src/types/templateVars.ts b/packages/grafana-data/src/types/templateVars.ts index a4b125c1005..168c7f6a451 100644 --- a/packages/grafana-data/src/types/templateVars.ts +++ b/packages/grafana-data/src/types/templateVars.ts @@ -186,6 +186,7 @@ export interface BaseVariableModel { error: any | null; description: string | null; usedInRepeat?: boolean; + showInControlsMenu?: boolean; } export interface SnapshotVariableModel extends VariableWithOptions { diff --git a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts index 46b54c556fa..f8d5b84cceb 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2beta1/types.spec.gen.ts @@ -993,6 +993,7 @@ export interface QueryVariableSpec { refresh: VariableRefresh; skipUrlSync: boolean; description?: string; + showInControlsMenu?: boolean; query: DataQueryKind; regex: string; sort: VariableSort; @@ -1087,6 +1088,7 @@ export interface TextVariableSpec { hide: VariableHide; skipUrlSync: boolean; description?: string; + showInControlsMenu?: boolean; } export const defaultTextVariableSpec = (): TextVariableSpec => ({ @@ -1117,6 +1119,7 @@ export interface ConstantVariableSpec { hide: VariableHide; skipUrlSync: boolean; description?: string; + showInControlsMenu?: boolean; } export const defaultConstantVariableSpec = (): ConstantVariableSpec => ({ @@ -1154,6 +1157,7 @@ export interface DatasourceVariableSpec { skipUrlSync: boolean; description?: string; allowCustomValue: boolean; + showInControlsMenu?: boolean; } export const defaultDatasourceVariableSpec = (): DatasourceVariableSpec => ({ @@ -1195,6 +1199,7 @@ export interface IntervalVariableSpec { hide: VariableHide; skipUrlSync: boolean; description?: string; + showInControlsMenu?: boolean; } export const defaultIntervalVariableSpec = (): IntervalVariableSpec => ({ @@ -1235,6 +1240,7 @@ export interface CustomVariableSpec { skipUrlSync: boolean; description?: string; allowCustomValue: boolean; + showInControlsMenu?: boolean; } export const defaultCustomVariableSpec = (): CustomVariableSpec => ({ @@ -1276,6 +1282,7 @@ export interface GroupByVariableSpec { hide: VariableHide; skipUrlSync: boolean; description?: string; + showInControlsMenu?: boolean; } export const defaultGroupByVariableSpec = (): GroupByVariableSpec => ({ @@ -1314,6 +1321,7 @@ export interface AdhocVariableSpec { skipUrlSync: boolean; description?: string; allowCustomValue: boolean; + showInControlsMenu?: boolean; } export const defaultAdhocVariableSpec = (): AdhocVariableSpec => ({ diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index f957ee3dd21..f5099ba1b95 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -22,6 +22,7 @@ import { getDashboardSceneFor } from '../utils/utils'; import { DashboardLinksControls } from './DashboardLinksControls'; import { DashboardScene } from './DashboardScene'; +import { DropdownVariableControls } from './DropdownVariableControls'; import { VariableControls } from './VariableControls'; export interface DashboardControlsState extends SceneObjectState { @@ -151,6 +152,9 @@ function DashboardControlsRenderer({ model }: SceneComponentProps )} + + + {showDebugger && } ); diff --git a/public/app/features/dashboard-scene/scene/DropdownVariableControls.test.tsx b/public/app/features/dashboard-scene/scene/DropdownVariableControls.test.tsx new file mode 100644 index 00000000000..1e90d69fcc1 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/DropdownVariableControls.test.tsx @@ -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(); + 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(); + 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(); + + // 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(); + + // 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(); + + // 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, + }), + }); +} diff --git a/public/app/features/dashboard-scene/scene/DropdownVariableControls.tsx b/public/app/features/dashboard-scene/scene/DropdownVariableControls.tsx new file mode 100644 index 00000000000..756a1c3225a --- /dev/null +++ b/public/app/features/dashboard-scene/scene/DropdownVariableControls.tsx @@ -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 ( + { + e.stopPropagation(); + }} + > + {variables.map((variable) => ( +
+ +
+ ))} + + } + > + +
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + menuItem: css({ + padding: theme.spacing(0.5), + }), +}); diff --git a/public/app/features/dashboard-scene/scene/VariableControls.tsx b/public/app/features/dashboard-scene/scene/VariableControls.tsx index 0c99500c2cd..cbe5fde1470 100644 --- a/public/app/features/dashboard-scene/scene/VariableControls.tsx +++ b/public/app/features/dashboard-scene/scene/VariableControls.tsx @@ -8,13 +8,15 @@ import { useElementSelection, useStyles2 } from '@grafana/ui'; import { DashboardScene } from './DashboardScene'; export function VariableControls({ dashboard }: { dashboard: DashboardScene }) { - const variables = sceneGraph.getVariables(dashboard)!.useState(); + const { variables } = sceneGraph.getVariables(dashboard)!.useState(); return ( <> - {variables.variables.map((variable) => ( - - ))} + {variables + .filter((v) => !v.state.showInControlsMenu) + .map((variable) => ( + + ))} ); } diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts index 931ee847770..3a5a15f7f68 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.test.ts @@ -93,6 +93,7 @@ describe('sceneVariablesSetToVariables', () => { name: 'test', label: 'test-label', description: 'test-desc', + showInControlsMenu: true, value: ['selected-value'], text: ['selected-value-text'], datasource: { uid: 'fake-uid', type: 'fake-type' }, @@ -138,6 +139,7 @@ describe('sceneVariablesSetToVariables', () => { "query": "query", "refresh": 1, "regex": "", + "showInControlsMenu": true, "staticOptions": [ { "text": "test", @@ -155,6 +157,7 @@ describe('sceneVariablesSetToVariables', () => { name: 'test', label: 'test-label', description: 'test-desc', + showInControlsMenu: true, value: ['selected-value'], text: ['selected-value-text'], datasource: { uid: 'fake-uid', type: 'fake-type' }, @@ -200,6 +203,7 @@ describe('sceneVariablesSetToVariables', () => { "query": "query", "refresh": 1, "regex": "", + "showInControlsMenu": true, "staticOptions": [ { "text": "test", @@ -217,6 +221,7 @@ describe('sceneVariablesSetToVariables', () => { name: 'test', label: 'test-label', description: 'test-desc', + showInControlsMenu: true, value: ['selected-value'], text: ['selected-value-text'], datasource: { uid: 'fake-uid', type: 'fake-type' }, @@ -244,6 +249,7 @@ describe('sceneVariablesSetToVariables', () => { name: 'test', label: 'test-label', description: 'test-desc', + showInControlsMenu: true, value: ['test'], text: ['test'], datasource: { uid: 'fake-uid', type: 'fake-type' }, @@ -273,6 +279,7 @@ describe('sceneVariablesSetToVariables', () => { name: 'test', label: 'test-label', description: 'test-desc', + showInControlsMenu: true, value: ['selected-ds-1', 'selected-ds-2'], text: ['selected-ds-1-text', 'selected-ds-2-text'], pluginId: 'fake-std', @@ -311,6 +318,7 @@ describe('sceneVariablesSetToVariables', () => { "query": "fake-std", "refresh": 1, "regex": "", + "showInControlsMenu": true, "type": "datasource", } `); @@ -321,6 +329,7 @@ describe('sceneVariablesSetToVariables', () => { name: 'test', label: 'test-label', description: 'test-desc', + showInControlsMenu: true, value: ['test', 'test2'], text: ['test', 'test2'], query: 'test,test1,test2', @@ -378,6 +387,7 @@ describe('sceneVariablesSetToVariables', () => { }, ], "query": "test,test1,test2", + "showInControlsMenu": true, "type": "custom", } `); @@ -388,6 +398,7 @@ describe('sceneVariablesSetToVariables', () => { name: 'test', label: 'test-label', description: 'test-desc', + showInControlsMenu: true, value: 'constant value', skipUrlSync: true, }); @@ -409,6 +420,7 @@ describe('sceneVariablesSetToVariables', () => { "label": "test-label", "name": "test", "query": "constant value", + "showInControlsMenu": true, "skipUrlSync": true, "type": "constant", } @@ -420,6 +432,7 @@ describe('sceneVariablesSetToVariables', () => { name: 'test', label: 'test-label', description: 'test-desc', + showInControlsMenu: true, value: 'text value', skipUrlSync: true, }); @@ -447,6 +460,7 @@ describe('sceneVariablesSetToVariables', () => { }, ], "query": "text value", + "showInControlsMenu": true, "skipUrlSync": true, "type": "textbox", } @@ -458,6 +472,7 @@ describe('sceneVariablesSetToVariables', () => { intervals: ['1m', '2m', '3m', '1h', '1d'], value: '1m', refresh: VariableRefresh.onDashboardLoad, + showInControlsMenu: true, }); const set = new SceneVariableSet({ variables: [variable], @@ -506,6 +521,7 @@ describe('sceneVariablesSetToVariables', () => { ], "query": "1m,2m,3m,1h,1d", "refresh": 1, + "showInControlsMenu": true, "type": "interval", } `); @@ -517,6 +533,7 @@ describe('sceneVariablesSetToVariables', () => { allowCustomValue: true, label: 'test-label', description: 'test-desc', + showInControlsMenu: true, datasource: { uid: 'fake-uid', type: 'fake-type' }, filters: [ { @@ -565,6 +582,7 @@ describe('sceneVariablesSetToVariables', () => { ], "label": "test-label", "name": "test", + "showInControlsMenu": true, "type": "adhoc", } `); @@ -577,6 +595,7 @@ describe('sceneVariablesSetToVariables', () => { allowCustomValue: true, label: 'test-label', description: 'test-desc', + showInControlsMenu: true, datasource: { uid: 'fake-std', type: 'fake-std' }, originFilters: [ { @@ -608,6 +627,7 @@ describe('sceneVariablesSetToVariables', () => { "filters": [], "label": "test-label", "name": "test", + "showInControlsMenu": true, "type": "adhoc", } `); @@ -619,6 +639,7 @@ describe('sceneVariablesSetToVariables', () => { allowCustomValue: true, label: 'test-label', description: 'test-desc', + showInControlsMenu: true, datasource: { uid: 'fake-std', type: 'fake-std' }, originFilters: [ { @@ -668,6 +689,7 @@ describe('sceneVariablesSetToVariables', () => { ], "label": "test-label", "name": "test", + "showInControlsMenu": true, "type": "adhoc", } `); @@ -680,6 +702,7 @@ describe('sceneVariablesSetToVariables', () => { allowCustomValue: true, label: 'test-label', description: 'test-desc', + showInControlsMenu: true, datasource: { uid: 'fake-uid', type: 'fake-type' }, defaultKeys: [ { @@ -755,6 +778,7 @@ describe('sceneVariablesSetToVariables', () => { ], "label": "test-label", "name": "test", + "showInControlsMenu": true, "type": "adhoc", } `); @@ -775,6 +799,7 @@ describe('sceneVariablesSetToVariables', () => { label: 'test-label', description: 'test-desc', allowCustomValue: true, + showInControlsMenu: true, datasource: { uid: 'fake-uid', type: 'fake-type' }, defaultOptions: [ { @@ -819,6 +844,7 @@ describe('sceneVariablesSetToVariables', () => { "value": "bar", }, ], + "showInControlsMenu": true, "type": "groupby", } `); @@ -831,6 +857,7 @@ describe('sceneVariablesSetToVariables', () => { name: 'test', label: 'test-label', description: 'test-desc', + showInControlsMenu: true, datasource: { uid: 'fake-uid', type: 'fake-type' }, defaultOptions: [ { @@ -866,6 +893,7 @@ describe('sceneVariablesSetToVariables', () => { isMulti: true, staticOptions: [{ label: 'test', value: 'test' }], staticOptionsOrder: 'after', + showInControlsMenu: true, }); const set = new SceneVariableSet({ @@ -910,6 +938,7 @@ describe('sceneVariablesSetToVariables', () => { }, "refresh": "onDashboardLoad", "regex": "", + "showInControlsMenu": true, "skipUrlSync": false, "sort": "disabled", "staticOptions": [ @@ -932,6 +961,7 @@ describe('sceneVariablesSetToVariables', () => { value: ['test', 'test2'], text: ['test', 'test2'], query: 'test,test1,test2', + showInControlsMenu: true, options: [ { label: 'test', value: 'test' }, { label: 'test1', value: 'test1' }, @@ -988,6 +1018,7 @@ describe('sceneVariablesSetToVariables', () => { }, ], "query": "test,test1,test2", + "showInControlsMenu": true, "skipUrlSync": false, }, } @@ -1005,6 +1036,7 @@ describe('sceneVariablesSetToVariables', () => { includeAll: true, allValue: 'test-all', isMulti: true, + showInControlsMenu: true, }); const set = new SceneVariableSet({ variables: [variable], @@ -1039,6 +1071,7 @@ describe('sceneVariablesSetToVariables', () => { "pluginId": "fake-std", "refresh": "onDashboardLoad", "regex": "", + "showInControlsMenu": true, "skipUrlSync": false, }, } @@ -1051,6 +1084,7 @@ describe('sceneVariablesSetToVariables', () => { label: 'test-label', description: 'test-desc', value: 'constant value', + showInControlsMenu: true, skipUrlSync: true, }); const set = new SceneVariableSet({ @@ -1073,6 +1107,7 @@ describe('sceneVariablesSetToVariables', () => { "label": "test-label", "name": "test", "query": "constant value", + "showInControlsMenu": true, "skipUrlSync": true, }, } @@ -1085,6 +1120,7 @@ describe('sceneVariablesSetToVariables', () => { label: 'test-label', description: 'test-desc', value: 'text value', + showInControlsMenu: true, skipUrlSync: true, }); const set = new SceneVariableSet({ @@ -1107,6 +1143,7 @@ describe('sceneVariablesSetToVariables', () => { "label": "test-label", "name": "test", "query": "text value", + "showInControlsMenu": true, "skipUrlSync": true, }, } @@ -1117,6 +1154,7 @@ describe('sceneVariablesSetToVariables', () => { const variable = new IntervalVariable({ intervals: ['1m', '2m', '3m', '1h', '1d'], value: '1m', + showInControlsMenu: true, refresh: VariableRefresh.onDashboardLoad, }); const set = new SceneVariableSet({ @@ -1169,6 +1207,7 @@ describe('sceneVariablesSetToVariables', () => { ], "query": "1m,2m,3m,1h,1d", "refresh": "onTimeRangeChanged", + "showInControlsMenu": true, "skipUrlSync": false, }, } @@ -1180,6 +1219,7 @@ describe('sceneVariablesSetToVariables', () => { name: 'test', label: 'test-label', description: 'test-desc', + showInControlsMenu: true, datasource: { uid: 'fake-uid', type: 'fake-type' }, filters: [ { @@ -1231,6 +1271,7 @@ describe('sceneVariablesSetToVariables', () => { "hide": "dontHide", "label": "test-label", "name": "test", + "showInControlsMenu": true, "skipUrlSync": false, }, } @@ -1242,6 +1283,7 @@ describe('sceneVariablesSetToVariables', () => { name: 'test', label: 'test-label', description: 'test-desc', + showInControlsMenu: true, datasource: { uid: 'fake-uid', type: 'fake-type' }, defaultKeys: [ { @@ -1320,6 +1362,7 @@ describe('sceneVariablesSetToVariables', () => { "hide": "dontHide", "label": "test-label", "name": "test", + "showInControlsMenu": true, "skipUrlSync": false, }, } @@ -1340,6 +1383,7 @@ describe('sceneVariablesSetToVariables', () => { name: 'test', label: 'test-label', description: 'test-desc', + showInControlsMenu: true, datasource: { uid: 'fake-uid', type: 'fake-type' }, defaultOptions: [ { @@ -1387,6 +1431,7 @@ describe('sceneVariablesSetToVariables', () => { "value": "bar", }, ], + "showInControlsMenu": true, "skipUrlSync": false, }, } @@ -1400,6 +1445,7 @@ describe('sceneVariablesSetToVariables', () => { name: 'test', label: 'test-label', description: 'test-desc', + showInControlsMenu: true, datasource: { uid: 'fake-uid', type: 'fake-type' }, defaultOptions: [ { diff --git a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts index 87cde15a7ea..a7668e681d1 100644 --- a/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts +++ b/public/app/features/dashboard-scene/serialization/sceneVariablesSetToVariables.ts @@ -60,6 +60,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio skipUrlSync: Boolean(variable.state.skipUrlSync), hide: variable.state.hide || OldVariableHide.dontHide, type: variable.state.type, + showInControlsMenu: variable.state.showInControlsMenu, }; if (sceneUtils.isQueryVariable(variable)) { @@ -283,6 +284,7 @@ export function sceneVariablesSetToSchemaV2Variables( description: variable.state.description ?? undefined, skipUrlSync: Boolean(variable.state.skipUrlSync), hide: transformVariableHideToEnum(variable.state.hide) || defaultVariableHide(), + showInControlsMenu: variable.state.showInControlsMenu, }; // current: VariableOption; diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 2a4c05839c2..94f7792592c 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -273,6 +273,7 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S name: variable.spec.name, label: variable.spec.label, description: variable.spec.description, + showInControlsMenu: variable.spec.showInControlsMenu, }; if (variable.kind === defaultAdhocVariableKind().kind) { const ds = getDataSourceForQuery( diff --git a/public/app/features/dashboard-scene/utils/variables.ts b/public/app/features/dashboard-scene/utils/variables.ts index 6b6eeae5e77..b42b7e7fadf 100644 --- a/public/app/features/dashboard-scene/utils/variables.ts +++ b/public/app/features/dashboard-scene/utils/variables.ts @@ -131,6 +131,7 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode name: variable.name, label: variable.label, description: variable.description, + showInControlsMenu: variable.showInControlsMenu, }; if (variable.type === 'adhoc') { const originFilters: AdHocVariableFilter[] = []; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 08933ca4737..4ecbce8fd2b 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -4562,6 +4562,12 @@ "overwrite": "Overwrite", "title-plugin-dashboard": "Plugin dashboard" }, + "controls": { + "menu": { + "aria-label": "", + "title": "" + } + }, "dash-nav": { "on-open-snapshot-original": { "confirmText": {