Dashboard: Add variable button in dashboard controls (#112387)

This commit is contained in:
Bogdan Matei
2025-10-21 09:16:09 +00:00
committed by GitHub
parent d1b9543189
commit 143ea45ea6
7 changed files with 177 additions and 68 deletions
@@ -17,6 +17,7 @@ import { SceneGridRowEditableElement } from '../scene/layout-default/SceneGridRo
import { redoButtonId, undoButtonID } from '../scene/new-toolbar/RightActions';
import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types/EditableDashboardElement';
import { LocalVariableEditableElement } from '../settings/variables/LocalVariableEditableElement';
import { VariableAdd, VariableAddEditableElement } from '../settings/variables/VariableAddEditableElement';
import { VariableEditableElement } from '../settings/variables/VariableEditableElement';
import { VariableSetEditableElement } from '../settings/variables/VariableSetEditableElement';
import { isSceneVariable } from '../settings/variables/utils';
@@ -61,6 +62,10 @@ export function getEditableElementFor(sceneObj: SceneObject | undefined): Editab
return new VariableEditableElement(sceneObj);
}
if (sceneObj instanceof VariableAdd) {
return new VariableAddEditableElement(sceneObj);
}
return undefined;
}
@@ -202,7 +207,7 @@ export const dashboardEditActions = {
}),
addVariable({ source, addedObject }: AddVariableActionHelperProps) {
const varsBeforeAddition = [...source.state.variables];
const varsBeforeAddition = [...(source.state.variables ?? [])];
dashboardEditActions.addElement({
source,
@@ -2,6 +2,7 @@ import { css, cx } from '@emotion/css';
import { VariableHide, GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config } from '@grafana/runtime';
import {
sceneGraph,
useSceneObjectState,
@@ -14,6 +15,7 @@ import {
import { useElementSelection, useStyles2 } from '@grafana/ui';
import { DashboardScene } from './DashboardScene';
import { AddVariableButton } from './VariableControlsAddButton';
export function VariableControls({ dashboard }: { dashboard: DashboardScene }) {
const { variables } = sceneGraph.getVariables(dashboard)!.useState();
@@ -25,6 +27,7 @@ export function VariableControls({ dashboard }: { dashboard: DashboardScene }) {
.map((variable) => (
<VariableValueSelectWrapper key={variable.state.key} variable={variable} />
))}
{config.featureToggles.dashboardNewLayouts ? <AddVariableButton dashboard={dashboard} /> : null}
</>
);
}
@@ -0,0 +1,32 @@
import { PointerEventHandler, useCallback } from 'react';
import { Trans } from '@grafana/i18n';
import { Button } from '@grafana/ui';
import { openAddVariablePane } from '../settings/variables/VariableAddEditableElement';
import { DashboardInteractions } from '../utils/interactions';
import { DashboardScene } from './DashboardScene';
export function AddVariableButton({ dashboard }: { dashboard: DashboardScene }) {
const { isEditing } = dashboard.useState();
const handlePointerDown: PointerEventHandler = useCallback(
(evt) => {
evt.stopPropagation();
openAddVariablePane(dashboard);
DashboardInteractions.addVariableButtonClicked({ source: 'variable_controls' });
},
[dashboard]
);
if (!isEditing) {
return null;
}
return (
<Button icon="plus" variant="primary" fill="text" onPointerDown={handlePointerDown}>
<Trans i18nKey="dashboard-scene.variable-controls.add-variable">Add</Trans>
</Button>
);
}
@@ -0,0 +1,116 @@
import { css } from '@emotion/css';
import { useCallback, useId, useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { sceneGraph, SceneObjectBase, SceneObjectRef, SceneObjectState, SceneVariableSet } from '@grafana/scenes';
import { Box, Card, Stack, useStyles2 } from '@grafana/ui';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
import { dashboardEditActions } from '../../edit-pane/shared';
import { DashboardScene } from '../../scene/DashboardScene';
import { EditableDashboardElement, EditableDashboardElementInfo } from '../../scene/types/EditableDashboardElement';
import { EditableVariableType, getNextAvailableId, getVariableScene, getVariableTypeSelectOptions } from './utils';
export function openAddVariablePane(dashboard: DashboardScene) {
const element = new VariableAdd({ dashboardRef: dashboard.getRef() });
dashboard.state.editPane.selectObject(element, element.state.key!, { force: true, multi: false });
}
export interface VariableAddState extends SceneObjectState {
dashboardRef: SceneObjectRef<DashboardScene>;
}
export class VariableAdd extends SceneObjectBase<VariableAddState> {}
function useEditPaneOptions(
this: VariableAddEditableElement,
variableAdd: VariableAdd
): OptionsPaneCategoryDescriptor[] {
const id = useId();
const options = useMemo(() => {
return new OptionsPaneCategoryDescriptor({ title: '', id: 'variables' }).addItem(
new OptionsPaneItemDescriptor({
title: '',
id,
skipField: true,
render: () => <VariableTypeSelection variableAdd={variableAdd} />,
})
);
}, [variableAdd, id]);
return [options];
}
export class VariableAddEditableElement implements EditableDashboardElement {
public readonly isEditableDashboardElement = true;
public readonly typeName = 'Variable';
public constructor(private variableAdd: VariableAdd) {}
public getEditableElementInfo(): EditableDashboardElementInfo {
return {
typeName: t('dashboard.edit-pane.elements.variable-set', 'Variables'),
icon: 'x',
instanceName: t('dashboard.edit-pane.elements.variable-set', 'Variables'),
};
}
public useEditPaneOptions = useEditPaneOptions.bind(this, this.variableAdd);
}
function VariableTypeSelection({ variableAdd }: { variableAdd: VariableAdd }) {
const options = useMemo(() => getVariableTypeSelectOptions(), []);
const styles = useStyles2(getStyles);
const onAddVariable = useCallback(
(type: EditableVariableType) => {
const dashboard = variableAdd.state.dashboardRef.resolve();
const variablesSet = sceneGraph.getVariables(dashboard);
if (!(variablesSet instanceof SceneVariableSet)) {
return;
}
const newVar = getVariableScene(type, { name: getNextAvailableId(type, variablesSet.state.variables ?? []) });
dashboardEditActions.addVariable({ source: variablesSet, addedObject: newVar });
dashboard.state.editPane.selectObject(newVar, newVar.state.key!, { force: true, multi: false });
},
[variableAdd]
);
return (
<Stack direction="column" gap={0}>
<Box paddingBottom={1} display={'flex'}>
<Trans i18nKey="dashboard.edit-pane.variables.select-type">Choose variable type</Trans>
</Box>
<Stack direction="column" gap={1}>
{options.map((option) => (
<Card
noMargin
isCompact
onClick={() => onAddVariable(option.value!)}
key={option.value}
title={t('dashboard.edit-pane.variables.select-type-card-tooltip', 'Click to select type')}
data-testid={selectors.components.PanelEditor.ElementEditPane.variableType(option.value!)}
>
<Card.Heading>{option.label}</Card.Heading>
<Card.Description className={styles.cardDescription}>{option.description}</Card.Description>
</Card>
))}
</Stack>
</Stack>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
cardDescription: css({
fontSize: theme.typography.bodySmall.fontSize,
marginTop: theme.spacing(0),
}),
};
}
@@ -1,22 +1,20 @@
import { css } from '@emotion/css';
import { useId, useMemo } from 'react';
import { useToggle } from 'react-use';
import { useCallback, useId, useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans, t } from '@grafana/i18n';
import { SceneVariable, SceneVariableSet } from '@grafana/scenes';
import { Box, Button, Card, Stack, Text, useStyles2 } from '@grafana/ui';
import { Box, Button, Stack, Text, useStyles2 } from '@grafana/ui';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
import { dashboardEditActions } from '../../edit-pane/shared';
import { DashboardScene } from '../../scene/DashboardScene';
import { EditableDashboardElement, EditableDashboardElementInfo } from '../../scene/types/EditableDashboardElement';
import { DashboardInteractions } from '../../utils/interactions';
import { getDashboardSceneFor } from '../../utils/utils';
import { EditableVariableType, getNextAvailableId, getVariableScene, getVariableTypeSelectOptions } from './utils';
import { openAddVariablePane } from './VariableAddEditableElement';
function useEditPaneOptions(this: VariableSetEditableElement, set: SceneVariableSet): OptionsPaneCategoryDescriptor[] {
const variableListId = useId();
@@ -33,6 +31,7 @@ function useEditPaneOptions(this: VariableSetEditableElement, set: SceneVariable
return [options];
}
export class VariableSetEditableElement implements EditableDashboardElement {
public readonly isEditableDashboardElement = true;
public readonly typeName = 'Variable';
@@ -57,30 +56,20 @@ export class VariableSetEditableElement implements EditableDashboardElement {
export function VariableList({ set }: { set: SceneVariableSet }) {
const { variables } = set.useState();
const styles = useStyles2(getStyles);
const [isAdding, setIsAdding] = useToggle(false);
const canAdd = set.parent instanceof DashboardScene;
const onEditVariable = (variable: SceneVariable) => {
const { editPane } = getDashboardSceneFor(set).state;
editPane.selectObject(variable, variable.state.key!);
};
const onEditVariable = useCallback(
(variable: SceneVariable) => {
const { editPane } = getDashboardSceneFor(set).state;
editPane.selectObject(variable, variable.state.key!);
},
[set]
);
const onAddVariable = (type: EditableVariableType) => {
const { variables } = set.state;
const nextName = getNextAvailableId(type, variables);
const newVar = getVariableScene(type, { name: nextName });
dashboardEditActions.addVariable({
source: set,
addedObject: newVar,
});
setIsAdding(false);
};
if (isAdding) {
return <VariableTypeSelection onAddVariable={onAddVariable} />;
}
const onAddVariable = useCallback(() => {
openAddVariablePane(getDashboardSceneFor(set));
DashboardInteractions.addVariableButtonClicked({ source: 'edit_pane' });
}, [set]);
return (
<Stack direction="column" gap={0}>
@@ -103,10 +92,7 @@ export function VariableList({ set }: { set: SceneVariableSet }) {
icon="plus"
size="sm"
variant="secondary"
onClick={() => {
DashboardInteractions.addVariableButtonClicked({ source: 'edit_pane' });
setIsAdding();
}}
onClick={onAddVariable}
data-testid={selectors.components.PanelEditor.ElementEditPane.addVariableButton}
>
<Trans i18nKey="dashboard.edit-pane.variables.add-variable">Add variable</Trans>
@@ -117,38 +103,6 @@ export function VariableList({ set }: { set: SceneVariableSet }) {
);
}
interface VariableTypeSelectionProps {
onAddVariable: (type: EditableVariableType) => void;
}
function VariableTypeSelection({ onAddVariable }: VariableTypeSelectionProps) {
const options = getVariableTypeSelectOptions();
const styles = useStyles2(getStyles);
return (
<Stack direction={'column'} gap={0}>
<Box paddingBottom={1} display={'flex'}>
<Trans i18nKey="dashboard.edit-pane.variables.select-type">Choose variable type</Trans>
</Box>
<Stack direction="column">
{options.map((option) => (
<Card
isCompact
noMargin
onClick={() => onAddVariable(option.value!)}
key={option.value}
title={t('dashboard.edit-pane.variables.select-type-card-tooltip', 'Click to select type')}
data-testid={selectors.components.PanelEditor.ElementEditPane.variableType(option.value!)}
>
<Card.Heading>{option.label}</Card.Heading>
<Card.Description className={styles.cardDescription}>{option.description}</Card.Description>
</Card>
))}
</Stack>
</Stack>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
variableItem: css({
@@ -177,9 +131,5 @@ function getStyles(theme: GrafanaTheme2) {
},
},
}),
cardDescription: css({
fontSize: theme.typography.bodySmall.fontSize,
marginTop: theme.spacing(0),
}),
};
}
@@ -69,7 +69,7 @@ export const DashboardInteractions = {
// dashboards_add_variable_button_clicked
// when a user clicks on ‘Add Variable’ or ‘New Variable’
addVariableButtonClicked: (properties: { source: 'edit_pane' | 'settings_pane' }) => {
addVariableButtonClicked: (properties: { source: 'edit_pane' | 'settings_pane' | 'variable_controls' }) => {
reportDashboardInteraction('add_variable_button_clicked', properties);
},
+3
View File
@@ -6294,6 +6294,9 @@
"content-variable-not-referenced-other-variables-dashboard": "This variable is not referenced by other variables or dashboard.",
"content-variable-referenced-other-variables-dashboard": "This variable is referenced by other variables or dashboard."
},
"variable-controls": {
"add-variable": "Add"
},
"variable-editor-form": {
"aria-label-variable-editor-form": "Variable editor form",
"back-to-list": "Back to list",