Dashboard: Variable controls via simple react component (#103442)

* Dashboard: Variable controls refactor

* Update tests

* Fix name

* fix lint
This commit is contained in:
Torkel Ödegaard
2025-04-04 11:49:04 +02:00
committed by GitHub
parent 48877f9187
commit 5aa358c481
6 changed files with 93 additions and 22 deletions

View File

@ -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(<scene.Component model={scene} />);
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(<scene.Component model={scene} />);

View File

@ -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<DashboardControlsState> {
public constructor(state: Partial<DashboardControlsState>) {
super({
variableControls: [],
timePicker: state.timePicker ?? new SceneTimePicker({}),
refreshPicker: state.refreshPicker ?? new SceneRefreshPicker({}),
...state,
@ -119,8 +118,7 @@ export class DashboardControls extends SceneObjectBase<DashboardControlsState> {
}
function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardControls>) {
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<DashboardContr
className={cx(styles.controls, editPanel && styles.controlsPanelEdit)}
>
<Stack grow={1} wrap={'wrap'}>
{!hideVariableControls && variableControls.map((c) => <c.Component model={c} key={c.state.key} />)}
{!hideVariableControls && (
<>
<VariableControls dashboard={dashboard} />
<DataLayerControls dashboard={dashboard} />
</>
)}
<Box grow={1} />
{!hideLinksControls && !editPanel && <DashboardLinksControls links={links} dashboard={dashboard} />}
{editPanel && <PanelEditControls panelEditor={editPanel} />}
@ -153,6 +156,18 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
);
}
function DataLayerControls({ dashboard }: { dashboard: DashboardScene }) {
const layers = sceneGraph.getDataLayers(dashboard, true);
return (
<>
{layers.map((layer) => (
<layer.Component model={layer} key={layer.state.key} />
))}
</>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
controls: css({

View File

@ -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) => (
<VariableValueSelectWrapper key={variable.state.key} variable={variable} />
))}
</>
);
}
interface VariableSelectProps {
variable: SceneVariable;
}
export function VariableValueSelectWrapper({ variable }: VariableSelectProps) {
const state = useSceneObjectState<SceneVariableState>(variable, { shouldActivateOrKeepAlive: true });
if (state.hide === VariableHide.hideVariable) {
return null;
}
return (
<div className={containerStyle} data-testid={selectors.pages.Dashboard.SubMenu.submenuItem}>
<VariableLabel variable={variable} />
<variable.Component model={variable} />
</div>
);
}
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 (
<ControlsLabel
htmlFor={elementId}
isLoading={state.loading}
onCancel={() => 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,
}),
});

View File

@ -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<D
annotationLayers,
}),
controls: new DashboardControls({
variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()],
timePicker: new SceneTimePicker({
quickRanges: dashboard.timeSettings.quickRanges,
}),

View File

@ -5,7 +5,6 @@ import {
AdHocFiltersVariable,
behaviors,
ConstantVariable,
SceneDataLayerControls,
SceneDataTransformer,
SceneGridLayout,
SceneGridRow,
@ -836,7 +835,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.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();

View File

@ -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,
}),