From 927ce79dcf1c5e3d39879ff2967f58589f1b4088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 9 Apr 2025 14:27:35 +0200 Subject: [PATCH] Dashboard: Variable selection and editing (#103451) * Variable selection * Update * fix * Update * Updates * update * Update --- .../new-query-variable.spec.ts | 2 +- .../new-query-variable.spec.ts | 2 +- package.json | 4 +- .../dashboard-scene/edit-pane/shared.ts | 11 +- .../scene/VariableControls.tsx | 50 ++++-- .../variables/VariableEditableElement.tsx | 151 ++++++++++++++++++ .../components/VariableHideSelect.tsx | 8 +- .../components/VariableTypeSelect.tsx | 2 +- public/locales/en-US/grafana.json | 13 +- yarn.lock | 22 +-- 10 files changed, 230 insertions(+), 35 deletions(-) create mode 100644 public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx diff --git a/e2e/dashboards-suite/new-query-variable.spec.ts b/e2e/dashboards-suite/new-query-variable.spec.ts index c37200741e7..4b3c09a6eb0 100644 --- a/e2e/dashboards-suite/new-query-variable.spec.ts +++ b/e2e/dashboards-suite/new-query-variable.spec.ts @@ -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"]') diff --git a/e2e/old-arch/dashboards-suite/new-query-variable.spec.ts b/e2e/old-arch/dashboards-suite/new-query-variable.spec.ts index d58716d40c3..48e6587468e 100644 --- a/e2e/old-arch/dashboards-suite/new-query-variable.spec.ts +++ b/e2e/old-arch/dashboards-suite/new-query-variable.spec.ts @@ -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"]') diff --git a/package.json b/package.json index 05df334a793..6e57c164378 100644 --- a/package.json +++ b/package.json @@ -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:*", diff --git a/public/app/features/dashboard-scene/edit-pane/shared.ts b/public/app/features/dashboard-scene/edit-pane/shared.ts index 82b3176ee91..8c078d5b2ce 100644 --- a/public/app/features/dashboard-scene/edit-pane/shared.ts +++ b/public/app/features/dashboard-scene/edit-pane/shared.ts @@ -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 { static type = 'new-object-added-to-canvas'; } diff --git a/public/app/features/dashboard-scene/scene/VariableControls.tsx b/public/app/features/dashboard-scene/scene/VariableControls.tsx index dc4dc9653a5..ee425f28519 100644 --- a/public/app/features/dashboard-scene/scene/VariableControls.tsx +++ b/public/app/features/dashboard-scene/scene/VariableControls.tsx @@ -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(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 ( -
- +
+
); } -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', }), }); diff --git a/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx b/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx new file mode 100644 index 00000000000..4c00dbab66a --- /dev/null +++ b/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx @@ -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: () => , + }) + ) + .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: () => , + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard-scene.variable-editor-form.description', 'Description'), + render: () => , + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: '', + skipField: true, + render: () => , + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard-scene.variable-editor-form.type', 'Type'), + render: () => , + }) + ); + }, [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 variable.setState({ name: e.currentTarget.value })} />; +} + +function VariableLabelInput({ variable }: VariableInputProps) { + const { label } = variable.useState(); + return variable.setState({ label: e.currentTarget.value })} />; +} + +function VariableDescriptionTextArea({ variable }: VariableInputProps) { + const { description } = variable.useState(); + + return ( +