Dashboard: Variables in outline (#103712)

* Update

* Progress

* update

* fixes

* Fixed crash
This commit is contained in:
Torkel Ödegaard
2025-04-10 13:28:07 +02:00
committed by GitHub
parent c3742fc506
commit 9e28886372
20 changed files with 260 additions and 104 deletions
+1
View File
@@ -268,6 +268,7 @@ export const availableIconsIndex = {
x: true,
'add-user': true,
attach: true,
'dollar-alt': true,
};
export type IconName = keyof typeof availableIconsIndex;
@@ -176,6 +176,7 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
private newObjectAddedToCanvas(obj: SceneObject) {
this.selectObject(obj, obj.state.key!);
this.state.selection!.markAsNewElement();
}
}
@@ -194,6 +195,7 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
const styles = useStyles2(getStyles);
const editableElement = useEditableElement(selection, editPane);
const selectedObject = selection?.getFirstObject();
const isNewElement = selection?.isNewElement() ?? false;
const [outlineCollapsed, setOutlineCollapsed] = useLocalStorage(
'grafana.dashboard.edit-pane.outline.collapsed',
true
@@ -233,7 +235,12 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
{openOverlay && (
<Resizable className={styles.overlayWrapper} defaultSize={{ height: '100%', width: '300px' }}>
<ElementEditPane element={editableElement} key={selectedObject?.state.key} editPane={editPane} />
<ElementEditPane
element={editableElement}
key={selectedObject?.state.key}
editPane={editPane}
isNewElement={isNewElement}
/>
</Resizable>
)}
</>
@@ -254,7 +261,12 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
<div className={styles.wrapper}>
<div {...splitter.containerProps}>
<div {...splitter.primaryProps} className={cx(splitter.primaryProps.className, styles.paneContent)}>
<ElementEditPane element={editableElement} key={selectedObject?.state.key} editPane={editPane} />
<ElementEditPane
element={editableElement}
key={selectedObject?.state.key}
editPane={editPane}
isNewElement={isNewElement}
/>
</div>
<div
{...splitter.splitterProps}
@@ -20,6 +20,7 @@ export class DashboardEditableElement implements EditableDashboardElement {
typeName: t('dashboard.edit-pane.elements.dashboard', 'Dashboard'),
icon: 'apps',
instanceName: t('dashboard.edit-pane.elements.dashboard', 'Dashboard'),
isContainer: true,
};
}
@@ -1,17 +1,19 @@
import { css, cx } from '@emotion/css';
import { sortBy } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneObject, VizPanel } from '@grafana/scenes';
import { SceneObject } from '@grafana/scenes';
import { Box, Icon, Text, useElementSelection, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { t, Trans } from 'app/core/internationalization';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { EditableDashboardElement } from '../scene/types/EditableDashboardElement';
import { isInCloneChain } from '../utils/clone';
import { getDashboardSceneFor } from '../utils/utils';
import { DashboardEditPane } from './DashboardEditPane';
import { getEditableElementFor, hasEditableElement } from './shared';
import { getEditableElementFor } from './shared';
export interface Props {
editPane: DashboardEditPane;
@@ -22,50 +24,54 @@ export function DashboardOutline({ editPane }: Props) {
return (
<Box padding={1} gap={0.25} display="flex" direction="column">
<DashboardOutlineNode sceneObject={dashboard} editPane={editPane} expandable />
<DashboardOutlineNode sceneObject={dashboard} editPane={editPane} depth={0} />
</Box>
);
}
function DashboardOutlineNode({
sceneObject,
expandable,
editPane,
depth,
}: {
sceneObject: SceneObject;
expandable: boolean;
editPane: DashboardEditPane;
depth: number;
}) {
const [isExpanded, setIsExpanded] = useState(true);
const [isCollapsed, setIsCollapsed] = useState(depth > 0);
const { key } = sceneObject.useState();
const styles = useStyles2(getStyles);
const { isSelected, onSelect } = useElementSelection(key);
const isCloned = useMemo(() => isInCloneChain(key!), [key]);
const editableElement = useMemo(() => getEditableElementFor(sceneObject)!, [sceneObject]);
const children = collectEditableElementChildren(sceneObject);
const children = sortBy(collectEditableElementChildren(sceneObject, [], 0), 'depth');
const elementInfo = editableElement.getEditableElementInfo();
const instanceName = elementInfo.instanceName === '' ? '<empty title>' : elementInfo.instanceName;
const elementExpanded = !editableElement.getCollapsedState?.();
const noTitleText = t('dashboard.outline.tree-item.no-title', '<no title>');
const instanceName = elementInfo.instanceName === '' ? noTitleText : elementInfo.instanceName;
const elementCollapsed = editableElement.getCollapsedState?.();
const onPointerDown = (evt: React.PointerEvent) => {
onSelect?.(evt);
setIsExpanded(!isExpanded);
// Only select via clicking outline never deselect
if (!isSelected) {
onSelect?.(evt);
}
setIsCollapsed(!isCollapsed);
editableElement.scrollIntoView?.();
// Sync expanded state with canvas element
if (editableElement.getCollapsedState) {
editableElement.setCollapsedState?.(isExpanded);
editableElement.setCollapsedState?.(!isCollapsed);
}
};
// Sync canvas element expanded state with outline element
useEffect(() => {
if (elementExpanded !== isExpanded) {
console.log('elementExpanded', elementExpanded);
setIsExpanded(elementExpanded);
if (elementCollapsed != null && elementCollapsed !== isCollapsed) {
setIsCollapsed(elementCollapsed);
}
}, [isExpanded, elementExpanded]);
}, [isCollapsed, elementCollapsed]);
return (
<>
@@ -74,25 +80,28 @@ function DashboardOutlineNode({
className={cx(styles.nodeButton, isCloned && styles.nodeButtonClone, isSelected && styles.nodeButtonSelected)}
onPointerDown={onPointerDown}
>
{expandable && <Icon name={isExpanded ? 'angle-down' : 'angle-right'} />}
{elementInfo.isContainer && <Icon name={!isCollapsed ? 'angle-down' : 'angle-right'} />}
<Icon size="sm" name={elementInfo.icon} />
<span>{instanceName}</span>
{elementInfo.isHidden && <Icon name="eye-slash" size="sm" className={styles.hiddenIcon} />}
{/* eslint-disable-next-line @grafana/no-untranslated-strings */}
{elementInfo.isContainer && isCollapsed && <span>({children.length})</span>}
</button>
{expandable && isExpanded && (
{elementInfo.isContainer && !isCollapsed && (
<div className={styles.container} role="group">
{children.length > 0 ? (
children.map((child) => (
<DashboardOutlineNode
key={child.sceneObject.state.key}
sceneObject={child.sceneObject}
expandable={child.expandable}
editPane={editPane}
depth={depth + 1}
/>
))
) : (
<Text element="p" color="secondary">
<Trans i18nKey="dashboard.outline.tree.item.empty">(empty)</Trans>
<Text color="secondary">
<Trans i18nKey="dashboard.outline.tree-item.empty">(empty)</Trans>
</Text>
)}
</div>
@@ -136,12 +145,16 @@ function getStyles(theme: GrafanaTheme2) {
}),
nodeButtonSelected: css({
color: theme.colors.text.primary,
outline: `1px dashed ${theme.colors.primary.border}`,
outline: `1px dashed ${theme.colors.primary.border} !important`,
outlineOffset: '0px',
'&:hover': {
outline: `1px dashed ${theme.colors.primary.border}`,
},
}),
hiddenIcon: css({
color: theme.colors.text.secondary,
marginLeft: theme.spacing(1),
}),
nodeButtonClone: css({
color: theme.colors.text.secondary,
cursor: 'not-allowed',
@@ -151,29 +164,37 @@ function getStyles(theme: GrafanaTheme2) {
interface EditableElementConfig {
sceneObject: SceneObject;
expandable: boolean;
editableElement: EditableDashboardElement;
depth: number;
}
function collectEditableElementChildren(
sceneObject: SceneObject,
children: EditableElementConfig[] = []
children: EditableElementConfig[],
depth: number
): EditableElementConfig[] {
sceneObject.forEachChild((child) => {
const editableElement = getEditableElementFor(child);
if (editableElement) {
children.push({ sceneObject: child, editableElement, depth });
return;
}
if (child instanceof DashboardGridItem) {
// DashboardGridItem is a special case as it can contain repeated panels
// In this case, we want to show the repeated panels as separate items, otherwise show the body panel
if (child.state.repeatedPanels?.length) {
children.push(...child.state.repeatedPanels.map((panel) => ({ sceneObject: panel, expandable: false })));
} else {
children.push({ sceneObject: child.state.body, expandable: false });
for (const repeatedPanel of child.state.repeatedPanels) {
const editableElement = getEditableElementFor(repeatedPanel)!;
children.push({ sceneObject: repeatedPanel, editableElement, depth });
}
return;
}
} else if (child instanceof VizPanel) {
children.push({ sceneObject: child, expandable: false });
} else if (hasEditableElement(child)) {
children.push({ sceneObject: child, expandable: true });
} else {
collectEditableElementChildren(child, children);
}
collectEditableElementChildren(child, children, depth + 1);
});
return children;
@@ -11,10 +11,11 @@ import { EditPaneHeader } from './EditPaneHeader';
export interface Props {
element: EditableDashboardElement;
editPane: DashboardEditPane;
isNewElement: boolean;
}
export function ElementEditPane({ element, editPane }: Props) {
const categories = element.useEditPaneOptions ? element.useEditPaneOptions() : [];
export function ElementEditPane({ element, editPane, isNewElement }: Props) {
const categories = element.useEditPaneOptions ? element.useEditPaneOptions(isNewElement) : [];
const styles = useStyles2(getStyles);
return (
@@ -10,8 +10,8 @@ import { getEditableElementFor } from './shared';
export class ElementSelection {
private selectedObjects: Map<string, SceneObjectRef<SceneObject>>;
private sameType?: boolean;
private _isMultiSelection: boolean;
private _isNewElement = false;
constructor(values: Array<[string, SceneObjectRef<SceneObject>]>) {
this.selectedObjects = new Map(values);
@@ -22,6 +22,14 @@ export class ElementSelection {
}
}
public markAsNewElement() {
this._isNewElement = true;
}
public isNewElement() {
return this._isNewElement;
}
private checkSameType() {
const values = this.selectedObjects.values();
const firstType = values.next().value?.resolve().constructor.name;
@@ -38,7 +38,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
};
}
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
public useEditPaneOptions(isNewElement: boolean): OptionsPaneCategoryDescriptor[] {
const panel = this.panel;
const layoutElement = panel.parent!;
@@ -55,7 +55,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
title: t('dashboard.viz-panel.options.title-option', 'Title'),
value: panel.state.title,
popularRank: 1,
render: () => <PanelFrameTitleInput panel={panel} />,
render: () => <PanelFrameTitleInput panel={panel} isNewElement={isNewElement} />,
})
)
.addItem(
@@ -71,7 +71,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
render: () => <PanelBackgroundSwitch panel={panel} />,
})
);
}, [panel]);
}, [panel, isNewElement]);
const layoutCategories = useMemo(
() => (isDashboardLayoutItem(layoutElement) && layoutElement.getOptions ? layoutElement.getOptions() : []),
@@ -1,12 +1,13 @@
import { useSessionStorage } from 'react-use';
import { BusEventWithPayload } from '@grafana/data';
import { SceneGridRow, SceneObject, SceneVariable, VizPanel } from '@grafana/scenes';
import { SceneGridRow, SceneObject, SceneVariable, SceneVariableSet, 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 { VariableSetEditableElement } from '../settings/variables/VariableSetEditableElement';
import { DashboardEditableElement } from './DashboardEditableElement';
import { VizPanelEditableElement } from './VizPanelEditableElement';
@@ -36,6 +37,10 @@ export function getEditableElementFor(sceneObj: SceneObject | undefined): Editab
return new DashboardEditableElement(sceneObj);
}
if (sceneObj instanceof SceneVariableSet) {
return new VariableSetEditableElement(sceneObj);
}
if (isSceneVariable(sceneObj)) {
return new VariableEditableElement(sceneObj);
}
@@ -43,23 +48,6 @@ export function getEditableElementFor(sceneObj: SceneObject | undefined): Editab
return undefined;
}
export function hasEditableElement(sceneObj: SceneObject | undefined): boolean {
if (!sceneObj) {
return false;
}
if (
isEditableDashboardElement(sceneObj) ||
sceneObj instanceof VizPanel ||
sceneObj instanceof SceneGridRow ||
sceneObj instanceof DashboardScene
) {
return true;
}
return false;
}
export function isSceneVariable(sceneObj: SceneObject): sceneObj is SceneVariable {
return 'getValue' in sceneObj;
}
@@ -3,7 +3,6 @@ import { selectors } from '@grafana/e2e-selectors';
import { config } from '@grafana/runtime';
import { SceneTimeRangeLike, VizPanel } from '@grafana/scenes';
import { DataLinksInlineEditor, Input, TextArea, Switch } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { GenAIPanelDescriptionButton } from 'app/features/dashboard/components/GenAI/GenAIPanelDescriptionButton';
import { GenAIPanelTitleButton } from 'app/features/dashboard/components/GenAI/GenAIPanelTitleButton';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
@@ -109,13 +108,12 @@ function ScenePanelLinksEditor({ panelLinks }: ScenePanelLinksEditorProps) {
);
}
export function PanelFrameTitleInput({ panel }: { panel: VizPanel }) {
export function PanelFrameTitleInput({ panel, isNewElement }: { panel: VizPanel; isNewElement?: boolean }) {
const { title } = panel.useState();
const notInPanelEdit = panel.getPanelContext().app !== CoreApp.PanelEditor;
const newPanelTitle = t('dashboard.new-panel-title', 'New panel');
let ref = useEditPaneInputAutoFocus({
autoFocus: notInPanelEdit && title === newPanelTitle,
autoFocus: notInPanelEdit && isNewElement,
});
return (
@@ -27,7 +27,8 @@ export class SceneGridRowEditableElement implements EditableDashboardElement, Bu
return {
typeName: t('dashboard.edit-pane.elements.row', 'Row'),
instanceName: sceneGraph.interpolate(this._row, this._row.state.title, undefined, 'text'),
icon: 'line-alt',
icon: 'list-ul',
isContainer: true,
};
}
@@ -31,7 +31,7 @@ import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
import { EditableDashboardElement, EditableDashboardElementInfo } from '../types/EditableDashboardElement';
import { LayoutParent } from '../types/LayoutParent';
import { getEditOptions } from './RowItemEditor';
import { useEditOptions } from './RowItemEditor';
import { RowItemRenderer } from './RowItemRenderer';
import { RowItemRepeaterBehavior } from './RowItemRepeaterBehavior';
import { RowItems } from './RowItems';
@@ -45,7 +45,6 @@ export interface RowItemState extends SceneObjectState {
fillScreen?: boolean;
isDropTarget?: boolean;
conditionalRendering?: ConditionalRendering;
isNew?: boolean;
}
export class RowItem
@@ -89,6 +88,7 @@ export class RowItem
typeName: t('dashboard.edit-pane.elements.row', 'Row'),
instanceName: sceneGraph.interpolate(this, this.state.title, undefined, 'text'),
icon: 'list-ul',
isContainer: true,
};
}
@@ -104,8 +104,8 @@ export class RowItem
this.setState({ layout: this._layoutRestorer.getLayout(layout, this.state.layout) });
}
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
return getEditOptions(this);
public useEditPaneOptions(isNewElement: boolean): OptionsPaneCategoryDescriptor[] {
return useEditOptions(this, isNewElement);
}
public onDelete() {
@@ -187,7 +187,7 @@ export class RowItem
}
public onChangeTitle(title: string) {
this.setState({ title, isNew: false });
this.setState({ title });
}
public onHeaderHiddenToggle(hideHeader = !this.state.hideHeader) {
@@ -16,7 +16,7 @@ import { useEditPaneInputAutoFocus } from '../layouts-shared/utils';
import { RowItem } from './RowItem';
export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[] {
export function useEditOptions(model: RowItem, isNewElement: boolean): OptionsPaneCategoryDescriptor[] {
const { layout } = model.useState();
const rowCategory = useMemo(
@@ -25,7 +25,7 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[]
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.rows-layout.row-options.row.title', 'Title'),
render: () => <RowTitleInput row={model} />,
render: () => <RowTitleInput row={model} isNewElement={isNewElement} />,
})
)
.addItem(
@@ -40,7 +40,7 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[]
render: () => <RowHeaderSwitch row={model} />,
})
),
[model]
[model, isNewElement]
);
const repeatCategory = useMemo(
@@ -78,9 +78,9 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[]
return editOptions;
}
function RowTitleInput({ row }: { row: RowItem }) {
const { title, isNew } = row.useState();
const ref = useEditPaneInputAutoFocus({ autoFocus: isNew });
function RowTitleInput({ row, isNewElement }: { row: RowItem; isNewElement: boolean }) {
const { title } = row.useState();
const ref = useEditPaneInputAutoFocus({ autoFocus: isNewElement });
const hasUniqueTitle = row.hasUniqueTitle();
return (
@@ -95,7 +95,7 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
}
public addNewRow(row?: RowItem): RowItem {
const newRow = row ?? new RowItem({ isNew: true });
const newRow = row ?? new RowItem({});
const existingNames = new Set(this.state.rows.map((row) => row.state.title).filter((title) => title !== undefined));
const newTitle = generateUniqueTitle(newRow.state.title, existingNames);
@@ -31,7 +31,7 @@ import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
import { EditableDashboardElement, EditableDashboardElementInfo } from '../types/EditableDashboardElement';
import { LayoutParent } from '../types/LayoutParent';
import { getEditOptions } from './TabItemEditor';
import { useEditOptions } from './TabItemEditor';
import { TabItemRenderer } from './TabItemRenderer';
import { TabItems } from './TabItems';
import { TabsLayoutManager } from './TabsLayoutManager';
@@ -39,10 +39,6 @@ import { TabsLayoutManager } from './TabsLayoutManager';
export interface TabItemState extends SceneObjectState {
layout: DashboardLayoutManager;
title?: string;
/**
* Used to auto focus the title input
*/
isNew?: boolean;
isDropTarget?: boolean;
conditionalRendering?: ConditionalRendering;
}
@@ -89,6 +85,7 @@ export class TabItem
typeName: t('dashboard.edit-pane.elements.tab', 'Tab'),
instanceName: sceneGraph.interpolate(this, this.state.title, undefined, 'text'),
icon: 'layers',
isContainer: true,
};
}
@@ -104,8 +101,8 @@ export class TabItem
this.setState({ layout: this._layoutRestorer.getLayout(layout, this.state.layout) });
}
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
return getEditOptions(this);
public useEditPaneOptions(isNewElement: boolean): OptionsPaneCategoryDescriptor[] {
return useEditOptions(this, isNewElement);
}
public onDelete() {
@@ -172,7 +169,7 @@ export class TabItem
}
public onChangeTitle(title: string) {
this.setState({ title, isNew: false });
this.setState({ title });
}
public setIsDropTarget(isDropTarget: boolean) {
@@ -11,7 +11,7 @@ import { useEditPaneInputAutoFocus } from '../layouts-shared/utils';
import { TabItem } from './TabItem';
export function getEditOptions(model: TabItem): OptionsPaneCategoryDescriptor[] {
export function useEditOptions(model: TabItem, isNewElement: boolean): OptionsPaneCategoryDescriptor[] {
const { layout } = model.useState();
const tabCategory = useMemo(
@@ -19,10 +19,10 @@ export function getEditOptions(model: TabItem): OptionsPaneCategoryDescriptor[]
new OptionsPaneCategoryDescriptor({ title: '', id: 'tab-item-options' }).addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.tabs-layout.tab-options.title-option', 'Title'),
render: () => <TabTitleInput tab={model} />,
render: () => <TabTitleInput tab={model} isNewElement={isNewElement} />,
})
),
[model]
[model, isNewElement]
);
const layoutCategory = useLayoutCategory(layout);
@@ -41,9 +41,9 @@ export function getEditOptions(model: TabItem): OptionsPaneCategoryDescriptor[]
return editOptions;
}
function TabTitleInput({ tab }: { tab: TabItem }) {
const { title, isNew } = tab.useState();
const ref = useEditPaneInputAutoFocus({ autoFocus: isNew });
function TabTitleInput({ tab, isNewElement }: { tab: TabItem; isNewElement: boolean }) {
const { title } = tab.useState();
const ref = useEditPaneInputAutoFocus({ autoFocus: isNewElement });
const hasUniqueTitle = tab.hasUniqueTitle();
return (
@@ -126,7 +126,7 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
}
public addNewTab(tab?: TabItem) {
const newTab = tab ?? new TabItem({ isNew: true });
const newTab = tab ?? new TabItem({});
const existingNames = new Set(this.state.tabs.map((tab) => tab.state.title).filter((title) => title !== undefined));
const newTitle = generateUniqueTitle(newTab.state.title, existingNames);
if (newTitle !== newTab.state.title) {
@@ -18,7 +18,7 @@ export interface EditableDashboardElement {
/**
* Hook that returns edit pane options
*/
useEditPaneOptions(): OptionsPaneCategoryDescriptor[];
useEditPaneOptions(isNewElement: boolean): OptionsPaneCategoryDescriptor[];
/**
* Panel Actions
@@ -70,6 +70,11 @@ export interface EditableDashboardElementInfo {
instanceName: string;
typeName: string;
icon: IconName;
/**
* Mark it as a container of other editable elements
*/
isContainer?: boolean;
isHidden?: boolean;
}
export function isEditableDashboardElement(obj: object): obj is EditableDashboardElement {
@@ -9,6 +9,7 @@ import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
import { ObjectRemovedFromCanvasEvent } from '../../edit-pane/shared';
import { useEditPaneInputAutoFocus } from '../../scene/layouts-shared/utils';
import { BulkActionElement } from '../../scene/types/BulkActionElement';
import { EditableDashboardElement, EditableDashboardElementInfo } from '../../scene/types/EditableDashboardElement';
import { VariableHideSelect } from '../../settings/variables/components/VariableHideSelect';
@@ -23,12 +24,13 @@ export class VariableEditableElement implements EditableDashboardElement, BulkAc
public getEditableElementInfo(): EditableDashboardElementInfo {
return {
typeName: t('dashboard.edit-pane.elements.variable', 'Variable'),
icon: 'chart-line',
icon: 'dollar-alt',
instanceName: this.variable.state.name,
isHidden: this.variable.state.hide === VariableHide.hideVariable,
};
}
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
public useEditPaneOptions(isNewElement: boolean): OptionsPaneCategoryDescriptor[] {
const variable = this.variable;
const options = useMemo(() => {
@@ -37,7 +39,7 @@ export class VariableEditableElement implements EditableDashboardElement, BulkAc
new OptionsPaneItemDescriptor({
title: t('dashboard-scene.variable-editor-form.name', 'Name'),
popularRank: 1,
render: () => <VariableNameInput variable={variable} />,
render: () => <VariableNameInput variable={variable} isNewElement={isNewElement} />,
})
)
.addItem(
@@ -69,7 +71,7 @@ export class VariableEditableElement implements EditableDashboardElement, BulkAc
render: () => <VariableTypeSelect variable={variable} />,
})
);
}, [variable]);
}, [variable, isNewElement]);
return [options];
}
@@ -87,9 +89,10 @@ interface VariableInputProps {
variable: SceneVariable;
}
function VariableNameInput({ variable }: VariableInputProps) {
function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable; isNewElement: boolean }) {
const { name } = variable.useState();
return <Input value={name} onChange={(e) => variable.setState({ name: e.currentTarget.value })} />;
const ref = useEditPaneInputAutoFocus({ autoFocus: isNewElement });
return <Input ref={ref} value={name} onChange={(e) => variable.setState({ name: e.currentTarget.value })} />;
}
function VariableLabelInput({ variable }: VariableInputProps) {
@@ -0,0 +1,116 @@
import { css } from '@emotion/css';
import { useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneVariable, SceneVariableSet } from '@grafana/scenes';
import { Stack, Button, useStyles2, Text, Box } 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 { NewObjectAddedToCanvasEvent } from '../../edit-pane/shared';
import { EditableDashboardElement, EditableDashboardElementInfo } from '../../scene/types/EditableDashboardElement';
import { getDashboardSceneFor } from '../../utils/utils';
import { getVariableDefault } from './utils';
export class VariableSetEditableElement implements EditableDashboardElement {
public readonly isEditableDashboardElement = true;
public readonly typeName = 'Variable';
public constructor(private set: SceneVariableSet) {}
public getEditableElementInfo(): EditableDashboardElementInfo {
return {
typeName: t('dashboard.edit-pane.elements.variable-set', 'Variables'),
icon: 'x',
instanceName: t('dashboard.edit-pane.elements.variable-set', 'Variables'),
isContainer: true,
};
}
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
const set = this.set;
const options = useMemo(() => {
return new OptionsPaneCategoryDescriptor({ title: '', id: 'variables' }).addItem(
new OptionsPaneItemDescriptor({
title: '',
skipField: true,
render: () => <VariableList set={set} />,
})
);
}, [set]);
return [options];
}
}
function VariableList({ set }: { set: SceneVariableSet }) {
const { variables } = set.useState();
const styles = useStyles2(getStyles);
const onEditVariable = (variable: SceneVariable) => {
const { editPane } = getDashboardSceneFor(set).state;
editPane.selectObject(variable, variable.state.key!);
};
const onAddVariable = () => {
//add the new variable to the end of the array
const newVariable = getVariableDefault(variables);
set.setState({ variables: [...variables, newVariable] });
set.publishEvent(new NewObjectAddedToCanvasEvent(newVariable), true);
};
return (
<Stack direction="column" gap={0}>
{variables.map((variable) => (
<div className={styles.variableItem} key={variable.state.name} onClick={() => onEditVariable(variable)}>
{/* eslint-disable-next-line @grafana/no-untranslated-strings */}
<Text>${variable.state.name}</Text>
<Stack direction="row" gap={1} alignItems="center">
<Button variant="primary" size="sm" fill="outline">
<Trans i18nKey="dashboard.edit-pane.variables.select-variable">Select</Trans>
</Button>
</Stack>
</div>
))}
<Box paddingBottom={1} display={'flex'}>
<Button fullWidth icon="plus" size="sm" variant="secondary" onClick={onAddVariable}>
<Trans i18nKey="dashboard.edit-pane.variables.add-variable">Add variable</Trans>
</Button>
</Box>
</Stack>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
variableItem: css({
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
gap: theme.spacing(1),
padding: theme.spacing(0.5),
borderRadius: theme.shape.radius.default,
cursor: 'pointer',
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
transition: theme.transitions.create(['color'], {
duration: theme.transitions.duration.short,
}),
},
'&:last-child': {
marginBottom: theme.spacing(2),
},
button: {
visibility: 'hidden',
},
'&:hover': {
color: theme.colors.text.link,
button: {
visibility: 'visible',
},
},
}),
};
}
+9 -5
View File
@@ -2786,7 +2786,8 @@
"rows": "Rows",
"tab": "Tab",
"tabs": "Tabs",
"variable": "Variable"
"variable": "Variable",
"variable-set": "Variables"
},
"open": "Open options pane",
"row": {
@@ -2794,6 +2795,10 @@
"hide": "Hide",
"title": "Row header"
}
},
"variables": {
"add-variable": "Add variable",
"select-variable": "Select"
}
},
"empty": {
@@ -2915,10 +2920,9 @@
"Recent options-title-recent-options": "Recent options"
},
"outline": {
"tree": {
"item": {
"empty": "(empty)"
}
"tree-item": {
"empty": "(empty)",
"no-title": "<no title>"
}
},
"override-category-title": {