Dashboard: Edit pane header design and refactorings (#101851)

* Edit pane header

* Update

* Update

* Progress

* Progress

* Update

* Delete

* Delete icon button

* Update

* Fix

* remove need for arrow functions

* Update

* update

* Update

* Update

* Update
This commit is contained in:
Torkel Ödegaard
2025-03-10 15:03:54 +01:00
committed by GitHub
parent cf60c4e77f
commit e7e0b689c2
24 changed files with 226 additions and 409 deletions
@@ -263,7 +263,7 @@ function getStyles(theme: GrafanaTheme2) {
}),
tabsbar: css({
padding: theme.spacing(0, 1),
margin: theme.spacing(0.5, 1),
margin: theme.spacing(0.5, 0),
}),
expandOptionsWrapper: css({
display: 'flex',
@@ -15,7 +15,11 @@ export class DashboardEditableElement implements EditableDashboardElement {
public constructor(private dashboard: DashboardScene) {}
public getEditableElementInfo(): EditableDashboardElementInfo {
return { typeId: 'dashboard', icon: 'apps', name: t('dashboard.edit-pane.elements.dashboard', 'Dashboard') };
return {
typeName: t('dashboard.edit-pane.elements.dashboard', 'Dashboard'),
icon: 'apps',
instanceName: this.dashboard.state.title,
};
}
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
@@ -25,11 +29,7 @@ export class DashboardEditableElement implements EditableDashboardElement {
const { body } = dashboard.useState();
const dashboardOptions = useMemo(() => {
const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({
title: t('dashboard.options.title', 'Dashboard options'),
id: 'dashboard-options',
isOpenable: false,
})
const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({ title: '', id: 'dashboard-options' })
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.options.title-option', 'Title'),
@@ -65,7 +65,7 @@ function DashboardOutlineNode({ sceneObject, expandable }: { sceneObject: SceneO
onPointerDown={(evt) => onSelect?.(evt)}
>
<Icon name={elementInfo.icon} />
<span>{elementInfo.name}</span>
<span>{elementInfo.instanceName}</span>
</button>
</Stack>
{expandable && isExpanded && (
@@ -1,57 +1,86 @@
import { Dropdown, Button, IconButton, Menu, Stack, Icon } from '@grafana/ui';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, Menu, Stack, Text, useStyles2, ConfirmButton, Dropdown, Icon } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { EditableDashboardElement } from '../scene/types/EditableDashboardElement';
interface EditPaneHeaderProps {
title: string;
onDelete?: () => void;
onCopy?: () => void;
onDuplicate?: () => void;
element: EditableDashboardElement;
}
export const EditPaneHeader = ({ title, onDelete, onCopy, onDuplicate }: EditPaneHeaderProps) => {
const addCopyOrDuplicate = onCopy || onDuplicate;
export function EditPaneHeader({ element }: EditPaneHeaderProps) {
const elementInfo = element.getEditableElementInfo();
const styles = useStyles2(getStyles);
const onCopy = element.onCopy?.bind(element);
const onDuplicate = element.onDuplicate?.bind(element);
const onDelete = element.onDelete?.bind(element);
return (
<Stack justifyContent="space-between" alignItems="center" width="100%">
<span>{title}</span>
<Stack alignItems="center">
{addCopyOrDuplicate ? (
<Dropdown overlay={<MenuItems onCopy={onCopy} onDuplicate={onDuplicate} />}>
<div className={styles.wrapper}>
<Text variant="h5">{elementInfo.typeName}</Text>
<Stack direction="row" gap={1}>
{(onCopy || onDelete) && (
<Dropdown
overlay={
<Menu>
{onCopy ? (
<Menu.Item icon="copy" label={t('dashboard.layout.common.copy', 'Copy')} onClick={onCopy} />
) : null}
{onDuplicate ? (
<Menu.Item
icon="file-copy-alt"
label={t('dashboard.layout.common.duplicate', 'Duplicate')}
onClick={onDuplicate}
/>
) : null}
</Menu>
}
>
<Button
tooltip={t('dashboard.layout.common.copy-or-duplicate', 'Copy or Duplicate')}
tooltipPlacement="bottom"
variant="secondary"
fill="text"
size="md"
size="sm"
icon="copy"
>
<Icon name="copy" /> <Icon name="angle-down" />
<Icon name="angle-down" />
</Button>
</Dropdown>
) : null}
)}
<IconButton
size="md"
variant="secondary"
onClick={onDelete}
name="trash-alt"
tooltip={t('dashboard.layout.common.delete', 'Delete')}
/>
{onDelete && (
<ConfirmButton
onConfirm={onDelete}
confirmText="Confirm"
confirmVariant="destructive"
size="sm"
closeOnConfirm={true}
>
<Button
size="sm"
variant="destructive"
fill="outline"
icon="trash-alt"
tooltip={t('dashboard.layout.common.delete', 'Delete')}
/>
</ConfirmButton>
)}
</Stack>
</Stack>
</div>
);
};
}
type MenuItemsProps = {
onCopy?: () => void;
onDuplicate?: () => void;
};
const MenuItems = ({ onCopy, onDuplicate }: MenuItemsProps) => {
return (
<Menu>
{onCopy ? <Menu.Item label={t('dashboard.layout.common.copy', 'Copy')} onClick={onCopy} /> : null}
{onDuplicate ? (
<Menu.Item label={t('dashboard.layout.common.duplicate', 'Duplicate')} onClick={onDuplicate} />
) : null}
</Menu>
);
};
function getStyles(theme: GrafanaTheme2) {
return {
wrapper: css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: theme.spacing(2),
borderBottom: `1px solid ${theme.colors.border.weak}`,
}),
};
}
@@ -1,50 +1,20 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Stack, useStyles2 } from '@grafana/ui';
import { OptionsPaneCategory } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategory';
import { Stack } from '@grafana/ui';
import { EditableDashboardElement } from '../scene/types/EditableDashboardElement';
import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement';
import { EditPaneHeader } from './EditPaneHeader';
export interface Props {
element: EditableDashboardElement | MultiSelectedEditableDashboardElement;
element: EditableDashboardElement;
}
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={elementInfo.name}
isOpenDefault={true}
className={styles.noBorderTop}
renderTitle={element.renderTitle}
isOpenable={element.isOpenable}
>
<div className={styles.actionsBox}>{element.renderActions()}</div>
</OptionsPaneCategory>
)}
<EditPaneHeader element={element} />
{categories.map((cat) => cat.render())}
</Stack>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
noBorderTop: css({
borderTop: 'none',
}),
actionsBox: css({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
paddingBottom: theme.spacing(1),
}),
};
}
@@ -1,17 +1,14 @@
import { SceneObject, SceneObjectRef, VizPanel } from '@grafana/scenes';
import { SceneObject, SceneObjectRef } from '@grafana/scenes';
import { ElementSelectionContextItem } from '@grafana/ui';
import { isBulkActionElement } from '../scene/types/BulkActionElement';
import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types/EditableDashboardElement';
import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement';
import { EditableDashboardElement } from '../scene/types/EditableDashboardElement';
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>>;
private selectedObjects: Map<string, SceneObjectRef<SceneObject>>;
private sameType?: boolean;
private _isMultiSelection: boolean;
@@ -26,15 +23,15 @@ export class ElementSelection {
}
private checkSameType() {
const values = this.selectedObjects?.values();
const firstType = values?.next().value?.resolve()?.constructor.name;
const values = this.selectedObjects.values();
const firstType = values.next().value?.resolve().constructor.name;
if (!firstType) {
return false;
}
for (let obj of values ?? []) {
if (obj.resolve()?.constructor.name !== firstType) {
if (obj.resolve().constructor.name !== firstType) {
return false;
}
}
@@ -43,13 +40,13 @@ export class ElementSelection {
}
public hasValue(id: string) {
return this.selectedObjects?.has(id);
return this.selectedObjects.has(id);
}
public removeValue(id: string) {
this.selectedObjects?.delete(id);
this.selectedObjects.delete(id);
if (this.selectedObjects && this.selectedObjects.size < 2) {
if (this.selectedObjects.size < 2) {
this.sameType = undefined;
this._isMultiSelection = false;
}
@@ -95,11 +92,11 @@ export class ElementSelection {
}
public getSelectionEntries(): Array<[string, SceneObjectRef<SceneObject>]> {
return Array.from(this.selectedObjects?.entries() ?? []);
return Array.from(this.selectedObjects.entries());
}
public getFirstObject(): SceneObject | undefined {
return this.selectedObjects?.values().next().value?.resolve();
return this.selectedObjects.values().next().value?.resolve();
}
public get isMultiSelection(): boolean {
@@ -107,51 +104,38 @@ export class ElementSelection {
}
private getSceneObjects(): SceneObject[] {
return Array.from(this.selectedObjects?.values() ?? []).map((obj) => obj.resolve());
return Array.from(this.selectedObjects.values() ?? []).map((obj) => obj.resolve());
}
public createSelectionElement() {
if (this.isMultiSelection) {
return this.createMultiSelectedElement();
}
return this.createSingleSelectedElement();
}
private createSingleSelectedElement(): EditableDashboardElement | undefined {
const sceneObj = this.selectedObjects?.values().next().value?.resolve();
return getEditableElementFor(sceneObj);
}
private createMultiSelectedElement(): MultiSelectedEditableDashboardElement | undefined {
if (!this.isMultiSelection) {
return;
}
public createSelectionElement(): EditableDashboardElement | undefined {
const sceneObjects = this.getSceneObjects();
if (this.sameType) {
const firstObj = this.selectedObjects?.values().next().value?.resolve();
if (sceneObjects.length === 0) {
return undefined;
}
if (firstObj instanceof VizPanel) {
return new MultiSelectedVizPanelsEditableElement(sceneObjects.filter((obj) => obj instanceof VizPanel));
}
const firstElement = getEditableElementFor(sceneObjects[0]);
if (isEditableDashboardElement(firstObj!)) {
return firstObj.createMultiSelectedElement?.(sceneObjects);
}
if (!firstElement) {
return undefined;
}
if (sceneObjects.length === 1) {
return firstElement;
}
if (this.sameType && firstElement.createMultiSelectedElement) {
const elements = sceneObjects.map((obj) => getEditableElementFor(obj)!);
return firstElement.createMultiSelectedElement(elements);
}
const bulkActionElements = [];
for (const sceneObject of sceneObjects) {
if (sceneObject instanceof VizPanel) {
const editableElement = new VizPanelEditableElement(sceneObject);
bulkActionElements.push(editableElement);
}
const element = getEditableElementFor(sceneObject);
if (isBulkActionElement(sceneObject)) {
bulkActionElements.push(sceneObject);
if (element && isBulkActionElement(element)) {
bulkActionElements.push(element);
}
}
@@ -1,42 +1,20 @@
import { ReactNode } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { Stack, Text, Button } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { t } from 'app/core/internationalization';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { BulkActionElement } from '../scene/types/BulkActionElement';
import { EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement';
import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement';
import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement';
export class MultiSelectedObjectsEditableElement implements MultiSelectedEditableDashboardElement {
public readonly isMultiSelectedEditableDashboardElement = true;
public readonly key: string;
export class MultiSelectedObjectsEditableElement implements EditableDashboardElement {
public readonly isEditableDashboardElement = true;
constructor(private _elements: BulkActionElement[]) {
this.key = uuidv4();
constructor(private _elements: BulkActionElement[]) {}
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
return [];
}
public getEditableElementInfo(): EditableDashboardElementInfo {
return { name: t('dashboard.edit-pane.elements.objects', 'Objects'), typeId: 'objects', icon: 'folder' };
}
public renderActions(): ReactNode {
return (
<Stack direction="column">
<Text>
<Trans
i18nKey="dashboard.edit-pane.objects.multi-select.selection-number"
values={{ length: this._elements.length }}
>
No. of objects selected: {{ length }}
</Trans>
</Text>
<Stack direction="row">
<Button size="sm" variant="secondary" icon="copy" />
<Button size="sm" variant="destructive" fill="outline" onClick={() => this.onDelete()} icon="trash-alt" />
</Stack>
</Stack>
);
return { typeName: t('dashboard.edit-pane.elements.objects', 'Objects'), icon: 'folder', instanceName: '' };
}
public onDelete() {
@@ -1,40 +1,28 @@
import { v4 as uuidv4 } from 'uuid';
import { VizPanel } from '@grafana/scenes';
import { t } from 'app/core/internationalization';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement';
import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement';
import { EditPaneHeader } from './EditPaneHeader';
import { VizPanelEditableElement } from './VizPanelEditableElement';
export class MultiSelectedVizPanelsEditableElement implements MultiSelectedEditableDashboardElement {
public readonly isMultiSelectedEditableDashboardElement = true;
export class MultiSelectedVizPanelsEditableElement implements EditableDashboardElement {
public readonly isEditableDashboardElement = true;
public readonly key: string;
constructor(private _panels: VizPanel[]) {
constructor(private _panels: VizPanelEditableElement[]) {
this.key = uuidv4();
}
public getEditableElementInfo(): EditableDashboardElementInfo {
return { name: t('dashboard.edit-pane.elements.panels', 'Panels'), typeId: 'panels', icon: 'folder' };
return { typeName: t('dashboard.edit-pane.elements.panels', 'Panels'), icon: 'folder', instanceName: '' };
}
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
const header = new OptionsPaneCategoryDescriptor({
title: ``,
id: 'panel-header',
isOpenable: false,
renderTitle: () => (
<EditPaneHeader
title={t('dashboard.layout.common.panels-title', '{{length}} panels selected', {
length: this._panels.length,
})}
onDelete={() => this.onDelete()}
/>
),
id: '',
});
return [header];
@@ -42,8 +30,7 @@ export class MultiSelectedVizPanelsEditableElement implements MultiSelectedEdita
public onDelete() {
this._panels.forEach((panel) => {
const layout = dashboardSceneGraph.getLayoutManagerFor(panel);
layout.removePanel?.(panel);
panel.onDelete();
});
}
}
@@ -18,9 +18,9 @@ import { isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem';
import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getEditPanelUrl } from '../utils/urlBuilders';
import { getPanelIdForVizPanel } from '../utils/utils';
import { getDashboardSceneFor, getPanelIdForVizPanel } from '../utils/utils';
import { EditPaneHeader } from './EditPaneHeader';
import { MultiSelectedVizPanelsEditableElement } from './MultiSelectedVizPanelsEditableElement';
export class VizPanelEditableElement implements EditableDashboardElement, BulkActionElement {
public readonly isEditableDashboardElement = true;
@@ -30,9 +30,9 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
public getEditableElementInfo(): EditableDashboardElementInfo {
return {
typeId: 'panel',
typeName: t('dashboard.edit-pane.elements.panel', 'Panel'),
icon: 'chart-line',
name: sceneGraph.interpolate(this.panel, this.panel.state.title, undefined, 'text'),
instanceName: sceneGraph.interpolate(this.panel, this.panel.state.title, undefined, 'text'),
};
}
@@ -41,14 +41,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
const layoutElement = panel.parent!;
const panelOptions = useMemo(() => {
return new OptionsPaneCategoryDescriptor({
title: ``,
id: 'panel-header',
isOpenable: false,
renderTitle: () => (
<EditPaneHeader title={t('dashboard.viz-panel.options.title', 'Panel')} onDelete={() => this.onDelete()} />
),
})
return new OptionsPaneCategoryDescriptor({ title: '', id: 'panel-options' })
.addItem(
new OptionsPaneItemDescriptor({
title: '',
@@ -97,6 +90,20 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
const layout = dashboardSceneGraph.getLayoutManagerFor(this.panel);
layout.removePanel?.(this.panel);
}
public onDuplicate() {
const layout = dashboardSceneGraph.getLayoutManagerFor(this.panel);
layout.duplicatePanel?.(this.panel);
}
public onCopy() {
const dashboard = getDashboardSceneFor(this.panel);
dashboard.copyPanel(this.panel);
}
public createMultiSelectedElement(items: VizPanelEditableElement[]) {
return new MultiSelectedVizPanelsEditableElement(items);
}
}
type OpenPanelEditVizProps = {
@@ -112,7 +119,7 @@ const OpenPanelEditViz = ({ panel }: OpenPanelEditVizProps) => {
return (
<Stack alignItems="center" width="100%">
{plugin ? (
<Tooltip content={t('dashboard.viz-panel.options.open-edit', 'Open Panel Edit')}>
<Tooltip content={t('dashboard.viz-panel.options.open-edit', 'Open panel editor')}>
<a
href={textUtil.sanitizeUrl(getEditPanelUrl(getPanelIdForVizPanel(panel)))}
className={cx(styles.pluginDescriptionWrapper)}
@@ -139,12 +146,15 @@ const getStyles = (theme: GrafanaTheme2) => ({
columnGap: theme.spacing(1),
rowGap: theme.spacing(0.5),
minHeight: theme.spacing(4),
backgroundColor: theme.components.input.background,
border: `1px solid ${theme.colors.border.strong}`,
backgroundColor: theme.colors.secondary.main,
border: `1px solid ${theme.colors.secondary.border}`,
borderRadius: theme.shape.radius.default,
paddingInline: theme.spacing(1),
paddingBlock: theme.spacing(0.5),
flexGrow: 1,
'&:hover': {
backgroundColor: theme.colors.secondary.shade,
},
}),
panelVizImg: css({
width: '16px',
@@ -1,7 +1,6 @@
import { useMemo } from 'react';
import { EditableDashboardElement } from '../scene/types/EditableDashboardElement';
import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement';
import { getDashboardSceneFor } from '../utils/utils';
import { DashboardEditPane } from './DashboardEditPane';
@@ -10,7 +9,7 @@ import { ElementSelection } from './ElementSelection';
export function useEditableElement(
selection: ElementSelection | undefined,
editPane: DashboardEditPane
): EditableDashboardElement | MultiSelectedEditableDashboardElement | undefined {
): EditableDashboardElement | undefined {
return useMemo(() => {
if (!selection) {
const dashboard = getDashboardSceneFor(editPane);
@@ -1,8 +1,8 @@
import { ReactNode, useMemo } from 'react';
import { 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 { Alert, 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';
@@ -25,9 +25,9 @@ export class SceneGridRowEditableElement implements EditableDashboardElement, Bu
public getEditableElementInfo(): EditableDashboardElementInfo {
return {
typeId: 'panel',
typeName: t('dashboard.edit-pane.elements.row', 'Row'),
instanceName: sceneGraph.interpolate(this._row, this._row.state.title, undefined, 'text'),
icon: 'line-alt',
name: sceneGraph.interpolate(this._row, this._row.state.title, undefined, 'text'),
};
}
@@ -72,14 +72,6 @@ export class SceneGridRowEditableElement implements EditableDashboardElement, Bu
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 }) {
@@ -44,7 +44,11 @@ export class RowItem
}
public getEditableElementInfo(): EditableDashboardElementInfo {
return { typeId: 'row', icon: 'line-alt', name: sceneGraph.interpolate(this, this.state.title, undefined, 'text') };
return {
typeName: t('dashboard.edit-pane.elements.row', 'Row'),
instanceName: sceneGraph.interpolate(this, this.state.title, undefined, 'text'),
icon: 'line-alt',
};
}
public getLayout(): DashboardLayoutManager {
@@ -10,7 +10,6 @@ import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSel
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants';
import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource';
import { EditPaneHeader } from '../../edit-pane/EditPaneHeader';
import { getDashboardSceneFor, getQueryRunnerFor } from '../../utils/utils';
import { DashboardScene } from '../DashboardScene';
import { DashboardLayoutSelector } from '../layouts-shared/DashboardLayoutSelector';
@@ -23,14 +22,7 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[]
const rowOptions = useMemo(() => {
const dashboard = getDashboardSceneFor(model);
const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({
title: t('dashboard.rows-layout.item-name', 'Row'),
id: 'row-options',
isOpenable: false,
renderTitle: () => (
<EditPaneHeader title={t('dashboard.rows-layout.item-name', 'Row')} onDelete={() => model.onDelete()} />
),
})
const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({ title: '', id: 'row-options' })
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.rows-layout.option.title', 'Title'),
@@ -1,24 +1,18 @@
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 { EditableDashboardElementInfo, EditableDashboardElement } from '../types/EditableDashboardElement';
import { RowItem } from './RowItem';
import { getEditOptions } from './RowItemsEditor';
export class RowItems implements MultiSelectedEditableDashboardElement {
public readonly isMultiSelectedEditableDashboardElement = true;
public readonly key: string;
export class RowItems implements EditableDashboardElement {
public readonly isEditableDashboardElement = true;
public constructor(private _rows: RowItem[]) {
this.key = uuidv4();
}
public constructor(private _rows: RowItem[]) {}
public getEditableElementInfo(): EditableDashboardElementInfo {
return { name: t('dashboard.edit-pane.elements.rows', 'Rows'), typeId: 'rows', icon: 'folder' };
return { typeName: t('dashboard.edit-pane.elements.rows', 'Rows'), icon: 'folder', instanceName: '' };
}
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
@@ -36,6 +30,4 @@ export class RowItems implements MultiSelectedEditableDashboardElement {
public onHeaderHiddenToggle(value: boolean, indeterminate: boolean) {
this._rows.forEach((row) => row.onHeaderHiddenToggle(indeterminate ? true : !value));
}
public getNumberOfRowsSelected = () => this._rows.length;
}
@@ -3,24 +3,10 @@ import { t } from 'app/core/internationalization';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
import { EditPaneHeader } from '../../edit-pane/EditPaneHeader';
import { RowItems } from './RowItems';
export function getEditOptions(model: RowItems): OptionsPaneCategoryDescriptor[] {
const options = new OptionsPaneCategoryDescriptor({
title: '',
id: `ms-row-options-${model.key}`,
isOpenable: false,
renderTitle: () => (
<EditPaneHeader
title={t('dashboard.edit-pane.row.multi-select.title', '{{length}} rows selected', {
length: model.getNumberOfRowsSelected(),
})}
onDelete={() => model.onDelete()}
/>
),
}).addItem(
const options = new OptionsPaneCategoryDescriptor({ title: '', id: `rows-options` }).addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.edit-pane.row.header.title', 'Row header'),
render: () => <RowHeaderCheckboxMulti model={model} />,
@@ -40,7 +40,11 @@ export class TabItem
}
public getEditableElementInfo(): EditableDashboardElementInfo {
return { typeId: 'tab', icon: 'tag-alt', name: sceneGraph.interpolate(this, this.state.title, undefined, 'text') };
return {
typeName: t('dashboard.edit-pane.elements.tab', 'Tab'),
instanceName: sceneGraph.interpolate(this, this.state.title, undefined, 'text'),
icon: 'tag-alt',
};
}
public getLayout(): DashboardLayoutManager {
@@ -5,7 +5,6 @@ import { t } from 'app/core/internationalization';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
import { EditPaneHeader } from '../../edit-pane/EditPaneHeader';
import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector';
import { useEditPaneInputAutoFocus } from '../layouts-shared/utils';
@@ -13,14 +12,7 @@ import { TabItem } from './TabItem';
export function getEditOptions(model: TabItem): OptionsPaneCategoryDescriptor[] {
const tabOptions = useMemo(() => {
return new OptionsPaneCategoryDescriptor({
title: '',
id: 'tab-options',
isOpenable: false,
renderTitle: () => (
<EditPaneHeader title={t('dashboard.tabs-layout.tab-options.title', 'Tab')} onDelete={() => model.onDelete()} />
),
}).addItem(
return new OptionsPaneCategoryDescriptor({ title: '', id: 'tab-item-options' }).addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.tabs-layout.tab-options.title-option', 'Title'),
render: () => <TabTitleInput tab={model} />,
@@ -1,28 +1,21 @@
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 { EditableDashboardElement, EditableDashboardElementInfo } from '../types/EditableDashboardElement';
import { TabItem } from './TabItem';
import { getEditOptions } from './TabItemsEditor';
export class TabItems implements MultiSelectedEditableDashboardElement {
public readonly isMultiSelectedEditableDashboardElement = true;
public readonly key: string;
export class TabItems implements EditableDashboardElement {
public readonly isEditableDashboardElement = true;
public constructor(private _tabs: TabItem[]) {
this.key = uuidv4();
}
public constructor(private _tabs: TabItem[]) {}
public getEditableElementInfo(): EditableDashboardElementInfo {
return { name: t('dashboard.edit-pane.elements.tabs', 'Tabs'), typeId: 'tabs', icon: 'folder' };
return { typeName: t('dashboard.edit-pane.elements.tabs', 'Tabs'), icon: 'folder', instanceName: '' };
}
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
return getEditOptions(this);
return [];
}
public getTabs(): TabItem[] {
@@ -1,27 +0,0 @@
import { useMemo } from 'react';
import { t } from 'app/core/internationalization';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { EditPaneHeader } from '../../edit-pane/EditPaneHeader';
import { TabItems } from './TabItems';
export function getEditOptions(model: TabItems): OptionsPaneCategoryDescriptor[] {
const tabOptions = useMemo(() => {
const tabs = model.getTabs();
return new OptionsPaneCategoryDescriptor({
title: ``,
id: 'ms-tab-options',
isOpenable: false,
renderTitle: () => (
<EditPaneHeader
title={t('dashboard.tabs-layout.multi-select.title', '{{length}} tabs selected', { length: tabs.length })}
onDelete={() => model.onDelete()}
/>
),
});
}, [model]);
return [tabOptions];
}
@@ -1,11 +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';
import { MultiSelectedEditableDashboardElement } from './MultiSelectedEditableDashboardElement';
/**
* Interface for elements that have options
*/
@@ -24,29 +19,29 @@ export interface EditableDashboardElement {
useEditPaneOptions(): OptionsPaneCategoryDescriptor[];
/**
* Panel Actions
**/
renderActions?(): ReactNode;
* Supports delete action
*/
onDelete?(): void;
/**
* Supports duplicate action
*/
onDuplicate?(): void;
/**
* Supports copy action
*/
onCopy?(): void;
/**
* creates a new multi-selection element from a list of selected items
*/
createMultiSelectedElement?(items: SceneObject[]): MultiSelectedEditableDashboardElement;
/**
* Return custom title for the edit panel header
*/
renderTitle?(): ReactNode;
/**
* determines if first edit panel header can be collapsed
*/
isOpenable?: Readonly<boolean>;
createMultiSelectedElement?(elements: this[]): EditableDashboardElement;
}
export interface EditableDashboardElementInfo {
name: string;
typeId: string;
instanceName: string;
typeName: string;
icon: IconName;
}
@@ -1,44 +0,0 @@
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;
/** A descriptor used by editing pane */
getEditableElementInfo(): EditableDashboardElementInfo;
/**
* Extremely useful for being able to access the useState inside the contained items
*/
key: Readonly<string>;
/**
* Hook that returns edit pane options
*/
useEditPaneOptions?(): OptionsPaneCategoryDescriptor[];
/**
* Panel Actions
**/
renderActions?(): ReactNode;
/**
* Return custom title for the edit panel header
*/
renderTitle?(): ReactNode;
/**
* determines if first edit panel header can be collapsed
*/
isOpenable?: Readonly<boolean>;
}
export function isMultiSelectedEditableDashboardElement(obj: object): obj is MultiSelectedEditableDashboardElement {
return 'isMultiSelectedEditableDashboardElement' in obj;
}
@@ -21,7 +21,6 @@ export interface OptionsPaneCategoryProps {
isNested?: boolean;
children: ReactNode;
sandboxId?: string;
isOpenable?: boolean;
}
const CATEGORY_PARAM_NAME = 'showCategory' as const;
@@ -38,13 +37,12 @@ export const OptionsPaneCategory = React.memo(
itemsCount,
isNested = false,
sandboxId,
isOpenable = true,
}: OptionsPaneCategoryProps) => {
const [savedState, setSavedState] = useLocalStorage(getOptionGroupStorageKey(id), {
isExpanded: isOpenDefault,
});
const [isExpanded, setIsExpanded] = useState(!isOpenable || (savedState?.isExpanded ?? isOpenDefault));
const [isExpanded, setIsExpanded] = useState(savedState?.isExpanded ?? isOpenDefault);
const manualClickTime = useRef(0);
const ref = useRef<HTMLDivElement>(null);
const [queryParams, updateQueryParams] = useQueryParams();
@@ -68,9 +66,6 @@ export const OptionsPaneCategory = React.memo(
}, [forceOpen, isExpanded, isOpenFromUrl]);
const onToggle = useCallback(() => {
if (!isOpenable) {
return;
}
manualClickTime.current = Date.now();
updateQueryParams(
{
@@ -80,7 +75,7 @@ export const OptionsPaneCategory = React.memo(
);
setSavedState({ isExpanded: !isExpanded });
setIsExpanded(!isExpanded);
}, [isOpenable, updateQueryParams, isExpanded, id, setSavedState]);
}, [updateQueryParams, isExpanded, id, setSavedState]);
if (!renderTitle) {
renderTitle = function defaultTitle(isExpanded: boolean) {
@@ -106,7 +101,6 @@ export const OptionsPaneCategory = React.memo(
);
const headerStyles = cx(styles.header, {
[styles.headerHover]: isOpenable,
[styles.headerExpanded]: isExpanded,
[styles.headerNested]: isNested,
});
@@ -129,19 +123,18 @@ export const OptionsPaneCategory = React.memo(
<h6 id={`button-${id}`} className={styles.title}>
{renderTitle(isExpanded)}
</h6>
{isOpenable ? (
<Button
data-testid={selectors.components.OptionsGroup.toggle(id)}
type="button"
fill="text"
size="md"
variant="secondary"
aria-expanded={isExpanded}
className={styles.toggleButton}
icon={isExpanded ? 'angle-up' : 'angle-down'}
onClick={onToggle}
/>
) : null}
<Button
data-testid={selectors.components.OptionsGroup.toggle(id)}
type="button"
fill="text"
size="md"
variant="secondary"
aria-expanded={isExpanded}
className={styles.toggleButton}
icon={isExpanded ? 'angle-up' : 'angle-down'}
onClick={onToggle}
/>
</div>
{isExpanded && (
<div className={bodyStyles} id={id} aria-labelledby={`button-${id}`}>
@@ -179,8 +172,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
padding: theme.spacing(0.5, 1.5),
color: theme.colors.text.primary,
fontWeight: theme.typography.fontWeightMedium,
}),
headerHover: css({
cursor: 'pointer',
'&:hover': {
background: theme.colors.emphasize(theme.colors.background.primary, 0.03),
@@ -16,7 +16,6 @@ export interface OptionsPaneCategoryDescriptorProps {
itemsCount?: number;
customRender?: () => React.ReactNode;
sandboxId?: string;
isOpenable?: boolean;
}
/**
@@ -60,8 +59,12 @@ export class OptionsPaneCategoryDescriptor {
return this.props.customRender();
}
if (this.props.id === '') {
return <Box padding={2}>{this.items.map((item) => item.render(searchQuery))}</Box>;
if (this.props.title === '') {
return (
<Box padding={2} key={this.props.title}>
{this.items.map((item) => item.render(searchQuery))}
</Box>
);
}
return (
+5 -18
View File
@@ -1068,23 +1068,18 @@
"elements": {
"dashboard": "Dashboard",
"objects": "Objects",
"panel": "Panel",
"panels": "Panels",
"row": "Row",
"rows": "Rows",
"tab": "Tab",
"tabs": "Tabs"
},
"objects": {
"multi-select": {
"selection-number": "No. of objects selected: {{length}}"
}
},
"open": "Open options pane",
"row": {
"header": {
"hide": "Hide",
"title": "Row header"
},
"multi-select": {
"title": "{{length}} rows selected"
}
}
},
@@ -1165,13 +1160,11 @@
"copy-or-duplicate": "Copy or Duplicate",
"delete": "Delete",
"duplicate": "Duplicate",
"layout": "Layout",
"panels-title": "{{length}} panels selected"
"layout": "Layout"
}
},
"options": {
"description": "Description",
"title": "Dashboard options",
"title-option": "Title"
},
"outline": {
@@ -1214,7 +1207,6 @@
},
"rows-layout": {
"description": "Rows layout",
"item-name": "Row",
"name": "Rows",
"option": {
"height": "Height",
@@ -1253,9 +1245,6 @@
"menu": {
"move-tab": "Move tab"
},
"multi-select": {
"title": "{{length}} tabs selected"
},
"name": "Tabs",
"tab": {
"menu": {
@@ -1270,7 +1259,6 @@
"new": "New tab"
},
"tab-options": {
"title": "Tab",
"title-option": "Title"
}
},
@@ -1345,9 +1333,8 @@
"viz-panel": {
"options": {
"description": "Description",
"open-edit": "Open Panel Edit",
"open-edit": "Open panel editor",
"plugin-type-image": "Image of plugin type",
"title": "Panel",
"title-option": "Title",
"transparent-background": "Transparent background"
}