From 9e2888637281ce411e8bb08156c23d7d045bf807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 10 Apr 2025 13:28:07 +0200 Subject: [PATCH] Dashboard: Variables in outline (#103712) * Update * Progress * update * fixes * Fixed crash --- packages/grafana-data/src/types/icon.ts | 1 + .../edit-pane/DashboardEditPane.tsx | 16 ++- .../edit-pane/DashboardEditableElement.tsx | 1 + .../edit-pane/DashboardOutline.tsx | 89 +++++++++----- .../edit-pane/ElementEditPane.tsx | 5 +- .../edit-pane/ElementSelection.ts | 10 +- .../edit-pane/VizPanelEditableElement.tsx | 6 +- .../dashboard-scene/edit-pane/shared.ts | 24 +--- .../panel-edit/getPanelFrameOptions.tsx | 6 +- .../SceneGridRowEditableElement.tsx | 3 +- .../scene/layout-rows/RowItem.tsx | 10 +- .../scene/layout-rows/RowItemEditor.tsx | 12 +- .../scene/layout-rows/RowsLayoutManager.tsx | 2 +- .../scene/layout-tabs/TabItem.tsx | 13 +- .../scene/layout-tabs/TabItemEditor.tsx | 12 +- .../scene/layout-tabs/TabsLayoutManager.tsx | 2 +- .../scene/types/EditableDashboardElement.ts | 7 +- .../variables/VariableEditableElement.tsx | 15 ++- .../variables/VariableSetEditableElement.tsx | 116 ++++++++++++++++++ public/locales/en-US/grafana.json | 14 ++- 20 files changed, 260 insertions(+), 104 deletions(-) create mode 100644 public/app/features/dashboard-scene/settings/variables/VariableSetEditableElement.tsx diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts index 685458d40ad..6c53ddcb48a 100644 --- a/packages/grafana-data/src/types/icon.ts +++ b/packages/grafana-data/src/types/icon.ts @@ -268,6 +268,7 @@ export const availableIconsIndex = { x: true, 'add-user': true, attach: true, + 'dollar-alt': true, }; export type IconName = keyof typeof availableIconsIndex; diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx index b425fe9015e..06673e54ee6 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx @@ -176,6 +176,7 @@ export class DashboardEditPane extends SceneObjectBase { 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 && ( - + )} @@ -254,7 +261,12 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
- +
- + ); } 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 === '' ? '' : elementInfo.instanceName; - const elementExpanded = !editableElement.getCollapsedState?.(); + const noTitleText = t('dashboard.outline.tree-item.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 && } + {elementInfo.isContainer && } {instanceName} + {elementInfo.isHidden && } + {/* eslint-disable-next-line @grafana/no-untranslated-strings */} + {elementInfo.isContainer && isCollapsed && ({children.length})} - {expandable && isExpanded && ( + {elementInfo.isContainer && !isCollapsed && (
{children.length > 0 ? ( children.map((child) => ( )) ) : ( - - (empty) + + (empty) )}
@@ -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; diff --git a/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx index 5572c81f1d4..df5a3e171f4 100644 --- a/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx @@ -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 ( diff --git a/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts b/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts index 0bae42a69f4..2d871758b15 100644 --- a/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts +++ b/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts @@ -10,8 +10,8 @@ import { getEditableElementFor } from './shared'; export class ElementSelection { private selectedObjects: Map>; private sameType?: boolean; - private _isMultiSelection: boolean; + private _isNewElement = false; constructor(values: Array<[string, SceneObjectRef]>) { 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; diff --git a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx index 640b494a7b6..19644cf099b 100644 --- a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx @@ -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: () => , + render: () => , }) ) .addItem( @@ -71,7 +71,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc render: () => , }) ); - }, [panel]); + }, [panel, isNewElement]); const layoutCategories = useMemo( () => (isDashboardLayoutItem(layoutElement) && layoutElement.getOptions ? layoutElement.getOptions() : []), diff --git a/public/app/features/dashboard-scene/edit-pane/shared.ts b/public/app/features/dashboard-scene/edit-pane/shared.ts index 8c078d5b2ce..9a0e0561368 100644 --- a/public/app/features/dashboard-scene/edit-pane/shared.ts +++ b/public/app/features/dashboard-scene/edit-pane/shared.ts @@ -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; } diff --git a/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx b/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx index 31037a28413..1e80ce97156 100644 --- a/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx +++ b/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx @@ -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 ( diff --git a/public/app/features/dashboard-scene/scene/layout-default/SceneGridRowEditableElement.tsx b/public/app/features/dashboard-scene/scene/layout-default/SceneGridRowEditableElement.tsx index 261856543a3..8568165e3d1 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/SceneGridRowEditableElement.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/SceneGridRowEditableElement.tsx @@ -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, }; } diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx index 90ff71c594e..dd83c4b4604 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx @@ -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) { diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx index 6012eae70cb..9f7c0426682 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx @@ -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: () => , + render: () => , }) ) .addItem( @@ -40,7 +40,7 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[] render: () => , }) ), - [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 ( diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx index ad82737cb81..264eb8d236f 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -95,7 +95,7 @@ export class RowsLayoutManager extends SceneObjectBase 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); diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx index b42705614e6..cb26022c2e2 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx @@ -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) { diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx index 35d864a3b38..9958e0c87ce 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx @@ -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: () => , + render: () => , }) ), - [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 ( diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx index 194d49c1d11..9be608411c1 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx @@ -126,7 +126,7 @@ export class TabsLayoutManager extends SceneObjectBase 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) { diff --git a/public/app/features/dashboard-scene/scene/types/EditableDashboardElement.ts b/public/app/features/dashboard-scene/scene/types/EditableDashboardElement.ts index 3455eb42bff..672a188b32b 100644 --- a/public/app/features/dashboard-scene/scene/types/EditableDashboardElement.ts +++ b/public/app/features/dashboard-scene/scene/types/EditableDashboardElement.ts @@ -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 { diff --git a/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx b/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx index 4c00dbab66a..9a86aff4c2d 100644 --- a/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx +++ b/public/app/features/dashboard-scene/settings/variables/VariableEditableElement.tsx @@ -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: () => , + render: () => , }) ) .addItem( @@ -69,7 +71,7 @@ export class VariableEditableElement implements EditableDashboardElement, BulkAc render: () => , }) ); - }, [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 variable.setState({ name: e.currentTarget.value })} />; + const ref = useEditPaneInputAutoFocus({ autoFocus: isNewElement }); + return variable.setState({ name: e.currentTarget.value })} />; } function VariableLabelInput({ variable }: VariableInputProps) { diff --git a/public/app/features/dashboard-scene/settings/variables/VariableSetEditableElement.tsx b/public/app/features/dashboard-scene/settings/variables/VariableSetEditableElement.tsx new file mode 100644 index 00000000000..964a134a27a --- /dev/null +++ b/public/app/features/dashboard-scene/settings/variables/VariableSetEditableElement.tsx @@ -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: () => , + }) + ); + }, [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 ( + + {variables.map((variable) => ( +
onEditVariable(variable)}> + {/* eslint-disable-next-line @grafana/no-untranslated-strings */} + ${variable.state.name} + + + +
+ ))} + + + +
+ ); +} + +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', + }, + }, + }), + }; +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 3c17b9c6c73..f94a028cb50 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -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": "" } }, "override-category-title": {