diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx
index fbaf5aa5260..51cd262b24a 100644
--- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx
+++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx
@@ -18,6 +18,7 @@ import { isInCloneChain } from '../utils/clone';
import { getDashboardSceneFor } from '../utils/utils';
import { DashboardAddPane } from './DashboardAddPane';
+import { DashboardOutline } from './DashboardOutline';
import { ElementEditPane } from './ElementEditPane';
import { ElementSelection } from './ElementSelection';
import { useEditableElement } from './useEditableElement';
@@ -181,6 +182,8 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
return null;
}
+ const { typeId } = editableElement.getEditableElementInfo();
+
if (isCollapsed) {
return (
<>
@@ -197,7 +200,7 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
{openOverlay && (
-
+
)}
>
@@ -225,8 +228,8 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
{tab === 'add' &&
}
- {tab === 'configure' &&
}
- {tab === 'outline' &&
}
+ {tab === 'configure' &&
}
+ {tab === 'outline' &&
}
);
diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx
index b2fc279ccde..f51540115c5 100644
--- a/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx
+++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx
@@ -73,38 +73,40 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
return (
-
{
- if (evt.shiftKey) {
- return;
- }
+
+ {
+ if (evt.shiftKey) {
+ return;
+ }
- editPane.clearSelection();
- }}
- >
-
-
{controls}
-
-
-
{body}
+ editPane.clearSelection();
+ }}
+ >
+
+
{controls}
+
-
- {isEditing && (
- <>
-
-
- 0}
- />
-
- >
- )}
+ {isEditing && (
+ <>
+
+
+ 0}
+ />
+
+ >
+ )}
+
);
}
diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx
index 4699e8c1c3f..77255075393 100644
--- a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx
+++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx
@@ -7,14 +7,17 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan
import { DashboardScene } from '../scene/DashboardScene';
import { useLayoutCategory } from '../scene/layouts-shared/DashboardLayoutSelector';
-import { EditableDashboardElement } from '../scene/types/EditableDashboardElement';
+import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement';
export class DashboardEditableElement implements EditableDashboardElement {
public readonly isEditableDashboardElement = true;
- public readonly typeName = 'Dashboard';
public constructor(private dashboard: DashboardScene) {}
+ public getEditableElementInfo(): EditableDashboardElementInfo {
+ return { typeId: 'dashboard', icon: 'apps', name: t('dashboard.edit-pane.elements.dashboard', 'Dashboard') };
+ }
+
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
const dashboard = this.dashboard;
diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx
new file mode 100644
index 00000000000..4f302a638fd
--- /dev/null
+++ b/public/app/features/dashboard-scene/edit-pane/DashboardOutline.tsx
@@ -0,0 +1,159 @@
+import { css, cx } from '@emotion/css';
+import { useMemo, useState } from 'react';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { SceneObject, VizPanel } from '@grafana/scenes';
+import { Box, Icon, IconButton, Stack, Text, useElementSelection, useStyles2 } from '@grafana/ui';
+import { t, Trans } from 'app/core/internationalization';
+
+import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
+import { isInCloneChain } from '../utils/clone';
+import { getDashboardSceneFor } from '../utils/utils';
+
+import { DashboardEditPane } from './DashboardEditPane';
+import { getEditableElementFor, hasEditableElement } from './shared';
+
+export interface Props {
+ editPane: DashboardEditPane;
+}
+
+export function DashboardOutline({ editPane }: Props) {
+ const dashboard = getDashboardSceneFor(editPane);
+
+ return (
+
+
+
+ );
+}
+
+function DashboardOutlineNode({ sceneObject, expandable }: { sceneObject: SceneObject; expandable: boolean }) {
+ const [isExpanded, setIsExpanded] = useState(true);
+ 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 elementInfo = editableElement.getEditableElementInfo();
+
+ return (
+ <>
+
+ {expandable && (
+ setIsExpanded(!isExpanded)}
+ aria-label={
+ isExpanded
+ ? t('dashboard.outline.tree.item.collapse', 'Collapse item')
+ : t('dashboard.outline.tree.item.expand', 'Expand item')
+ }
+ />
+ )}
+
+
+ {expandable && isExpanded && (
+
+ {children.length > 0 ? (
+ children.map((child) => (
+
+ ))
+ ) : (
+
+ (empty)
+
+ )}
+
+ )}
+ >
+ );
+}
+
+function getStyles(theme: GrafanaTheme2) {
+ return {
+ container: css({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: theme.spacing(1),
+ marginLeft: theme.spacing(1),
+ paddingLeft: theme.spacing(1.5),
+ borderLeft: `1px solid ${theme.colors.border.medium}`,
+ }),
+ nodeButton: css({
+ boxShadow: 'none',
+ border: 'none',
+ background: 'transparent',
+ padding: theme.spacing(0.25, 1),
+ borderRadius: theme.shape.radius.default,
+ display: 'flex',
+ alignItems: 'center',
+ gap: theme.spacing(1),
+ overflow: 'hidden',
+ '&:hover': {
+ backgroundColor: theme.colors.action.hover,
+ },
+ '> span': {
+ whiteSpace: 'nowrap',
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ },
+ }),
+ nodeButtonSelected: css({
+ color: theme.colors.primary.text,
+ }),
+ nodeButtonClone: css({
+ color: theme.colors.text.secondary,
+ cursor: 'not-allowed',
+ }),
+ };
+}
+
+interface EditableElementConfig {
+ sceneObject: SceneObject;
+ expandable: boolean;
+}
+
+function collectEditableElementChildren(
+ sceneObject: SceneObject,
+ children: EditableElementConfig[] = []
+): EditableElementConfig[] {
+ sceneObject.forEachChild((child) => {
+ 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 });
+ }
+ } 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);
+ }
+ });
+
+ 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 16adfe8004a..47cd84ade4b 100644
--- a/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx
+++ b/public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx
@@ -14,13 +14,14 @@ export interface Props {
export function ElementEditPane({ element }: Props) {
const categories = element.useEditPaneOptions ? element.useEditPaneOptions() : [];
const styles = useStyles2(getStyles);
+ const elementInfo = element.getEditableElementInfo();
return (
{element.renderActions && (
diff --git a/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts b/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts
index cecd28020cf..2141781408d 100644
--- a/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts
+++ b/public/app/features/dashboard-scene/edit-pane/ElementSelection.ts
@@ -1,15 +1,14 @@
import { SceneObject, SceneObjectRef, VizPanel } from '@grafana/scenes';
import { ElementSelectionContextItem } from '@grafana/ui';
-import { DashboardScene } from '../scene/DashboardScene';
import { isBulkActionElement } from '../scene/types/BulkActionElement';
import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types/EditableDashboardElement';
import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement';
-import { DashboardEditableElement } from './DashboardEditableElement';
import { MultiSelectedObjectsEditableElement } from './MultiSelectedObjectsEditableElement';
import { MultiSelectedVizPanelsEditableElement } from './MultiSelectedVizPanelsEditableElement';
import { VizPanelEditableElement } from './VizPanelEditableElement';
+import { getEditableElementFor } from './shared';
export class ElementSelection {
private selectedObjects?: Map>;
@@ -121,24 +120,7 @@ export class ElementSelection {
private createSingleSelectedElement(): EditableDashboardElement | undefined {
const sceneObj = this.selectedObjects?.values().next().value?.resolve();
-
- if (!sceneObj) {
- return undefined;
- }
-
- if (isEditableDashboardElement(sceneObj)) {
- return sceneObj;
- }
-
- if (sceneObj instanceof VizPanel) {
- return new VizPanelEditableElement(sceneObj);
- }
-
- if (sceneObj instanceof DashboardScene) {
- return new DashboardEditableElement(sceneObj);
- }
-
- return undefined;
+ return getEditableElementFor(sceneObj);
}
private createMultiSelectedElement(): MultiSelectedEditableDashboardElement | undefined {
@@ -161,6 +143,7 @@ export class ElementSelection {
}
const bulkActionElements = [];
+
for (const sceneObject of sceneObjects) {
if (sceneObject instanceof VizPanel) {
const editableElement = new VizPanelEditableElement(sceneObject);
diff --git a/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx
index 3278deb3a17..a55b5f0d8c9 100644
--- a/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx
+++ b/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx
@@ -2,20 +2,24 @@ import { ReactNode } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { Stack, Text, Button } from '@grafana/ui';
-import { Trans } from 'app/core/internationalization';
+import { t, Trans } from 'app/core/internationalization';
import { BulkActionElement } from '../scene/types/BulkActionElement';
+import { EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement';
import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement';
export class MultiSelectedObjectsEditableElement implements MultiSelectedEditableDashboardElement {
public readonly isMultiSelectedEditableDashboardElement = true;
- public readonly typeName = 'Objects';
public readonly key: string;
constructor(private _elements: BulkActionElement[]) {
this.key = uuidv4();
}
+ public getEditableElementInfo(): EditableDashboardElementInfo {
+ return { name: t('dashboard.edit-pane.elements.objects', 'Objects'), typeId: 'objects', icon: 'folder' };
+ }
+
public renderActions(): ReactNode {
return (
diff --git a/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx
index 259ae9d91cc..afa29772adc 100644
--- a/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx
+++ b/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx
@@ -3,20 +3,24 @@ import { v4 as uuidv4 } from 'uuid';
import { VizPanel } from '@grafana/scenes';
import { Button, Stack, Text } from '@grafana/ui';
-import { Trans } from 'app/core/internationalization';
+import { t, Trans } from 'app/core/internationalization';
+import { EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement';
import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
export class MultiSelectedVizPanelsEditableElement implements MultiSelectedEditableDashboardElement {
public readonly isMultiSelectedEditableDashboardElement = true;
- public readonly typeName = 'Panels';
public readonly key: string;
constructor(private _panels: VizPanel[]) {
this.key = uuidv4();
}
+ public getEditableElementInfo(): EditableDashboardElementInfo {
+ return { name: t('dashboard.edit-pane.elements.panels', 'Panels'), typeId: 'panels', icon: 'folder' };
+ }
+
renderActions(): ReactNode {
return (
diff --git a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx
index 4d97803b8db..c20c61113fa 100644
--- a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx
+++ b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx
@@ -14,15 +14,22 @@ import {
} from '../panel-edit/getPanelFrameOptions';
import { BulkActionElement } from '../scene/types/BulkActionElement';
import { isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem';
-import { EditableDashboardElement } from '../scene/types/EditableDashboardElement';
+import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
export class VizPanelEditableElement implements EditableDashboardElement, BulkActionElement {
public readonly isEditableDashboardElement = true;
- public readonly typeName = 'Panel';
public constructor(private panel: VizPanel) {}
+ public getEditableElementInfo(): EditableDashboardElementInfo {
+ return {
+ typeId: 'panel',
+ icon: 'chart-line',
+ name: sceneGraph.interpolate(this.panel, this.panel.state.title, undefined, 'text'),
+ };
+ }
+
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
const panel = this.panel;
const layoutElement = panel.parent!;
diff --git a/public/app/features/dashboard-scene/edit-pane/shared.ts b/public/app/features/dashboard-scene/edit-pane/shared.ts
index 59ee48dde75..72c44d5dc2b 100644
--- a/public/app/features/dashboard-scene/edit-pane/shared.ts
+++ b/public/app/features/dashboard-scene/edit-pane/shared.ts
@@ -1,5 +1,55 @@
import { useSessionStorage } from 'react-use';
+import { SceneGridRow, SceneObject, VizPanel } from '@grafana/scenes';
+
+import { DashboardScene } from '../scene/DashboardScene';
+import { SceneGridRowEditableElement } from '../scene/layout-default/SceneGridRowEditableElement';
+import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types/EditableDashboardElement';
+
+import { DashboardEditableElement } from './DashboardEditableElement';
+import { VizPanelEditableElement } from './VizPanelEditableElement';
+
export function useEditPaneCollapsed() {
return useSessionStorage('grafana.dashboards.edit-pane.isCollapsed', false);
}
+
+export function getEditableElementFor(sceneObj: SceneObject | undefined): EditableDashboardElement | undefined {
+ if (!sceneObj) {
+ return undefined;
+ }
+
+ if (isEditableDashboardElement(sceneObj)) {
+ return sceneObj;
+ }
+
+ if (sceneObj instanceof VizPanel) {
+ return new VizPanelEditableElement(sceneObj);
+ }
+
+ if (sceneObj instanceof SceneGridRow) {
+ return new SceneGridRowEditableElement(sceneObj);
+ }
+
+ if (sceneObj instanceof DashboardScene) {
+ return new DashboardEditableElement(sceneObj);
+ }
+
+ 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;
+}
diff --git a/public/app/features/dashboard-scene/scene/layout-default/SceneGridRowEditableElement.tsx b/public/app/features/dashboard-scene/scene/layout-default/SceneGridRowEditableElement.tsx
new file mode 100644
index 00000000000..497b4087da9
--- /dev/null
+++ b/public/app/features/dashboard-scene/scene/layout-default/SceneGridRowEditableElement.tsx
@@ -0,0 +1,149 @@
+import { ReactNode, useMemo } from 'react';
+
+import { selectors } from '@grafana/e2e-selectors';
+import { sceneGraph, SceneGridRow, VizPanel } from '@grafana/scenes';
+import { Alert, Button, Input, TextLink } 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 { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect';
+import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants';
+import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
+
+import { getDashboardSceneFor, getLayoutManagerFor, getQueryRunnerFor } from '../../utils/utils';
+import { DashboardScene } from '../DashboardScene';
+import { BulkActionElement } from '../types/BulkActionElement';
+import { EditableDashboardElement, EditableDashboardElementInfo } from '../types/EditableDashboardElement';
+
+import { DefaultGridLayoutManager } from './DefaultGridLayoutManager';
+import { RowRepeaterBehavior } from './RowRepeaterBehavior';
+
+export class SceneGridRowEditableElement implements EditableDashboardElement, BulkActionElement {
+ public readonly isEditableDashboardElement = true;
+
+ public constructor(private _row: SceneGridRow) {}
+
+ public getEditableElementInfo(): EditableDashboardElementInfo {
+ return {
+ typeId: 'panel',
+ icon: 'line-alt',
+ name: sceneGraph.interpolate(this._row, this._row.state.title, undefined, 'text'),
+ };
+ }
+
+ public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
+ const row = this._row;
+
+ const rowOptions = useMemo(() => {
+ return new OptionsPaneCategoryDescriptor({
+ title: t('dashboard.default-layout.row-options.title', 'Row options'),
+ id: 'row-options',
+ isOpenDefault: true,
+ }).addItem(
+ new OptionsPaneItemDescriptor({
+ title: t('dashboard.default-layout.row-options.form.title', 'Title'),
+ render: () => ,
+ })
+ );
+ }, [row]);
+
+ const rowRepeatOptions = useMemo(() => {
+ const dashboard = getDashboardSceneFor(row);
+
+ return new OptionsPaneCategoryDescriptor({
+ title: t('dashboard.default-layout.row-options.repeat.title', 'Repeat options'),
+ id: 'row-repeat-options',
+ isOpenDefault: true,
+ }).addItem(
+ new OptionsPaneItemDescriptor({
+ title: t('dashboard.default-layout.row-options.repeat.variable.title', 'Variable'),
+ render: () => ,
+ })
+ );
+ }, [row]);
+
+ return [rowOptions, rowRepeatOptions];
+ }
+
+ public onDelete() {
+ const layoutManager = getLayoutManagerFor(this._row);
+
+ if (layoutManager instanceof DefaultGridLayoutManager) {
+ layoutManager.removeRow(this._row);
+ }
+ }
+
+ public renderActions(): ReactNode {
+ return (
+ <>
+