Dashboard: Variable selection and editing (#103451)

* Variable selection

* Update

* fix

* Update

* Updates

* update

* Update
This commit is contained in:
Torkel Ödegaard
2025-04-09 14:27:35 +02:00
committed by GitHub
parent 5698159ee3
commit 927ce79dcf
10 changed files with 230 additions and 35 deletions
@@ -41,7 +41,7 @@ describe('Variables - Query - Add variable', () => {
expect(input.attr('placeholder')).equals('Descriptive text');
expect(input.val()).equals('');
});
cy.get('label').contains('Show on dashboard').should('be.visible');
cy.get('label').contains('Hide').should('be.visible');
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect()
.get('input[placeholder="gdev-testdata"]')
@@ -37,7 +37,7 @@ describe('Variables - Query - Add variable', () => {
expect(input.attr('placeholder')).equals('Descriptive text');
expect(input.val()).equals('');
});
cy.get('label').contains('Show on dashboard').should('be.visible');
cy.get('label').contains('Hide').should('be.visible');
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect()
.get('input[placeholder="gdev-testdata"]')
+2 -2
View File
@@ -275,8 +275,8 @@
"@grafana/prometheus": "workspace:*",
"@grafana/runtime": "workspace:*",
"@grafana/saga-icons": "workspace:*",
"@grafana/scenes": "6.6.3",
"@grafana/scenes-react": "6.6.3",
"@grafana/scenes": "^6.7.0",
"@grafana/scenes-react": "^6.7.0",
"@grafana/schema": "workspace:*",
"@grafana/sql": "workspace:*",
"@grafana/ui": "workspace:*",
@@ -1,11 +1,12 @@
import { useSessionStorage } from 'react-use';
import { BusEventWithPayload } from '@grafana/data';
import { SceneGridRow, SceneObject, VizPanel } from '@grafana/scenes';
import { SceneGridRow, SceneObject, SceneVariable, VizPanel } from '@grafana/scenes';
import { DashboardScene } from '../scene/DashboardScene';
import { SceneGridRowEditableElement } from '../scene/layout-default/SceneGridRowEditableElement';
import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types/EditableDashboardElement';
import { VariableEditableElement } from '../settings/variables/VariableEditableElement';
import { DashboardEditableElement } from './DashboardEditableElement';
import { VizPanelEditableElement } from './VizPanelEditableElement';
@@ -35,6 +36,10 @@ export function getEditableElementFor(sceneObj: SceneObject | undefined): Editab
return new DashboardEditableElement(sceneObj);
}
if (isSceneVariable(sceneObj)) {
return new VariableEditableElement(sceneObj);
}
return undefined;
}
@@ -55,6 +60,10 @@ export function hasEditableElement(sceneObj: SceneObject | undefined): boolean {
return false;
}
export function isSceneVariable(sceneObj: SceneObject): sceneObj is SceneVariable {
return 'getValue' in sceneObj;
}
export class NewObjectAddedToCanvasEvent extends BusEventWithPayload<SceneObject> {
static type = 'new-object-added-to-canvas';
}
@@ -1,8 +1,9 @@
import { css } from '@emotion/css';
import { css, cx } from '@emotion/css';
import { VariableHide } from '@grafana/data';
import { VariableHide, GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { sceneGraph, useSceneObjectState, SceneVariable, SceneVariableState, ControlsLabel } from '@grafana/scenes';
import { useElementSelection, useStyles2 } from '@grafana/ui';
import { DashboardScene } from './DashboardScene';
@@ -24,20 +25,37 @@ interface VariableSelectProps {
export function VariableValueSelectWrapper({ variable }: VariableSelectProps) {
const state = useSceneObjectState<SceneVariableState>(variable, { shouldActivateOrKeepAlive: true });
const { isSelected, onSelect, isSelectable } = useElementSelection(variable.state.key);
const styles = useStyles2(getStyles);
if (state.hide === VariableHide.hideVariable) {
return null;
}
const onPointerDown = (evt: React.PointerEvent) => {
if (isSelectable && onSelect) {
evt.stopPropagation();
onSelect(evt);
}
};
return (
<div className={containerStyle} data-testid={selectors.pages.Dashboard.SubMenu.submenuItem}>
<VariableLabel variable={variable} />
<div
className={cx(
styles.container,
isSelected && 'dashboard-selected-element',
isSelectable && !isSelected && 'dashboard-selectable-element'
)}
onPointerDown={onPointerDown}
data-testid={selectors.pages.Dashboard.SubMenu.submenuItem}
>
<VariableLabel variable={variable} className={cx(isSelectable && styles.labelSelectable)} />
<variable.Component model={variable} />
</div>
);
}
function VariableLabel({ variable }: VariableSelectProps) {
function VariableLabel({ variable, className }: { variable: SceneVariable; className?: string }) {
const { state } = variable;
if (variable.state.hide === VariableHide.hideLabel) {
@@ -56,15 +74,25 @@ function VariableLabel({ variable }: VariableSelectProps) {
error={state.error}
layout={'horizontal'}
description={state.description ?? undefined}
className={className}
/>
);
}
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,
const getStyles = (theme: GrafanaTheme2) => ({
container: css({
display: 'flex',
// No border for second element (inputs) as label and input border is shared
'> :nth-child(2)': css({
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
}),
}),
labelWrapper: css({
display: 'flex',
alignItems: 'center',
}),
labelSelectable: css({
cursor: 'pointer',
}),
});
@@ -0,0 +1,151 @@
import { useMemo } from 'react';
import { VariableHide } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { SceneVariable, SceneVariableSet } from '@grafana/scenes';
import { Combobox, Input, TextArea, Stack, Button } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
import { ObjectRemovedFromCanvasEvent } from '../../edit-pane/shared';
import { BulkActionElement } from '../../scene/types/BulkActionElement';
import { EditableDashboardElement, EditableDashboardElementInfo } from '../../scene/types/EditableDashboardElement';
import { VariableHideSelect } from '../../settings/variables/components/VariableHideSelect';
import { getVariableTypeSelectOptions } from '../../settings/variables/utils';
export class VariableEditableElement implements EditableDashboardElement, BulkActionElement {
public readonly isEditableDashboardElement = true;
public readonly typeName = 'Variable';
public constructor(public variable: SceneVariable) {}
public getEditableElementInfo(): EditableDashboardElementInfo {
return {
typeName: t('dashboard.edit-pane.elements.variable', 'Variable'),
icon: 'chart-line',
instanceName: this.variable.state.name,
};
}
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
const variable = this.variable;
const options = useMemo(() => {
return new OptionsPaneCategoryDescriptor({ title: '', id: 'panel-options' })
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard-scene.variable-editor-form.name', 'Name'),
popularRank: 1,
render: () => <VariableNameInput variable={variable} />,
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard-scene.variable-editor-form.label', 'Label'),
description: t(
'dashboard-scene.variable-editor-form.description-optional-display-name',
'Optional display name'
),
render: () => <VariableLabelInput variable={variable} />,
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard-scene.variable-editor-form.description', 'Description'),
render: () => <VariableDescriptionTextArea variable={variable} />,
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: '',
skipField: true,
render: () => <VariableHideInput variable={variable} />,
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard-scene.variable-editor-form.type', 'Type'),
render: () => <VariableTypeSelect variable={variable} />,
})
);
}, [variable]);
return [options];
}
public onDelete() {
const set = this.variable.parent!;
if (set instanceof SceneVariableSet) {
this.variable.publishEvent(new ObjectRemovedFromCanvasEvent(this.variable), true);
set.setState({ variables: set.state.variables.filter((v) => v !== this.variable) });
}
}
}
interface VariableInputProps {
variable: SceneVariable;
}
function VariableNameInput({ variable }: VariableInputProps) {
const { name } = variable.useState();
return <Input value={name} onChange={(e) => variable.setState({ name: e.currentTarget.value })} />;
}
function VariableLabelInput({ variable }: VariableInputProps) {
const { label } = variable.useState();
return <Input value={label} onChange={(e) => variable.setState({ label: e.currentTarget.value })} />;
}
function VariableDescriptionTextArea({ variable }: VariableInputProps) {
const { description } = variable.useState();
return (
<TextArea
id="description-text-area"
value={description ?? ''}
placeholder={t('dashboard-scene.variable-editor-form.placeholder-descriptive-text', 'Descriptive text')}
onChange={(e) => variable.setState({ description: e.currentTarget.value })}
/>
);
}
function VariableHideInput({ variable }: VariableInputProps) {
const { hide = VariableHide.dontHide } = variable.useState();
const onChange = (option: VariableHide) => {
variable.setState({ hide: option });
};
return <VariableHideSelect hide={hide} type={variable.state.type} onChange={onChange} />;
}
function VariableTypeSelect({ variable }: VariableInputProps) {
const options = useMemo(() => getVariableTypeSelectOptions().map((o) => ({ value: o.value!, label: o.label })), []);
const onOpenVariableEdior = () => {
const set = variable.parent!;
if (!(set instanceof SceneVariableSet)) {
return;
}
const variableIndex = set.state.variables.indexOf(variable);
locationService.partial({ editview: 'variables', editIndex: variableIndex });
};
return (
<Stack gap={2} direction={'column'}>
<Combobox value={variable.state.type} options={options} disabled={true} onChange={() => {}} />
<Button
tooltip={t(
'dashboard-scene.variable-editor-form.open-editor-tooltip',
'For more variable options open variable editor'
)}
onClick={onOpenVariableEdior}
fullWidth
>
<Trans i18nKey="dashboard-scene.variable-editor-form.open-editor">Open variable editor</Trans>
</Button>
</Stack>
);
}
@@ -11,9 +11,9 @@ interface Props {
}
const HIDE_OPTIONS = [
{ label: 'Label and value', value: VariableHide.dontHide },
{ label: 'Value', value: VariableHide.hideLabel },
{ label: 'Nothing', value: VariableHide.hideVariable },
{ label: 'Nothing', value: VariableHide.dontHide },
{ label: 'Variable', value: VariableHide.hideVariable },
{ label: 'Label', value: VariableHide.hideLabel },
];
export function VariableHideSelect({ onChange, hide, type }: PropsWithChildren<Props>) {
@@ -24,7 +24,7 @@ export function VariableHideSelect({ onChange, hide, type }: PropsWithChildren<P
}
return (
<Field label={t('dashboard-scene.variable-hide-select.label-show-on-dashboard', 'Show on dashboard')}>
<Field label={t('dashboard-scene.variable-hide-select.label', 'Hide')}>
<RadioButtonGroup options={HIDE_OPTIONS} onChange={onChange} value={value} />
</Field>
);
@@ -20,7 +20,7 @@ export function VariableTypeSelect({ onChange, type }: PropsWithChildren<Props>)
return (
<VariableSelectField
name="Select variable type"
name="Variable type"
value={value}
options={options}
onChange={onChange}
+10 -3
View File
@@ -2781,7 +2781,8 @@
"row": "Row",
"rows": "Rows",
"tab": "Tab",
"tabs": "Tabs"
"tabs": "Tabs",
"variable": "Variable"
},
"open": "Open options pane",
"row": {
@@ -3622,13 +3623,19 @@
"aria-label-variable-editor-form": "Variable editor form",
"back-to-list": "Back to list",
"delete": "Delete",
"description": "Description",
"description-optional-display-name": "Optional display name",
"description-template-variable-characters": "The name of the template variable. (Max. 50 characters)",
"general": "General",
"label": "Label",
"name": "Name",
"open-editor": "Open variable editor",
"open-editor-tooltip": "For more variable options open variable editor",
"placeholder-descriptive-text": "Descriptive text",
"placeholder-label-name": "Label name",
"placeholder-variable-name": "Variable name",
"text-running-query": "Running query..."
"text-running-query": "Running query...",
"type": "Type"
},
"variable-editor-list": {
"definition": "Definition",
@@ -3641,7 +3648,7 @@
"tooltip-remove-variable": "Remove variable"
},
"variable-hide-select": {
"label-show-on-dashboard": "Show on dashboard"
"label": "Hide"
},
"variable-usages-button": {
"title-show-usages": "Showing usages for: {{variableId}}",
+11 -11
View File
@@ -3466,11 +3466,11 @@ __metadata:
languageName: unknown
linkType: soft
"@grafana/scenes-react@npm:6.6.3":
version: 6.6.3
resolution: "@grafana/scenes-react@npm:6.6.3"
"@grafana/scenes-react@npm:^6.7.0":
version: 6.7.0
resolution: "@grafana/scenes-react@npm:6.7.0"
dependencies:
"@grafana/scenes": "npm:6.6.3"
"@grafana/scenes": "npm:6.7.0"
lru-cache: "npm:^10.2.2"
react-use: "npm:^17.4.0"
peerDependencies:
@@ -3482,13 +3482,13 @@ __metadata:
react: ^18.0.0
react-dom: ^18.0.0
react-router-dom: ^6.28.0
checksum: 10/65d28b1a2d1213a40fd0f9fbfac8f6fe209c7fdf6160f6df14aff15746efe010baa931f7732c467bcbd8d7574185aa75e778b5151a18cc6415e64994489f7636
checksum: 10/3561d1d0a9a2c07d8e5b560ff8cc03de017d29c73f1451f03f0d56a0cfb71e71bfe1c1e657840d9afb2fe5ed7e101532cbc843322b1d9a54ee2870a05acf77fb
languageName: node
linkType: hard
"@grafana/scenes@npm:6.6.3":
version: 6.6.3
resolution: "@grafana/scenes@npm:6.6.3"
"@grafana/scenes@npm:6.7.0, @grafana/scenes@npm:^6.7.0":
version: 6.7.0
resolution: "@grafana/scenes@npm:6.7.0"
dependencies:
"@floating-ui/react": "npm:^0.26.16"
"@leeoniya/ufuzzy": "npm:^1.0.16"
@@ -3506,7 +3506,7 @@ __metadata:
react: ^18.0.0
react-dom: ^18.0.0
react-router-dom: ^6.28.0
checksum: 10/f2ef34be526a6f59591ea496a514b734591778c870491bc09d175a3bf29845c5691e6e1ddfb2e4622c9139db8edfd3943bec7095cfc5804f01d6ed5584b06760
checksum: 10/5b0a7f49b11d88a10c67f697c7951d730d6e3bf2d7ca6354134ccf099009feaa269ac7aa559147c3565a925935db2cf5c9d645c0e194d9cd02a99566d7b7299a
languageName: node
linkType: hard
@@ -17642,8 +17642,8 @@ __metadata:
"@grafana/prometheus": "workspace:*"
"@grafana/runtime": "workspace:*"
"@grafana/saga-icons": "workspace:*"
"@grafana/scenes": "npm:6.6.3"
"@grafana/scenes-react": "npm:6.6.3"
"@grafana/scenes": "npm:^6.7.0"
"@grafana/scenes-react": "npm:^6.7.0"
"@grafana/schema": "workspace:*"
"@grafana/sql": "workspace:*"
"@grafana/tsconfig": "npm:^2.0.0"