Dashboard: Outline using EditableElement interface (#101076)
This commit is contained in:
@@ -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 && (
|
||||
<Resizable className={cx(styles.fixed, styles.container)} defaultSize={{ height: '100%', width: '20vw' }}>
|
||||
<ElementEditPane element={editableElement} key={editableElement.typeName} />
|
||||
<ElementEditPane element={editableElement} key={typeId} />
|
||||
</Resizable>
|
||||
)}
|
||||
</>
|
||||
@@ -225,8 +228,8 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
|
||||
</TabsBar>
|
||||
<div className={styles.tabContent}>
|
||||
{tab === 'add' && <DashboardAddPane editPane={editPane} />}
|
||||
{tab === 'configure' && <ElementEditPane element={editableElement} key={editableElement.typeName} />}
|
||||
{tab === 'outline' && <div />}
|
||||
{tab === 'configure' && <ElementEditPane element={editableElement} key={typeId} />}
|
||||
{tab === 'outline' && <DashboardOutline editPane={editPane} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -73,38 +73,40 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
|
||||
|
||||
return (
|
||||
<div {...containerProps} style={containerStyle}>
|
||||
<div
|
||||
{...primaryProps}
|
||||
className={cx(primaryProps.className, styles.canvasWithSplitter)}
|
||||
onPointerDown={(evt) => {
|
||||
if (evt.shiftKey) {
|
||||
return;
|
||||
}
|
||||
<ElementSelectionContext.Provider value={selectionContext}>
|
||||
<div
|
||||
{...primaryProps}
|
||||
className={cx(primaryProps.className, styles.canvasWithSplitter)}
|
||||
onPointerDown={(evt) => {
|
||||
if (evt.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
editPane.clearSelection();
|
||||
}}
|
||||
>
|
||||
<NavToolbarActions dashboard={dashboard} />
|
||||
<div className={cx(!isEditing && styles.controlsWrapperSticky)}>{controls}</div>
|
||||
<div className={styles.bodyWrapper}>
|
||||
<div className={cx(styles.body, isEditing && styles.bodyEditing)} ref={onBodyRef}>
|
||||
<ElementSelectionContext.Provider value={selectionContext}>{body}</ElementSelectionContext.Provider>
|
||||
editPane.clearSelection();
|
||||
}}
|
||||
>
|
||||
<NavToolbarActions dashboard={dashboard} />
|
||||
<div className={cx(!isEditing && styles.controlsWrapperSticky)}>{controls}</div>
|
||||
<div className={styles.bodyWrapper}>
|
||||
<div className={cx(styles.body, isEditing && styles.bodyEditing)} ref={onBodyRef}>
|
||||
{body}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isEditing && (
|
||||
<>
|
||||
<div {...splitterProps} data-edit-pane-splitter={true} />
|
||||
<div {...secondaryProps} className={cx(secondaryProps.className, styles.editPane)}>
|
||||
<DashboardEditPaneRenderer
|
||||
editPane={editPane}
|
||||
isCollapsed={splitterState.collapsed}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
openOverlay={selectionContext.selected.length > 0}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isEditing && (
|
||||
<>
|
||||
<div {...splitterProps} data-edit-pane-splitter={true} />
|
||||
<div {...secondaryProps} className={cx(secondaryProps.className, styles.editPane)}>
|
||||
<DashboardEditPaneRenderer
|
||||
editPane={editPane}
|
||||
isCollapsed={splitterState.collapsed}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
openOverlay={selectionContext.selected.length > 0}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</ElementSelectionContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Box padding={1} gap={0.5} display="flex" direction="column">
|
||||
<DashboardOutlineNode sceneObject={dashboard} expandable />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={0.5}
|
||||
alignItems="center"
|
||||
role="presentation"
|
||||
aria-expanded={expandable ? isExpanded : undefined}
|
||||
aria-owns={expandable ? key : undefined}
|
||||
>
|
||||
{expandable && (
|
||||
<IconButton
|
||||
name={isExpanded ? 'angle-down' : 'angle-right'}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
aria-label={
|
||||
isExpanded
|
||||
? t('dashboard.outline.tree.item.collapse', 'Collapse item')
|
||||
: t('dashboard.outline.tree.item.expand', 'Expand item')
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
role="treeitem"
|
||||
className={cx(styles.nodeButton, isCloned && styles.nodeButtonClone, isSelected && styles.nodeButtonSelected)}
|
||||
onPointerDown={(evt) => onSelect?.(evt)}
|
||||
>
|
||||
<Icon name={elementInfo.icon} />
|
||||
<span>{elementInfo.name}</span>
|
||||
</button>
|
||||
</Stack>
|
||||
{expandable && isExpanded && (
|
||||
<div className={styles.container} role="group">
|
||||
{children.length > 0 ? (
|
||||
children.map((child) => (
|
||||
<DashboardOutlineNode
|
||||
key={child.sceneObject.state.key}
|
||||
sceneObject={child.sceneObject}
|
||||
expandable={child.expandable}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Text element="p" color="secondary">
|
||||
<Trans i18nKey="dashboard.outline.tree.item.empty">(empty)</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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 (
|
||||
<Stack direction="column" gap={0}>
|
||||
{element.renderActions && (
|
||||
<OptionsPaneCategory
|
||||
id="selected-item"
|
||||
title={element.typeName}
|
||||
title={elementInfo.name}
|
||||
isOpenDefault={true}
|
||||
className={styles.noBorderTop}
|
||||
>
|
||||
|
||||
@@ -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<string, SceneObjectRef<SceneObject>>;
|
||||
@@ -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);
|
||||
|
||||
+6
-2
@@ -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 (
|
||||
<Stack direction="column">
|
||||
|
||||
+6
-2
@@ -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 (
|
||||
<Stack direction="column">
|
||||
|
||||
@@ -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!;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+149
@@ -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: () => <RowTitleInput row={row} />,
|
||||
})
|
||||
);
|
||||
}, [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: () => <RowRepeatSelect row={row} dashboard={dashboard} />,
|
||||
})
|
||||
);
|
||||
}, [row]);
|
||||
|
||||
return [rowOptions, rowRepeatOptions];
|
||||
}
|
||||
|
||||
public onDelete() {
|
||||
const layoutManager = getLayoutManagerFor(this._row);
|
||||
|
||||
if (layoutManager instanceof DefaultGridLayoutManager) {
|
||||
layoutManager.removeRow(this._row);
|
||||
}
|
||||
}
|
||||
|
||||
public renderActions(): ReactNode {
|
||||
return (
|
||||
<>
|
||||
<Button size="sm" variant="destructive" fill="outline" onClick={() => this.onDelete()} icon="trash-alt" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function RowTitleInput({ row }: { row: SceneGridRow }) {
|
||||
const { title } = row.useState();
|
||||
|
||||
return <Input value={title} onChange={(e) => row.setState({ title: e.currentTarget.value })} />;
|
||||
}
|
||||
|
||||
function RowRepeatSelect({ row, dashboard }: { row: SceneGridRow; dashboard: DashboardScene }) {
|
||||
const { $behaviors, children } = row.useState();
|
||||
let repeatBehavior = $behaviors?.find((b) => b instanceof RowRepeaterBehavior);
|
||||
const vizPanels = useMemo(
|
||||
() => children.reduce<VizPanel[]>((acc, child) => [...acc, ...sceneGraph.findDescendents(child, VizPanel)], []),
|
||||
[children]
|
||||
);
|
||||
|
||||
const isAnyPanelUsingDashboardDS = vizPanels.some((vizPanel) => {
|
||||
const runner = getQueryRunnerFor(vizPanel);
|
||||
return (
|
||||
runner?.state.datasource?.uid === SHARED_DASHBOARD_QUERY ||
|
||||
(runner?.state.datasource?.uid === MIXED_DATASOURCE_NAME &&
|
||||
runner?.state.queries.some((query) => query.datasource?.uid === SHARED_DASHBOARD_QUERY))
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<RepeatRowSelect2
|
||||
sceneContext={dashboard}
|
||||
repeat={repeatBehavior?.state.variableName}
|
||||
onChange={(repeat) => {
|
||||
if (repeat) {
|
||||
repeatBehavior?.removeBehavior();
|
||||
repeatBehavior = new RowRepeaterBehavior({ variableName: repeat });
|
||||
row.setState({ $behaviors: [...(row.state.$behaviors ?? []), repeatBehavior] });
|
||||
} else {
|
||||
repeatBehavior?.removeBehavior();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{isAnyPanelUsingDashboardDS ? (
|
||||
<Alert
|
||||
data-testid={selectors.pages.Dashboard.Rows.Repeated.ConfigSection.warningMessage}
|
||||
severity="warning"
|
||||
title=""
|
||||
topSpacing={3}
|
||||
bottomSpacing={0}
|
||||
>
|
||||
<p>
|
||||
<Trans i18nKey="dashboard.default-layout.row-options.form.repeat-for.warning.text">
|
||||
Panels in this row use the {{ SHARED_DASHBOARD_QUERY }} data source. These panels will reference the panel
|
||||
in the original row, not the ones in the repeated rows.
|
||||
</Trans>
|
||||
</p>
|
||||
<TextLink
|
||||
external
|
||||
href={
|
||||
'https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/create-dashboard/#configure-repeating-rows'
|
||||
}
|
||||
>
|
||||
<Trans i18nKey="dashboard.default-layout.row-options.form.repeat-for.learn-more">Learn more</Trans>
|
||||
</TextLink>
|
||||
</Alert>
|
||||
) : undefined}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components
|
||||
import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager';
|
||||
import { BulkActionElement } from '../types/BulkActionElement';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { EditableDashboardElement } from '../types/EditableDashboardElement';
|
||||
import { EditableDashboardElement, EditableDashboardElementInfo } from '../types/EditableDashboardElement';
|
||||
import { LayoutParent } from '../types/LayoutParent';
|
||||
|
||||
import { getEditOptions, renderActions } from './RowItemEditor';
|
||||
@@ -35,7 +35,6 @@ export class RowItem
|
||||
});
|
||||
|
||||
public readonly isEditableDashboardElement = true;
|
||||
public readonly typeName = 'Row';
|
||||
|
||||
public constructor(state?: Partial<RowItemState>) {
|
||||
super({
|
||||
@@ -45,6 +44,10 @@ export class RowItem
|
||||
});
|
||||
}
|
||||
|
||||
public getEditableElementInfo(): EditableDashboardElementInfo {
|
||||
return { typeId: 'row', icon: 'line-alt', name: sceneGraph.interpolate(this, this.state.title, undefined, 'text') };
|
||||
}
|
||||
|
||||
public getLayout(): DashboardLayoutManager {
|
||||
return this.state.layout;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
|
||||
import { EditableDashboardElementInfo } from '../types/EditableDashboardElement';
|
||||
import { MultiSelectedEditableDashboardElement } from '../types/MultiSelectedEditableDashboardElement';
|
||||
|
||||
import { RowItem } from './RowItem';
|
||||
@@ -10,13 +12,16 @@ import { getEditOptions, renderActions } from './RowItemsEditor';
|
||||
|
||||
export class RowItems implements MultiSelectedEditableDashboardElement {
|
||||
public readonly isMultiSelectedEditableDashboardElement = true;
|
||||
public readonly typeName = 'Rows';
|
||||
public readonly key: string;
|
||||
|
||||
public constructor(private _rows: RowItem[]) {
|
||||
this.key = uuidv4();
|
||||
}
|
||||
|
||||
public getEditableElementInfo(): EditableDashboardElementInfo {
|
||||
return { name: t('dashboard.edit-pane.elements.rows', 'Rows'), typeId: 'rows', icon: 'folder' };
|
||||
}
|
||||
|
||||
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
|
||||
return getEditOptions(this);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components
|
||||
import { ResponsiveGridLayoutManager } from '../layout-responsive-grid/ResponsiveGridLayoutManager';
|
||||
import { BulkActionElement } from '../types/BulkActionElement';
|
||||
import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { EditableDashboardElement } from '../types/EditableDashboardElement';
|
||||
import { EditableDashboardElement, EditableDashboardElementInfo } from '../types/EditableDashboardElement';
|
||||
import { LayoutParent } from '../types/LayoutParent';
|
||||
|
||||
import { getEditOptions, renderActions } from './TabItemEditor';
|
||||
@@ -31,7 +31,6 @@ export class TabItem
|
||||
});
|
||||
|
||||
public readonly isEditableDashboardElement = true;
|
||||
public readonly typeName = 'Tab';
|
||||
|
||||
constructor(state?: Partial<TabItemState>) {
|
||||
super({
|
||||
@@ -41,6 +40,10 @@ export class TabItem
|
||||
});
|
||||
}
|
||||
|
||||
public getEditableElementInfo(): EditableDashboardElementInfo {
|
||||
return { typeId: 'tab', icon: 'tag-alt', name: sceneGraph.interpolate(this, this.state.title, undefined, 'text') };
|
||||
}
|
||||
|
||||
public getLayout(): DashboardLayoutManager {
|
||||
return this.state.layout;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
|
||||
import { EditableDashboardElementInfo } from '../types/EditableDashboardElement';
|
||||
import { MultiSelectedEditableDashboardElement } from '../types/MultiSelectedEditableDashboardElement';
|
||||
|
||||
import { TabItem } from './TabItem';
|
||||
@@ -10,13 +12,16 @@ import { getEditOptions, renderActions } from './TabItemsEditor';
|
||||
|
||||
export class TabItems implements MultiSelectedEditableDashboardElement {
|
||||
public readonly isMultiSelectedEditableDashboardElement = true;
|
||||
public readonly typeName = 'Tabs';
|
||||
public readonly key: string;
|
||||
|
||||
public constructor(private _tabs: TabItem[]) {
|
||||
this.key = uuidv4();
|
||||
}
|
||||
|
||||
public getEditableElementInfo(): EditableDashboardElementInfo {
|
||||
return { name: t('dashboard.edit-pane.elements.tabs', 'Tabs'), typeId: 'tabs', icon: 'folder' };
|
||||
}
|
||||
|
||||
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
|
||||
return getEditOptions(this);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { IconName } from '@grafana/data';
|
||||
import { SceneObject } from '@grafana/scenes';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
|
||||
@@ -14,10 +15,8 @@ export interface EditableDashboardElement {
|
||||
*/
|
||||
isEditableDashboardElement: true;
|
||||
|
||||
/**
|
||||
* Type name of the element
|
||||
*/
|
||||
typeName: Readonly<string>;
|
||||
/** A descriptor used by editing pane */
|
||||
getEditableElementInfo(): EditableDashboardElementInfo;
|
||||
|
||||
/**
|
||||
* Hook that returns edit pane options
|
||||
@@ -35,6 +34,12 @@ export interface EditableDashboardElement {
|
||||
createMultiSelectedElement?(items: SceneObject[]): MultiSelectedEditableDashboardElement;
|
||||
}
|
||||
|
||||
export interface EditableDashboardElementInfo {
|
||||
name: string;
|
||||
typeId: string;
|
||||
icon: IconName;
|
||||
}
|
||||
|
||||
export function isEditableDashboardElement(obj: object): obj is EditableDashboardElement {
|
||||
return 'isEditableDashboardElement' in obj;
|
||||
}
|
||||
|
||||
+4
-4
@@ -2,16 +2,16 @@ import { ReactNode } from 'react';
|
||||
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
|
||||
import { EditableDashboardElementInfo } from './EditableDashboardElement';
|
||||
|
||||
export interface MultiSelectedEditableDashboardElement {
|
||||
/**
|
||||
* Marks this object as an element that can be selected and edited directly on the canvas
|
||||
*/
|
||||
isMultiSelectedEditableDashboardElement: true;
|
||||
|
||||
/**
|
||||
* Type name of the element
|
||||
*/
|
||||
typeName: Readonly<string>;
|
||||
/** A descriptor used by editing pane */
|
||||
getEditableElementInfo(): EditableDashboardElementInfo;
|
||||
|
||||
/**
|
||||
* Extremely useful for being able to access the useState inside the contained items
|
||||
|
||||
@@ -1043,10 +1043,24 @@
|
||||
},
|
||||
"modal": {
|
||||
"title": "Row options"
|
||||
}
|
||||
},
|
||||
"repeat": {
|
||||
"title": "Repeat options",
|
||||
"variable": {
|
||||
"title": "Variable"
|
||||
}
|
||||
},
|
||||
"title": "Row options"
|
||||
}
|
||||
},
|
||||
"edit-pane": {
|
||||
"elements": {
|
||||
"dashboard": "Dashboard",
|
||||
"objects": "Objects",
|
||||
"panels": "Panels",
|
||||
"rows": "Rows",
|
||||
"tabs": "Tabs"
|
||||
},
|
||||
"objects": {
|
||||
"multi-select": {
|
||||
"selection-number": "No. of objects selected: {{length}}"
|
||||
@@ -1151,6 +1165,15 @@
|
||||
"title": "Dashboard options",
|
||||
"title-option": "Title"
|
||||
},
|
||||
"outline": {
|
||||
"tree": {
|
||||
"item": {
|
||||
"collapse": "Collapse item",
|
||||
"empty": "(empty)",
|
||||
"expand": "Expand item"
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-edit": {
|
||||
"alerting-tab": {
|
||||
"dashboard-not-saved": "Dashboard must be saved before alerts can be added.",
|
||||
|
||||
@@ -1043,10 +1043,24 @@
|
||||
},
|
||||
"modal": {
|
||||
"title": "Ŗőŵ őpŧįőʼnş"
|
||||
}
|
||||
},
|
||||
"repeat": {
|
||||
"title": "Ŗępęäŧ őpŧįőʼnş",
|
||||
"variable": {
|
||||
"title": "Väřįäþľę"
|
||||
}
|
||||
},
|
||||
"title": "Ŗőŵ őpŧįőʼnş"
|
||||
}
|
||||
},
|
||||
"edit-pane": {
|
||||
"elements": {
|
||||
"dashboard": "Đäşĥþőäřđ",
|
||||
"objects": "Øþĵęčŧş",
|
||||
"panels": "Päʼnęľş",
|
||||
"rows": "Ŗőŵş",
|
||||
"tabs": "Ŧäþş"
|
||||
},
|
||||
"objects": {
|
||||
"multi-select": {
|
||||
"selection-number": "Ńő. őƒ őþĵęčŧş şęľęčŧęđ: {{length}}"
|
||||
@@ -1151,6 +1165,15 @@
|
||||
"title": "Đäşĥþőäřđ őpŧįőʼnş",
|
||||
"title-option": "Ŧįŧľę"
|
||||
},
|
||||
"outline": {
|
||||
"tree": {
|
||||
"item": {
|
||||
"collapse": "Cőľľäpşę įŧęm",
|
||||
"empty": "(ęmpŧy)",
|
||||
"expand": "Ēχpäʼnđ įŧęm"
|
||||
}
|
||||
}
|
||||
},
|
||||
"panel-edit": {
|
||||
"alerting-tab": {
|
||||
"dashboard-not-saved": "Đäşĥþőäřđ mūşŧ þę şävęđ þęƒőřę äľęřŧş čäʼn þę äđđęđ.",
|
||||
|
||||
Reference in New Issue
Block a user