From 5aa358c481590a9f52d7c8ff07d94672bc97466c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 4 Apr 2025 11:49:04 +0200 Subject: [PATCH] Dashboard: Variable controls via simple react component (#103442) * Dashboard: Variable controls refactor * Update tests * Fix name * fix lint --- .../scene/DashboardControls.test.tsx | 8 +-- .../scene/DashboardControls.tsx | 27 +++++-- .../scene/VariableControls.tsx | 70 +++++++++++++++++++ .../transformSaveModelSchemaV2ToScene.ts | 3 - .../transformSaveModelToScene.test.ts | 4 -- .../transformSaveModelToScene.ts | 3 - 6 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/VariableControls.tsx diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx index 878f6cc5e87..dc4c492bf92 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.test.tsx @@ -1,7 +1,7 @@ import { render } from '@testing-library/react'; import { selectors } from '@grafana/e2e-selectors'; -import { SceneDataLayerControls, SceneVariableSet, TextBoxVariable, VariableValueSelectors } from '@grafana/scenes'; +import { SceneVariableSet, TextBoxVariable } from '@grafana/scenes'; import { DashboardControls, DashboardControlsState } from './DashboardControls'; import { DashboardScene } from './DashboardScene'; @@ -10,7 +10,6 @@ describe('DashboardControls', () => { describe('Given a standard scene', () => { it('should initialize with default values', () => { const scene = buildTestScene(); - expect(scene.state.variableControls).toEqual([]); expect(scene.state.timePicker).toBeDefined(); expect(scene.state.refreshPicker).toBeDefined(); }); @@ -38,9 +37,7 @@ describe('DashboardControls', () => { }); it('should render visible controls', async () => { - const scene = buildTestScene({ - variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()], - }); + const scene = buildTestScene({}); const renderer = render(); expect(await renderer.findByTestId(selectors.pages.Dashboard.Controls)).toBeInTheDocument(); @@ -55,7 +52,6 @@ describe('DashboardControls', () => { hideTimeControls: true, hideVariableControls: true, hideLinksControls: true, - variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()], }); const renderer = render(); diff --git a/public/app/features/dashboard-scene/scene/DashboardControls.tsx b/public/app/features/dashboard-scene/scene/DashboardControls.tsx index 9b1f95097d0..423d3e3e054 100644 --- a/public/app/features/dashboard-scene/scene/DashboardControls.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardControls.tsx @@ -4,7 +4,6 @@ import { GrafanaTheme2, VariableHide } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { SceneObjectState, - SceneObject, SceneObjectBase, SceneComponentProps, SceneTimePicker, @@ -22,9 +21,10 @@ import { PanelEditControls } from '../panel-edit/PanelEditControls'; import { getDashboardSceneFor } from '../utils/utils'; import { DashboardLinksControls } from './DashboardLinksControls'; +import { DashboardScene } from './DashboardScene'; +import { VariableControls } from './VariableControls'; export interface DashboardControlsState extends SceneObjectState { - variableControls: SceneObject[]; timePicker: SceneTimePicker; refreshPicker: SceneRefreshPicker; hideTimeControls?: boolean; @@ -73,7 +73,6 @@ export class DashboardControls extends SceneObjectBase { public constructor(state: Partial) { super({ - variableControls: [], timePicker: state.timePicker ?? new SceneTimePicker({}), refreshPicker: state.refreshPicker ?? new SceneRefreshPicker({}), ...state, @@ -119,8 +118,7 @@ export class DashboardControls extends SceneObjectBase { } function DashboardControlsRenderer({ model }: SceneComponentProps) { - const { variableControls, refreshPicker, timePicker, hideTimeControls, hideVariableControls, hideLinksControls } = - model.useState(); + const { refreshPicker, timePicker, hideTimeControls, hideVariableControls, hideLinksControls } = model.useState(); const dashboard = getDashboardSceneFor(model); const { links, editPanel } = dashboard.useState(); const styles = useStyles2(getStyles); @@ -137,7 +135,12 @@ function DashboardControlsRenderer({ model }: SceneComponentProps - {!hideVariableControls && variableControls.map((c) => )} + {!hideVariableControls && ( + <> + + + + )} {!hideLinksControls && !editPanel && } {editPanel && } @@ -153,6 +156,18 @@ function DashboardControlsRenderer({ model }: SceneComponentProps + {layers.map((layer) => ( + + ))} + + ); +} + function getStyles(theme: GrafanaTheme2) { return { controls: css({ diff --git a/public/app/features/dashboard-scene/scene/VariableControls.tsx b/public/app/features/dashboard-scene/scene/VariableControls.tsx new file mode 100644 index 00000000000..dc4dc9653a5 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/VariableControls.tsx @@ -0,0 +1,70 @@ +import { css } from '@emotion/css'; + +import { VariableHide } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { sceneGraph, useSceneObjectState, SceneVariable, SceneVariableState, ControlsLabel } from '@grafana/scenes'; + +import { DashboardScene } from './DashboardScene'; + +export function VariableControls({ dashboard }: { dashboard: DashboardScene }) { + const variables = sceneGraph.getVariables(dashboard)!.useState(); + + return ( + <> + {variables.variables.map((variable) => ( + + ))} + + ); +} + +interface VariableSelectProps { + variable: SceneVariable; +} + +export function VariableValueSelectWrapper({ variable }: VariableSelectProps) { + const state = useSceneObjectState(variable, { shouldActivateOrKeepAlive: true }); + + if (state.hide === VariableHide.hideVariable) { + return null; + } + + return ( +
+ + +
+ ); +} + +function VariableLabel({ variable }: VariableSelectProps) { + const { state } = variable; + + if (variable.state.hide === VariableHide.hideLabel) { + return null; + } + + const labelOrName = state.label || state.name; + const elementId = `var-${state.key}`; + + return ( + variable.onCancel?.()} + label={labelOrName} + error={state.error} + layout={'horizontal'} + description={state.description ?? undefined} + /> + ); +} + +const containerStyle = css({ + display: 'flex', + // No border for second element (inputs) as label and input border is shared + '> :nth-child(2)': css({ + borderTopLeftRadius: 0, + borderBottomLeftRadius: 0, + }), +}); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts index 6cb7c7462c1..10ccc34252f 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelSchemaV2ToScene.ts @@ -10,14 +10,12 @@ import { GroupByVariable, IntervalVariable, QueryVariable, - SceneDataLayerControls, SceneRefreshPicker, SceneTimePicker, SceneTimeRange, SceneVariable, SceneVariableSet, TextBoxVariable, - VariableValueSelectors, } from '@grafana/scenes'; import { AdhocVariableKind, @@ -191,7 +189,6 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo { const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as DashboardDataDTO, meta: {} }); expect(scene.state.$data).toBeInstanceOf(DashboardDataLayerSet); - expect(scene.state.controls!.state.variableControls[1]).toBeInstanceOf(SceneDataLayerControls); const dataLayers = scene.state.$data as DashboardDataLayerSet; expect(dataLayers.state.annotationLayers).toHaveLength(4); @@ -864,7 +862,6 @@ describe('transformSaveModelToScene', () => { const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as DashboardDataDTO, meta: {} }); expect(scene.state.$data).toBeInstanceOf(DashboardDataLayerSet); - expect(scene.state.controls!.state.variableControls[1]).toBeInstanceOf(SceneDataLayerControls); const dataLayers = scene.state.$data as DashboardDataLayerSet; expect(dataLayers.state.alertStatesLayer).toBeDefined(); @@ -877,7 +874,6 @@ describe('transformSaveModelToScene', () => { const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as DashboardDataDTO, meta: {} }); expect(scene.state.$data).toBeInstanceOf(DashboardDataLayerSet); - expect(scene.state.controls!.state.variableControls[1]).toBeInstanceOf(SceneDataLayerControls); const dataLayers = scene.state.$data as DashboardDataLayerSet; expect(dataLayers.state.alertStatesLayer).toBeDefined(); diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index aaed4127842..0b7435c1fe9 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -9,7 +9,6 @@ import { SceneGridRow, SceneTimeRange, SceneVariableSet, - VariableValueSelectors, SceneRefreshPicker, SceneObject, VizPanelMenu, @@ -17,7 +16,6 @@ import { VizPanelState, SceneGridItemLike, SceneDataLayerProvider, - SceneDataLayerControls, UserActionEvent, SceneInteractionProfileEvent, SceneObjectState, @@ -278,7 +276,6 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, $behaviors: behaviorList, $data: new DashboardDataLayerSet({ annotationLayers, alertStatesLayer }), controls: new DashboardControls({ - variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()], timePicker: new SceneTimePicker({ quickRanges: oldModel.timepicker.quick_ranges, }),