diff --git a/.betterer.results b/.betterer.results index 96c73913a71..cc2d71fb109 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1965,6 +1965,11 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], + "public/app/features/dashboard-scene/edit-pane/VizPanelEditPaneBehavior.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + ], "public/app/features/dashboard-scene/embedding/EmbeddedDashboardTestPage.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], @@ -2088,7 +2093,10 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "9"], [0, 0, 0, "No untranslated strings. Wrap text with ", "10"], [0, 0, 0, "No untranslated strings. Wrap text with ", "11"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "12"] + [0, 0, 0, "No untranslated strings. Wrap text with ", "12"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "13"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "14"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "15"] ], "public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] @@ -2096,6 +2104,11 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] + ], "public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx index b0959584c68..745782546c7 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx @@ -5,6 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { SceneObjectState, SceneObjectBase, SceneObject, SceneObjectRef } from '@grafana/scenes'; import { ToolbarButton, useStyles2 } from '@grafana/ui'; +import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types'; import { getDashboardSceneFor } from '../utils/utils'; import { ElementEditPane } from './ElementEditPane'; @@ -13,7 +14,17 @@ export interface DashboardEditPaneState extends SceneObjectState { selectedObject?: SceneObjectRef; } -export class DashboardEditPane extends SceneObjectBase {} +export class DashboardEditPane extends SceneObjectBase { + public selectObject(obj: SceneObject) { + const currentSelection = this.state.selectedObject?.resolve(); + if (currentSelection === obj) { + const dashboard = getDashboardSceneFor(this); + this.setState({ selectedObject: dashboard.getRef() }); + } else { + this.setState({ selectedObject: obj.getRef() }); + } + } +} export interface Props { editPane: DashboardEditPane; @@ -57,13 +68,29 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla ); } + const element = getEditableElementFor(selectedObject.resolve()); + return (
- +
); } +function getEditableElementFor(obj: SceneObject): EditableDashboardElement { + if (isEditableDashboardElement(obj)) { + return obj; + } + + for (const behavior of obj.state.$behaviors ?? []) { + if (isEditableDashboardElement(behavior)) { + return behavior; + } + } + + throw new Error("Can't find editable element for selected object"); +} + function getStyles(theme: GrafanaTheme2) { return { wrapper: css({ diff --git a/public/app/features/dashboard-scene/edit-pane/DummySelectedObject.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneBehavior.tsx similarity index 77% rename from public/app/features/dashboard-scene/edit-pane/DummySelectedObject.tsx rename to public/app/features/dashboard-scene/edit-pane/DashboardEditPaneBehavior.tsx index 0be2f6511af..5e195498da2 100644 --- a/public/app/features/dashboard-scene/edit-pane/DummySelectedObject.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneBehavior.tsx @@ -1,19 +1,23 @@ import { useMemo } from 'react'; +import { SceneObjectBase } from '@grafana/scenes'; import { Input, TextArea } from '@grafana/ui'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; import { DashboardScene } from '../scene/DashboardScene'; +import { useLayoutCategory } from '../scene/layouts-shared/DashboardLayoutSelector'; import { EditableDashboardElement } from '../scene/types'; +import { getDashboardSceneFor } from '../utils/utils'; -export class DummySelectedObject implements EditableDashboardElement { +export class DashboardEditPaneBehavior extends SceneObjectBase implements EditableDashboardElement { public isEditableDashboardElement: true = true; - constructor(private dashboard: DashboardScene) {} - public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] { - const dashboard = this.dashboard; + const dashboard = getDashboardSceneFor(this); + + // When layout changes we need to update options list + const { body } = dashboard.useState(); const dashboardOptions = useMemo(() => { return new OptionsPaneCategoryDescriptor({ @@ -39,7 +43,9 @@ export class DummySelectedObject implements EditableDashboardElement { ); }, [dashboard]); - return [dashboardOptions]; + const layoutCategory = useLayoutCategory(body); + + return [dashboardOptions, layoutCategory]; } public getTypeName(): string { diff --git a/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx index cfb472342ea..ff2ef751086 100644 --- a/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx +++ b/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx @@ -1,21 +1,16 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { SceneObject } from '@grafana/scenes'; import { Stack, useStyles2 } from '@grafana/ui'; import { OptionsPaneCategory } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategory'; -import { DashboardScene } from '../scene/DashboardScene'; -import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types'; - -import { DummySelectedObject } from './DummySelectedObject'; +import { EditableDashboardElement } from '../scene/types'; export interface Props { - obj: SceneObject; + element: EditableDashboardElement; } -export function ElementEditPane({ obj }: Props) { - const element = getEditableElementFor(obj); +export function ElementEditPane({ element }: Props) { const categories = element.useEditPaneOptions(); const styles = useStyles2(getStyles); @@ -36,25 +31,6 @@ export function ElementEditPane({ obj }: Props) { ); } -function getEditableElementFor(obj: SceneObject): EditableDashboardElement { - if (isEditableDashboardElement(obj)) { - return obj; - } - - for (const behavior of obj.state.$behaviors ?? []) { - if (isEditableDashboardElement(behavior)) { - return behavior; - } - } - - // Temp thing to show somethin in edit pane - if (obj instanceof DashboardScene) { - return new DummySelectedObject(obj); - } - - throw new Error("Can't find editable element for selected object"); -} - function getStyles(theme: GrafanaTheme2) { return { noBorderTop: css({ diff --git a/public/app/features/dashboard-scene/edit-pane/VizPanelEditPaneBehavior.tsx b/public/app/features/dashboard-scene/edit-pane/VizPanelEditPaneBehavior.tsx new file mode 100644 index 00000000000..16ba425515d --- /dev/null +++ b/public/app/features/dashboard-scene/edit-pane/VizPanelEditPaneBehavior.tsx @@ -0,0 +1,130 @@ +import { useMemo } from 'react'; + +import { sceneGraph, SceneObjectBase, VizPanel } from '@grafana/scenes'; +import { Button } from '@grafana/ui'; +import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; +import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; +import { getVisualizationOptions2 } from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions'; + +import { + PanelBackgroundSwitch, + PanelDescriptionTextArea, + PanelFrameTitleInput, +} from '../panel-edit/getPanelFrameOptions'; +import { EditableDashboardElement, isDashboardLayoutItem } from '../scene/types'; +import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; + +export class VizPanelEditPaneBehavior extends SceneObjectBase implements EditableDashboardElement { + public isEditableDashboardElement: true = true; + + private getPanel(): VizPanel { + const panel = this.parent; + + if (!(panel instanceof VizPanel)) { + throw new Error('VizPanelEditPaneBehavior must have a VizPanel parent'); + } + + return panel; + } + + public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] { + const panel = this.getPanel(); + const layoutElement = panel.parent!; + + const panelOptions = useMemo(() => { + return new OptionsPaneCategoryDescriptor({ + title: 'Panel options', + id: 'panel-options', + isOpenDefault: true, + }) + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Title', + value: panel.state.title, + popularRank: 1, + render: function renderTitle() { + return ; + }, + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Description', + value: panel.state.description, + render: function renderDescription() { + return ; + }, + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Transparent background', + render: function renderTransparent() { + return ; + }, + }) + ); + }, [panel]); + + const layoutCategory = useMemo(() => { + if (isDashboardLayoutItem(layoutElement) && layoutElement.getOptions) { + return layoutElement.getOptions(); + } + return undefined; + }, [layoutElement]); + + const { options, fieldConfig, _pluginInstanceState } = panel.useState(); + const dataProvider = sceneGraph.getData(panel); + const { data } = dataProvider.useState(); + + const visualizationOptions = useMemo(() => { + const plugin = panel.getPlugin(); + if (!plugin) { + return []; + } + + return getVisualizationOptions2({ + panel, + data, + plugin: plugin, + eventBus: panel.getPanelContext().eventBus, + instanceState: _pluginInstanceState, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data, panel, options, fieldConfig, _pluginInstanceState]); + + const categories = [panelOptions]; + if (layoutCategory) { + categories.push(layoutCategory); + } + + categories.push(...visualizationOptions); + + return categories; + } + + public getTypeName(): string { + return 'Panel'; + } + + public onDelete = () => { + const layout = dashboardSceneGraph.getLayoutManagerFor(this); + layout.removePanel(this.getPanel()); + }; + + public renderActions(): React.ReactNode { + return ( + <> + + + + + ); + } +} diff --git a/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx b/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx index a75b82a2531..98c93b0635e 100644 --- a/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx +++ b/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx @@ -34,7 +34,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri value: panel.state.title, popularRank: 1, render: function renderTitle() { - return ; + return ; }, addon: config.featureToggles.dashgpt && ( ; + return ; }, addon: config.featureToggles.dashgpt && ( { - panel.setState({ - displayMode: panel.state.displayMode === 'transparent' ? 'default' : 'transparent', - }); - }} - /> - ); + return ; }, }) ) @@ -116,7 +106,7 @@ function ScenePanelLinksEditor({ panelLinks }: ScenePanelLinksEditorProps) { ); } -function PanelFrameTitle({ panel }: { panel: VizPanel }) { +export function PanelFrameTitleInput({ panel }: { panel: VizPanel }) { const { title } = panel.useState(); return ( @@ -128,7 +118,7 @@ function PanelFrameTitle({ panel }: { panel: VizPanel }) { ); } -function DescriptionTextArea({ panel }: { panel: VizPanel }) { +export function PanelDescriptionTextArea({ panel }: { panel: VizPanel }) { const { description } = panel.useState(); return ( @@ -140,6 +130,22 @@ function DescriptionTextArea({ panel }: { panel: VizPanel }) { ); } +export function PanelBackgroundSwitch({ panel }: { panel: VizPanel }) { + const { displayMode } = panel.useState(); + + return ( + { + panel.setState({ + displayMode: panel.state.displayMode === 'transparent' ? 'default' : 'transparent', + }); + }} + /> + ); +} + function setPanelTitle(panel: VizPanel, title: string) { panel.setState({ title: title, hoverHeader: getUpdatedHoverHeader(title, panel.state.$timeRange) }); } diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 80cf335232e..f72c9d30db7 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -60,6 +60,7 @@ export function ToolbarActions({ dashboard }: Props) { const canSaveAs = contextSrv.hasEditPermissionInFolders; const toolbarActions: ToolbarAction[] = []; + const leftActions: ToolbarAction[] = []; const styles = useStyles2(getStyles); const isEditingPanel = Boolean(editPanel); const isViewingPanel = Boolean(viewPanelScene); @@ -69,7 +70,8 @@ export function ToolbarActions({ dashboard }: Props) { // Means we are not in settings view, fullscreen panel or edit panel const isShowingDashboard = !editview && !isViewingPanel && !isEditingPanel; const isEditingAndShowingDashboard = isEditing && isShowingDashboard; - const showScopesSelector = config.featureToggles.singleTopNav && config.featureToggles.scopeFilters; + const showScopesSelector = config.featureToggles.singleTopNav && config.featureToggles.scopeFilters && !isEditing; + const dashboardNewLayouts = config.featureToggles.dashboardNewLayouts; if (!isEditingPanel) { // This adds the precence indicators in enterprise @@ -151,74 +153,135 @@ export function ToolbarActions({ dashboard }: Props) { addDynamicActions(toolbarActions, dynamicDashNavActions.right, 'icon-actions'); } - toolbarActions.push({ - group: 'add-panel', - condition: isEditingAndShowingDashboard, - render: () => ( - { - setIsAddPanelMenuOpen(isOpen); - DashboardInteractions.toolbarAddClick(); - }} - overlay={() => ( - - { - const vizPanel = dashboard.onCreateNewPanel(); - DashboardInteractions.toolbarAddButtonClicked({ item: 'add_visualization' }); - dashboard.setState({ editPanel: buildPanelEditScene(vizPanel, true) }); - }} - /> - { - dashboard.onShowAddLibraryPanelDrawer(); - DashboardInteractions.toolbarAddButtonClicked({ item: 'add_library_panel' }); - }} - /> - { - dashboard.onCreateNewRow(); - DashboardInteractions.toolbarAddButtonClicked({ item: 'add_row' }); - }} - /> - { - dashboard.pastePanel(); - DashboardInteractions.toolbarAddButtonClicked({ item: 'paste_panel' }); - }} - /> - - )} - placement="bottom" - offset={[0, 6]} - > + if (dashboardNewLayouts) { + leftActions.push({ + group: 'add-panel', + condition: isEditingAndShowingDashboard, + render: () => ( - - ), - }); + ), + }); + leftActions.push({ + group: 'add-panel', + condition: isEditingAndShowingDashboard, + render: () => ( + + ), + }); + leftActions.push({ + group: 'add-panel', + condition: isEditingAndShowingDashboard, + render: () => ( + + ), + }); + } else { + toolbarActions.push({ + group: 'add-panel', + condition: isEditingAndShowingDashboard, + render: () => ( + { + setIsAddPanelMenuOpen(isOpen); + DashboardInteractions.toolbarAddClick(); + }} + overlay={() => ( + + { + const vizPanel = dashboard.onCreateNewPanel(); + DashboardInteractions.toolbarAddButtonClicked({ item: 'add_visualization' }); + dashboard.setState({ editPanel: buildPanelEditScene(vizPanel, true) }); + }} + /> + { + dashboard.onShowAddLibraryPanelDrawer(); + DashboardInteractions.toolbarAddButtonClicked({ item: 'add_library_panel' }); + }} + /> + { + dashboard.onCreateNewRow(); + DashboardInteractions.toolbarAddButtonClicked({ item: 'add_row' }); + }} + /> + { + dashboard.pastePanel(); + DashboardInteractions.toolbarAddButtonClicked({ item: 'paste_panel' }); + }} + /> + + )} + placement="bottom" + offset={[0, 6]} + > + + + ), + }); + } toolbarActions.push({ group: 'playlist-actions', @@ -595,6 +658,20 @@ export function ToolbarActions({ dashboard }: Props) { }, }); + const rigthActionsElements: React.ReactNode[] = renderActionElements(toolbarActions); + const leftActionsElements: React.ReactNode[] = renderActionElements(leftActions); + const hasActionsToLeftAndRight = showScopesSelector || leftActionsElements.length > 0; + + return ( + + {showScopesSelector && } + {leftActionsElements.length > 0 && {leftActionsElements}} + {rigthActionsElements} + + ); +} + +function renderActionElements(toolbarActions: ToolbarAction[]) { const actionElements: React.ReactNode[] = []; let lastGroup = ''; @@ -610,13 +687,7 @@ export function ToolbarActions({ dashboard }: Props) { actionElements.push(action.render()); lastGroup = action.group; } - - return ( - - {showScopesSelector && } - {actionElements} - - ); + return actionElements; } function addDynamicActions( diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index 35a59a600c8..914b852d8e6 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -9,23 +9,18 @@ import { sceneUtils, SceneComponentProps, } from '@grafana/scenes'; -import { Button } from '@grafana/ui'; import { GRID_COLUMN_COUNT } from 'app/core/constants'; -import { Trans } from 'app/core/internationalization'; -import { DashboardInteractions } from '../../utils/interactions'; import { forceRenderChildren, getPanelIdForVizPanel, NEW_PANEL_HEIGHT, NEW_PANEL_WIDTH, getVizPanelKeyForPanelId, - getDefaultVizPanel, } from '../../utils/utils'; import { RowRepeaterBehavior } from '../RowRepeaterBehavior'; -import { LayoutEditChrome } from '../layouts-shared/LayoutEditChrome'; import { RowActions } from '../row-actions/RowActions'; -import { DashboardLayoutManager, LayoutEditorProps, LayoutRegistryItem } from '../types'; +import { DashboardLayoutManager, LayoutRegistryItem } from '../types'; import { DashboardGridItem } from './DashboardGridItem'; @@ -40,6 +35,8 @@ export class DefaultGridLayoutManager extends SceneObjectBase implements DashboardLayoutManager { + public isDashboardLayoutManager: true = true; + public editModeChanged(isEditing: boolean): void { const updateResizeAndDragging = () => { this.state.grid.setState({ isDraggable: isEditing, isResizable: isEditing }); @@ -387,48 +384,7 @@ export class DefaultGridLayoutManager }); } - public renderEditor() { - return ; - } - public static Component = ({ model }: SceneComponentProps) => { - if (!config.featureToggles.dashboardNewLayouts) { - return ; - } - - return ( - - - - ); + return ; }; } - -function DefaultGridLayoutEditor({ layoutManager }: LayoutEditorProps) { - return ( - <> - - - - - ); -} diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItem.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItem.tsx index d1671c3cb23..342d23b42de 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItem.tsx @@ -1,5 +1,8 @@ +import { css, cx } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; import { SceneObjectState, VizPanel, SceneObjectBase, SceneObject, SceneComponentProps } from '@grafana/scenes'; -import { Switch } from '@grafana/ui'; +import { Switch, useStyles2 } from '@grafana/ui'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; @@ -65,7 +68,22 @@ export class ResponsiveGridItem extends SceneObjectBase public static Component = ({ model }: SceneComponentProps) => { const { body } = model.useState(); + const style = useStyles2(getStyles); - return ; + return ( +
+ +
+ ); + }; +} + +function getStyles(theme: GrafanaTheme2) { + return { + wrapper: css({ + width: '100%', + height: '100%', + position: 'relative', + }), }; } diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx index 25ac031c34f..51219ae9178 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx @@ -1,12 +1,10 @@ import { SelectableValue } from '@grafana/data'; import { SceneComponentProps, SceneCSSGridLayout, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; -import { Button, Field, Select } from '@grafana/ui'; -import { Trans } from 'app/core/internationalization'; +import { Select } from '@grafana/ui'; +import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; -import { DashboardInteractions } from '../../utils/interactions'; -import { getDefaultVizPanel, getPanelIdForVizPanel, getVizPanelKeyForPanelId } from '../../utils/utils'; -import { LayoutEditChrome } from '../layouts-shared/LayoutEditChrome'; -import { DashboardLayoutManager, LayoutRegistryItem, LayoutEditorProps } from '../types'; +import { getPanelIdForVizPanel, getVizPanelKeyForPanelId } from '../../utils/utils'; +import { DashboardLayoutManager, LayoutRegistryItem } from '../types'; import { ResponsiveGridItem } from './ResponsiveGridItem'; @@ -18,6 +16,8 @@ export class ResponsiveGridLayoutManager extends SceneObjectBase implements DashboardLayoutManager { + public isDashboardLayoutManager: true = true; + public editModeChanged(isEditing: boolean): void {} public addPanel(vizPanel: VizPanel): void { @@ -72,8 +72,8 @@ export class ResponsiveGridLayoutManager return panels; } - public renderEditor() { - return ; + public getOptions(): OptionsPaneItemDescriptor[] { + return getOptions(this); } public getDescriptor(): LayoutRegistryItem { @@ -90,7 +90,13 @@ export class ResponsiveGridLayoutManager } public static createEmpty() { - return new ResponsiveGridLayoutManager({ layout: new SceneCSSGridLayout({ children: [] }) }); + return new ResponsiveGridLayoutManager({ + layout: new SceneCSSGridLayout({ + children: [], + templateColumns: 'repeat(auto-fit, minmax(400px, auto))', + autoRows: 'minmax(300px, auto)', + }), + }); } public static createFromLayout(layout: DashboardLayoutManager): ResponsiveGridLayoutManager { @@ -110,18 +116,22 @@ export class ResponsiveGridLayoutManager }); } + toSaveModel?() { + throw new Error('Method not implemented.'); + } + + activateRepeaters?(): void { + throw new Error('Method not implemented.'); + } public static Component = ({ model }: SceneComponentProps) => { - return ( - - - - ); + return ; }; } -function AutomaticGridEditor({ layoutManager }: LayoutEditorProps) { +function getOptions(layoutManager: ResponsiveGridLayoutManager): OptionsPaneItemDescriptor[] { + const options: OptionsPaneItemDescriptor[] = []; + const cssLayout = layoutManager.state.layout; - const { templateColumns, autoRows } = cssLayout.useState(); const rowOptions: Array> = []; const sizes = [100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 650]; @@ -143,39 +153,43 @@ function AutomaticGridEditor({ layoutManager }: LayoutEditorProps) => { - cssLayout.setState({ templateColumns: value.value }); - }; - - const onRowsChange = (value: SelectableValue) => { - cssLayout.setState({ autoRows: value.value }); - }; - - return ( - <> - - - - - + options.push( + new OptionsPaneItemDescriptor({ + title: 'Columns', + render: () => { + const { templateColumns } = cssLayout.useState(); + return ( + { + cssLayout.setState({ autoRows: value.value }); + }} + allowCustomValue={true} + /> + ); + }, + }) + ); + + return options; } diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx new file mode 100644 index 00000000000..1a224fadb0c --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx @@ -0,0 +1,242 @@ +import { css, cx } from '@emotion/css'; +import { useMemo, useRef } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; +import { SceneObjectState, SceneObjectBase, SceneComponentProps, sceneGraph } from '@grafana/scenes'; +import { Button, Icon, Input, RadioButtonGroup, Switch, useStyles2 } from '@grafana/ui'; +import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; +import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; + +import { getDashboardSceneFor, getDefaultVizPanel } from '../../utils/utils'; +import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector'; +import { DashboardLayoutManager, EditableDashboardElement, LayoutParent } from '../types'; + +import { RowsLayoutManager } from './RowsLayoutManager'; + +export interface RowItemState extends SceneObjectState { + layout: DashboardLayoutManager; + title?: string; + isCollapsed?: boolean; + isHeaderHidden?: boolean; + height?: 'expand' | 'min'; +} + +export class RowItem extends SceneObjectBase implements LayoutParent, EditableDashboardElement { + public isEditableDashboardElement: true = true; + + public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] { + const row = this; + + const rowOptions = useMemo(() => { + return new OptionsPaneCategoryDescriptor({ + title: 'Row options', + id: 'row-options', + isOpenDefault: true, + }) + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Title', + render: () => , + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Height', + render: () => , + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: 'Hide row header', + render: () => , + }) + ); + }, [row]); + + const { layout } = this.useState(); + const layoutOptions = useLayoutCategory(layout); + + return [rowOptions, layoutOptions]; + } + + public getTypeName(): string { + return 'Row'; + } + + public onDelete = () => { + const layout = sceneGraph.getAncestor(this, RowsLayoutManager); + layout.removeRow(this); + }; + + public renderActions(): React.ReactNode { + return ( + <> + + + + + ); + } + + public getLayout(): DashboardLayoutManager { + return this.state.layout; + } + + public switchLayout(layout: DashboardLayoutManager): void { + this.setState({ layout }); + } + + public onCollapseToggle = () => { + this.setState({ isCollapsed: !this.state.isCollapsed }); + }; + + public onAddPanel = () => { + const vizPanel = getDefaultVizPanel(); + this.state.layout.addPanel(vizPanel); + }; + + public onEdit = () => { + const dashboard = getDashboardSceneFor(this); + dashboard.state.editPane.selectObject(this); + }; + + public static Component = ({ model }: SceneComponentProps) => { + const { layout, title, isCollapsed, height = 'expand' } = model.useState(); + const { isEditing } = getDashboardSceneFor(model).useState(); + const styles = useStyles2(getStyles); + const titleInterpolated = sceneGraph.interpolate(model, title, undefined, 'text'); + const ref = useRef(null); + const shouldGrow = !isCollapsed && height === 'expand'; + + return ( +
+
+ + {isEditing &&
+ {!isCollapsed && } +
+ ); + }; +} + +function getStyles(theme: GrafanaTheme2) { + return { + rowHeader: css({ + width: '100%', + display: 'flex', + gap: theme.spacing(1), + padding: theme.spacing(0, 0, 0.5, 0), + margin: theme.spacing(0, 0, 1, 0), + alignItems: 'center', + + '&:hover, &:focus-within': { + '& > div': { + opacity: 1, + }, + }, + + '& > div': { + marginBottom: 0, + marginRight: theme.spacing(1), + }, + }), + rowTitleButton: css({ + display: 'flex', + alignItems: 'center', + cursor: 'pointer', + background: 'transparent', + border: 'none', + minWidth: 0, + gap: theme.spacing(1), + }), + rowTitle: css({ + fontSize: theme.typography.h5.fontSize, + fontWeight: theme.typography.fontWeightMedium, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + maxWidth: '100%', + flexGrow: 1, + minWidth: 0, + }), + wrapper: css({ + display: 'flex', + flexDirection: 'column', + width: '100%', + }), + wrapperGrow: css({ + flexGrow: 1, + }), + wrapperCollapsed: css({ + flexGrow: 0, + borderBottom: `1px solid ${theme.colors.border.weak}`, + }), + rowActions: css({ + display: 'flex', + opacity: 0, + }), + }; +} + +export function RowTitleInput({ row }: { row: RowItem }) { + const { title } = row.useState(); + + return row.setState({ title: e.currentTarget.value })} />; +} + +export function RowHeaderSwitch({ row }: { row: RowItem }) { + const { isHeaderHidden } = row.useState(); + + return ( + { + row.setState({ + isHeaderHidden: !row.state.isHeaderHidden, + }); + }} + /> + ); +} + +export function RowHeightSelect({ row }: { row: RowItem }) { + const { height = 'expand' } = row.useState(); + + const options = [ + { label: 'Expand', value: 'expand' as const }, + { label: 'Min', value: 'min' as const }, + ]; + + return ( + + row.setState({ + height: option, + }) + } + /> + ); +} diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx new file mode 100644 index 00000000000..1fa3e29bdf2 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -0,0 +1,113 @@ +import { css } from '@emotion/css'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { SceneComponentProps, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; +import { useStyles2 } from '@grafana/ui'; + +import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager'; +import { DashboardLayoutManager, LayoutRegistryItem } from '../types'; + +import { RowItem } from './RowItem'; + +interface RowsLayoutManagerState extends SceneObjectState { + rows: RowItem[]; +} + +export class RowsLayoutManager extends SceneObjectBase implements DashboardLayoutManager { + public isDashboardLayoutManager: true = true; + + public editModeChanged(isEditing: boolean): void {} + + public addPanel(vizPanel: VizPanel): void {} + + public addNewRow(): void { + this.setState({ + rows: [ + ...this.state.rows, + new RowItem({ + title: 'New row', + layout: ResponsiveGridLayoutManager.createEmpty(), + }), + ], + }); + } + + public getNextPanelId(): number { + return 0; + } + + public removePanel(panel: VizPanel) {} + + public removeRow(row: RowItem) { + this.setState({ + rows: this.state.rows.filter((r) => r !== row), + }); + } + + public duplicatePanel(panel: VizPanel): void { + throw new Error('Method not implemented.'); + } + + public getVizPanels(): VizPanel[] { + const panels: VizPanel[] = []; + + for (const row of this.state.rows) { + const innerPanels = row.state.layout.getVizPanels(); + panels.push(...innerPanels); + } + + return panels; + } + + public getOptions() { + return []; + } + + public getDescriptor(): LayoutRegistryItem { + return RowsLayoutManager.getDescriptor(); + } + + public static getDescriptor(): LayoutRegistryItem { + return { + name: 'Rows', + description: 'Rows layout', + id: 'rows-layout', + createFromLayout: RowsLayoutManager.createFromLayout, + }; + } + + public static createEmpty() { + return new RowsLayoutManager({ rows: [] }); + } + + public static createFromLayout(layout: DashboardLayoutManager): RowsLayoutManager { + const row = new RowItem({ layout: layout.clone(), title: 'Row title' }); + + return new RowsLayoutManager({ rows: [row] }); + } + + public static Component = ({ model }: SceneComponentProps) => { + const { rows } = model.useState(); + const styles = useStyles2(getStyles); + + return ( +
+ {rows.map((row) => ( + + ))} +
+ ); + }; +} + +function getStyles(theme: GrafanaTheme2) { + return { + wrapper: css({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(1), + height: '100%', + width: '100%', + }), + }; +} diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx new file mode 100644 index 00000000000..ff81e9c6ffc --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx @@ -0,0 +1,66 @@ +import { useMemo } from 'react'; + +import { Select } from '@grafana/ui'; +import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; +import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; + +import { DashboardLayoutManager, isLayoutParent, LayoutRegistryItem } from '../types'; + +import { layoutRegistry } from './layoutRegistry'; + +export interface Props { + layoutManager: DashboardLayoutManager; +} + +export function DashboardLayoutSelector({ layoutManager }: { layoutManager: DashboardLayoutManager }) { + const layouts = layoutRegistry.list(); + const options = layouts.map((layout) => ({ + label: layout.name, + value: layout, + })); + + const currentLayoutId = layoutManager.getDescriptor().id; + const currentLayoutOption = options.find((option) => option.value.id === currentLayoutId); + + return ( + changeLayoutTo(layoutManager, option.value!)} - /> - - {layoutManager.renderEditor?.()} - - )} - {children} - - ); -} - -function getStyles(theme: GrafanaTheme2) { - return { - editHeader: css({ - width: '100%', - display: 'flex', - gap: theme.spacing(1), - padding: theme.spacing(0, 1, 0.5, 1), - margin: theme.spacing(0, 0, 1, 0), - alignItems: 'flex-end', - borderBottom: `1px solid ${theme.colors.border.weak}`, - paddingBottom: theme.spacing(1), - - '&:hover, &:focus-within': { - '& > div': { - opacity: 1, - }, - }, - - '& > div': { - marginBottom: 0, - marginRight: theme.spacing(1), - }, - }), - wrapper: css({ - display: 'flex', - flexDirection: 'column', - flex: '1 1 0', - width: '100%', - }), - icon: css({ - display: 'flex', - alignItems: 'center', - cursor: 'pointer', - background: 'transparent', - border: 'none', - gap: theme.spacing(1), - }), - rowTitle: css({}), - rowActions: css({ - display: 'flex', - opacity: 0, - [theme.transitions.handleMotion('no-preference', 'reduce')]: { - transition: 'opacity 200ms ease-in', - }, - - '&:hover, &:focus-within': { - opacity: 1, - }, - }), - }; -} - -function changeLayoutTo(currentLayout: DashboardLayoutManager, newLayoutDescriptor: LayoutRegistryItem) { - const layoutParent = currentLayout.parent; - if (layoutParent && isLayoutParent(layoutParent)) { - layoutParent.switchLayout(newLayoutDescriptor.createFromLayout(currentLayout)); - } -} diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/layoutRegistry.ts b/public/app/features/dashboard-scene/scene/layouts-shared/layoutRegistry.ts index 2fbd9e9ecc2..95f345e71b9 100644 --- a/public/app/features/dashboard-scene/scene/layouts-shared/layoutRegistry.ts +++ b/public/app/features/dashboard-scene/scene/layouts-shared/layoutRegistry.ts @@ -2,8 +2,13 @@ import { Registry } from '@grafana/data'; import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager'; +import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; import { LayoutRegistryItem } from '../types'; export const layoutRegistry: Registry = new Registry(() => { - return [DefaultGridLayoutManager.getDescriptor(), ResponsiveGridLayoutManager.getDescriptor()]; + return [ + DefaultGridLayoutManager.getDescriptor(), + ResponsiveGridLayoutManager.getDescriptor(), + RowsLayoutManager.getDescriptor(), + ]; }); diff --git a/public/app/features/dashboard-scene/scene/types.ts b/public/app/features/dashboard-scene/scene/types.ts index fd50c4e2e28..a399e2d5938 100644 --- a/public/app/features/dashboard-scene/scene/types.ts +++ b/public/app/features/dashboard-scene/scene/types.ts @@ -1,21 +1,20 @@ import { BusEventWithPayload, RegistryItem } from '@grafana/data'; import { SceneObject, VizPanel } from '@grafana/scenes'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; +import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; /** * A scene object that usually wraps an underlying layout * Dealing with all the state management and editing of the layout */ export interface DashboardLayoutManager extends SceneObject { + /** Marks it as a DashboardLayoutManager */ + isDashboardLayoutManager: true; /** * Notify the layout manager that the edit mode has changed * @param isEditing */ editModeChanged(isEditing: boolean): void; - /** - * Not sure we will need this in the long run, we should be able to handle this inside internally - */ - getNextPanelId(): number; /** * Remove an element / panel * @param element @@ -54,7 +53,11 @@ export interface DashboardLayoutManager extends SceneObject { /** * Renders options and layout actions */ - renderEditor?(): React.ReactNode; + getOptions?(): OptionsPaneItemDescriptor[]; +} + +export function isDashboardLayoutManager(obj: SceneObject): obj is DashboardLayoutManager { + return 'isDashboardLayoutManager' in obj; } /** @@ -73,10 +76,6 @@ export interface LayoutRegistryItem extends RegistryItem { createFromSaveModel?(saveModel: any): void; } -export interface LayoutEditorProps { - layoutManager: T; -} - /** * This interface is needed to support layouts existing on different levels of the scene (DashboardScene and inside the TabsLayoutManager) */ diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 8c421b83e34..34f10356027 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -19,12 +19,14 @@ import { SceneDataLayerProvider, SceneDataLayerControls, UserActionEvent, + SceneObjectState, } from '@grafana/scenes'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { DashboardDTO, DashboardDataDTO } from 'app/types'; import { addPanelsOnLoadBehavior } from '../addToDashboard/addPanelsOnLoadBehavior'; +import { DashboardEditPaneBehavior } from '../edit-pane/DashboardEditPaneBehavior'; import { AlertStatesDataLayer } from '../scene/AlertStatesDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardControls } from '../scene/DashboardControls'; @@ -214,6 +216,32 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, }); } + const behaviorList: SceneObjectState['$behaviors'] = [ + new behaviors.CursorSync({ + sync: oldModel.graphTooltip, + }), + new behaviors.SceneQueryController(), + registerDashboardMacro, + registerPanelInteractionsReporter, + new DashboardEditPaneBehavior({}), + new behaviors.LiveNowTimer({ enabled: oldModel.liveNow }), + preserveDashboardSceneStateInLocalStorage, + addPanelsOnLoadBehavior, + new DashboardScopesFacade({ + reloadOnParamsChange: config.featureToggles.reloadDashboardsOnParamsChange && oldModel.meta.reloadOnParamsChange, + uid: oldModel.uid, + }), + new DashboardReloadBehavior({ + reloadOnParamsChange: config.featureToggles.reloadDashboardsOnParamsChange && oldModel.meta.reloadOnParamsChange, + uid: oldModel.uid, + version: oldModel.version, + }), + ]; + + if (config.featureToggles.dashboardNewLayouts) { + behaviorList.push(new DashboardEditPaneBehavior({})); + } + const dashboardScene = new DashboardScene({ description: oldModel.description, editable: oldModel.editable, @@ -242,28 +270,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel, UNSAFE_nowDelay: oldModel.timepicker?.nowDelay, }), $variables: variables, - $behaviors: [ - new behaviors.CursorSync({ - sync: oldModel.graphTooltip, - }), - new behaviors.SceneQueryController(), - registerDashboardMacro, - registerPanelInteractionsReporter, - new behaviors.LiveNowTimer({ enabled: oldModel.liveNow }), - preserveDashboardSceneStateInLocalStorage, - addPanelsOnLoadBehavior, - new DashboardScopesFacade({ - reloadOnParamsChange: - config.featureToggles.reloadDashboardsOnParamsChange && oldModel.meta.reloadOnParamsChange, - uid: oldModel.uid, - }), - new DashboardReloadBehavior({ - reloadOnParamsChange: - config.featureToggles.reloadDashboardsOnParamsChange && oldModel.meta.reloadOnParamsChange, - uid: oldModel.uid, - version: oldModel.version, - }), - ], + $behaviors: behaviorList, $data: new DashboardDataLayerSet({ annotationLayers, alertStatesLayer }), controls: new DashboardControls({ variableControls: [new VariableValueSelectors({}), new SceneDataLayerControls()], diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts index d7a08c729d4..fd6169589f4 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts @@ -34,7 +34,7 @@ import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLay import { transformSceneToSaveModelSchemaV2 } from './transformSceneToSaveModelSchemaV2'; -function setupDashboardScene(state: DashboardSceneState): DashboardScene { +function setupDashboardScene(state: Partial): DashboardScene { return new DashboardScene(state); } diff --git a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts index 9a0a0fa8f72..10edc9904d7 100644 --- a/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts +++ b/public/app/features/dashboard-scene/utils/dashboardSceneGraph.ts @@ -4,6 +4,8 @@ import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet'; import { DashboardScene } from '../scene/DashboardScene'; import { VizPanelLinks } from '../scene/PanelLinks'; +import { getLayoutManagerFor } from './utils'; + function getTimePicker(scene: DashboardScene) { return scene.state.controls?.state.timePicker; } @@ -53,4 +55,5 @@ export const dashboardSceneGraph = { getVizPanels, getDataLayers, getCursorSync, + getLayoutManagerFor, }; diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 09792696251..3e34ec23362 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -18,6 +18,7 @@ import { DashboardScene } from '../scene/DashboardScene'; import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior'; import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks'; import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; +import { DashboardLayoutManager, isDashboardLayoutManager } from '../scene/types'; export const NEW_PANEL_HEIGHT = 8; export const NEW_PANEL_WIDTH = 12; @@ -220,6 +221,7 @@ export function getDefaultVizPanel(): VizPanel { pluginId: 'timeseries', titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })], hoverHeaderOffset: 0, + $behaviors: [], menu: new VizPanelMenu({ $behaviors: [panelMenuBehavior], }), @@ -279,3 +281,16 @@ export function activateSceneObjectAndParentTree(so: SceneObject): CancelActivat * Useful when rendering a scene object out of context of it's parent */ export const activateInActiveParents = activateSceneObjectAndParentTree; + +export function getLayoutManagerFor(sceneObject: SceneObject): DashboardLayoutManager { + let parent = sceneObject.parent; + + while (parent) { + if (isDashboardLayoutManager(parent)) { + return parent; + } + parent = parent.parent; + } + + throw new Error('Could not find layout manager for scene object'); +} diff --git a/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor.tsx b/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor.tsx index debeed4e2a2..9106be64a5d 100644 --- a/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor.tsx +++ b/public/app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor.tsx @@ -1,5 +1,7 @@ import * as React from 'react'; +import { Box } from '@grafana/ui'; + import { OptionsPaneCategory } from './OptionsPaneCategory'; import { OptionsPaneItemDescriptor } from './OptionsPaneItemDescriptor'; @@ -57,6 +59,10 @@ export class OptionsPaneCategoryDescriptor { return this.props.customRender(); } + if (this.props.id === '') { + return {this.items.map((item) => item.render(searchQuery))}; + } + return ( {this.items.map((item) => item.render(searchQuery))}