Dashboard: Variables in outline (#103712)
* Update * Progress * update * fixes * Fixed crash
This commit is contained in:
@@ -268,6 +268,7 @@ export const availableIconsIndex = {
|
||||
x: true,
|
||||
'add-user': true,
|
||||
attach: true,
|
||||
'dollar-alt': true,
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof availableIconsIndex;
|
||||
|
||||
@@ -176,6 +176,7 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
|
||||
|
||||
private newObjectAddedToCanvas(obj: SceneObject) {
|
||||
this.selectObject(obj, obj.state.key!);
|
||||
this.state.selection!.markAsNewElement();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +195,7 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
|
||||
const styles = useStyles2(getStyles);
|
||||
const editableElement = useEditableElement(selection, editPane);
|
||||
const selectedObject = selection?.getFirstObject();
|
||||
const isNewElement = selection?.isNewElement() ?? false;
|
||||
const [outlineCollapsed, setOutlineCollapsed] = useLocalStorage(
|
||||
'grafana.dashboard.edit-pane.outline.collapsed',
|
||||
true
|
||||
@@ -233,7 +235,12 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
|
||||
|
||||
{openOverlay && (
|
||||
<Resizable className={styles.overlayWrapper} defaultSize={{ height: '100%', width: '300px' }}>
|
||||
<ElementEditPane element={editableElement} key={selectedObject?.state.key} editPane={editPane} />
|
||||
<ElementEditPane
|
||||
element={editableElement}
|
||||
key={selectedObject?.state.key}
|
||||
editPane={editPane}
|
||||
isNewElement={isNewElement}
|
||||
/>
|
||||
</Resizable>
|
||||
)}
|
||||
</>
|
||||
@@ -254,7 +261,12 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
|
||||
<div className={styles.wrapper}>
|
||||
<div {...splitter.containerProps}>
|
||||
<div {...splitter.primaryProps} className={cx(splitter.primaryProps.className, styles.paneContent)}>
|
||||
<ElementEditPane element={editableElement} key={selectedObject?.state.key} editPane={editPane} />
|
||||
<ElementEditPane
|
||||
element={editableElement}
|
||||
key={selectedObject?.state.key}
|
||||
editPane={editPane}
|
||||
isNewElement={isNewElement}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
{...splitter.splitterProps}
|
||||
|
||||
@@ -20,6 +20,7 @@ export class DashboardEditableElement implements EditableDashboardElement {
|
||||
typeName: t('dashboard.edit-pane.elements.dashboard', 'Dashboard'),
|
||||
icon: 'apps',
|
||||
instanceName: t('dashboard.edit-pane.elements.dashboard', 'Dashboard'),
|
||||
isContainer: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { sortBy } from 'lodash';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SceneObject, VizPanel } from '@grafana/scenes';
|
||||
import { SceneObject } from '@grafana/scenes';
|
||||
import { Box, Icon, Text, useElementSelection, useStyles2 } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
|
||||
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
|
||||
import { EditableDashboardElement } from '../scene/types/EditableDashboardElement';
|
||||
import { isInCloneChain } from '../utils/clone';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
import { DashboardEditPane } from './DashboardEditPane';
|
||||
import { getEditableElementFor, hasEditableElement } from './shared';
|
||||
import { getEditableElementFor } from './shared';
|
||||
|
||||
export interface Props {
|
||||
editPane: DashboardEditPane;
|
||||
@@ -22,50 +24,54 @@ export function DashboardOutline({ editPane }: Props) {
|
||||
|
||||
return (
|
||||
<Box padding={1} gap={0.25} display="flex" direction="column">
|
||||
<DashboardOutlineNode sceneObject={dashboard} editPane={editPane} expandable />
|
||||
<DashboardOutlineNode sceneObject={dashboard} editPane={editPane} depth={0} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardOutlineNode({
|
||||
sceneObject,
|
||||
expandable,
|
||||
editPane,
|
||||
depth,
|
||||
}: {
|
||||
sceneObject: SceneObject;
|
||||
expandable: boolean;
|
||||
editPane: DashboardEditPane;
|
||||
depth: number;
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const [isCollapsed, setIsCollapsed] = useState(depth > 0);
|
||||
const { key } = sceneObject.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
const { isSelected, onSelect } = useElementSelection(key);
|
||||
const isCloned = useMemo(() => isInCloneChain(key!), [key]);
|
||||
const editableElement = useMemo(() => getEditableElementFor(sceneObject)!, [sceneObject]);
|
||||
|
||||
const children = collectEditableElementChildren(sceneObject);
|
||||
const children = sortBy(collectEditableElementChildren(sceneObject, [], 0), 'depth');
|
||||
const elementInfo = editableElement.getEditableElementInfo();
|
||||
const instanceName = elementInfo.instanceName === '' ? '<empty title>' : elementInfo.instanceName;
|
||||
const elementExpanded = !editableElement.getCollapsedState?.();
|
||||
const noTitleText = t('dashboard.outline.tree-item.no-title', '<no title>');
|
||||
const instanceName = elementInfo.instanceName === '' ? noTitleText : elementInfo.instanceName;
|
||||
const elementCollapsed = editableElement.getCollapsedState?.();
|
||||
|
||||
const onPointerDown = (evt: React.PointerEvent) => {
|
||||
onSelect?.(evt);
|
||||
setIsExpanded(!isExpanded);
|
||||
// Only select via clicking outline never deselect
|
||||
if (!isSelected) {
|
||||
onSelect?.(evt);
|
||||
}
|
||||
|
||||
setIsCollapsed(!isCollapsed);
|
||||
editableElement.scrollIntoView?.();
|
||||
|
||||
// Sync expanded state with canvas element
|
||||
if (editableElement.getCollapsedState) {
|
||||
editableElement.setCollapsedState?.(isExpanded);
|
||||
editableElement.setCollapsedState?.(!isCollapsed);
|
||||
}
|
||||
};
|
||||
|
||||
// Sync canvas element expanded state with outline element
|
||||
useEffect(() => {
|
||||
if (elementExpanded !== isExpanded) {
|
||||
console.log('elementExpanded', elementExpanded);
|
||||
setIsExpanded(elementExpanded);
|
||||
if (elementCollapsed != null && elementCollapsed !== isCollapsed) {
|
||||
setIsCollapsed(elementCollapsed);
|
||||
}
|
||||
}, [isExpanded, elementExpanded]);
|
||||
}, [isCollapsed, elementCollapsed]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -74,25 +80,28 @@ function DashboardOutlineNode({
|
||||
className={cx(styles.nodeButton, isCloned && styles.nodeButtonClone, isSelected && styles.nodeButtonSelected)}
|
||||
onPointerDown={onPointerDown}
|
||||
>
|
||||
{expandable && <Icon name={isExpanded ? 'angle-down' : 'angle-right'} />}
|
||||
{elementInfo.isContainer && <Icon name={!isCollapsed ? 'angle-down' : 'angle-right'} />}
|
||||
<Icon size="sm" name={elementInfo.icon} />
|
||||
<span>{instanceName}</span>
|
||||
{elementInfo.isHidden && <Icon name="eye-slash" size="sm" className={styles.hiddenIcon} />}
|
||||
{/* eslint-disable-next-line @grafana/no-untranslated-strings */}
|
||||
{elementInfo.isContainer && isCollapsed && <span>({children.length})</span>}
|
||||
</button>
|
||||
|
||||
{expandable && isExpanded && (
|
||||
{elementInfo.isContainer && !isCollapsed && (
|
||||
<div className={styles.container} role="group">
|
||||
{children.length > 0 ? (
|
||||
children.map((child) => (
|
||||
<DashboardOutlineNode
|
||||
key={child.sceneObject.state.key}
|
||||
sceneObject={child.sceneObject}
|
||||
expandable={child.expandable}
|
||||
editPane={editPane}
|
||||
depth={depth + 1}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Text element="p" color="secondary">
|
||||
<Trans i18nKey="dashboard.outline.tree.item.empty">(empty)</Trans>
|
||||
<Text color="secondary">
|
||||
<Trans i18nKey="dashboard.outline.tree-item.empty">(empty)</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
@@ -136,12 +145,16 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
}),
|
||||
nodeButtonSelected: css({
|
||||
color: theme.colors.text.primary,
|
||||
outline: `1px dashed ${theme.colors.primary.border}`,
|
||||
outline: `1px dashed ${theme.colors.primary.border} !important`,
|
||||
outlineOffset: '0px',
|
||||
'&:hover': {
|
||||
outline: `1px dashed ${theme.colors.primary.border}`,
|
||||
},
|
||||
}),
|
||||
hiddenIcon: css({
|
||||
color: theme.colors.text.secondary,
|
||||
marginLeft: theme.spacing(1),
|
||||
}),
|
||||
nodeButtonClone: css({
|
||||
color: theme.colors.text.secondary,
|
||||
cursor: 'not-allowed',
|
||||
@@ -151,29 +164,37 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
|
||||
interface EditableElementConfig {
|
||||
sceneObject: SceneObject;
|
||||
expandable: boolean;
|
||||
editableElement: EditableDashboardElement;
|
||||
depth: number;
|
||||
}
|
||||
|
||||
function collectEditableElementChildren(
|
||||
sceneObject: SceneObject,
|
||||
children: EditableElementConfig[] = []
|
||||
children: EditableElementConfig[],
|
||||
depth: number
|
||||
): EditableElementConfig[] {
|
||||
sceneObject.forEachChild((child) => {
|
||||
const editableElement = getEditableElementFor(child);
|
||||
|
||||
if (editableElement) {
|
||||
children.push({ sceneObject: child, editableElement, depth });
|
||||
return;
|
||||
}
|
||||
|
||||
if (child instanceof DashboardGridItem) {
|
||||
// DashboardGridItem is a special case as it can contain repeated panels
|
||||
// In this case, we want to show the repeated panels as separate items, otherwise show the body panel
|
||||
if (child.state.repeatedPanels?.length) {
|
||||
children.push(...child.state.repeatedPanels.map((panel) => ({ sceneObject: panel, expandable: false })));
|
||||
} else {
|
||||
children.push({ sceneObject: child.state.body, expandable: false });
|
||||
for (const repeatedPanel of child.state.repeatedPanels) {
|
||||
const editableElement = getEditableElementFor(repeatedPanel)!;
|
||||
children.push({ sceneObject: repeatedPanel, editableElement, depth });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
} else if (child instanceof VizPanel) {
|
||||
children.push({ sceneObject: child, expandable: false });
|
||||
} else if (hasEditableElement(child)) {
|
||||
children.push({ sceneObject: child, expandable: true });
|
||||
} else {
|
||||
collectEditableElementChildren(child, children);
|
||||
}
|
||||
|
||||
collectEditableElementChildren(child, children, depth + 1);
|
||||
});
|
||||
|
||||
return children;
|
||||
|
||||
@@ -11,10 +11,11 @@ import { EditPaneHeader } from './EditPaneHeader';
|
||||
export interface Props {
|
||||
element: EditableDashboardElement;
|
||||
editPane: DashboardEditPane;
|
||||
isNewElement: boolean;
|
||||
}
|
||||
|
||||
export function ElementEditPane({ element, editPane }: Props) {
|
||||
const categories = element.useEditPaneOptions ? element.useEditPaneOptions() : [];
|
||||
export function ElementEditPane({ element, editPane, isNewElement }: Props) {
|
||||
const categories = element.useEditPaneOptions ? element.useEditPaneOptions(isNewElement) : [];
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,8 +10,8 @@ import { getEditableElementFor } from './shared';
|
||||
export class ElementSelection {
|
||||
private selectedObjects: Map<string, SceneObjectRef<SceneObject>>;
|
||||
private sameType?: boolean;
|
||||
|
||||
private _isMultiSelection: boolean;
|
||||
private _isNewElement = false;
|
||||
|
||||
constructor(values: Array<[string, SceneObjectRef<SceneObject>]>) {
|
||||
this.selectedObjects = new Map(values);
|
||||
@@ -22,6 +22,14 @@ export class ElementSelection {
|
||||
}
|
||||
}
|
||||
|
||||
public markAsNewElement() {
|
||||
this._isNewElement = true;
|
||||
}
|
||||
|
||||
public isNewElement() {
|
||||
return this._isNewElement;
|
||||
}
|
||||
|
||||
private checkSameType() {
|
||||
const values = this.selectedObjects.values();
|
||||
const firstType = values.next().value?.resolve().constructor.name;
|
||||
|
||||
@@ -38,7 +38,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
|
||||
};
|
||||
}
|
||||
|
||||
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
|
||||
public useEditPaneOptions(isNewElement: boolean): OptionsPaneCategoryDescriptor[] {
|
||||
const panel = this.panel;
|
||||
const layoutElement = panel.parent!;
|
||||
|
||||
@@ -55,7 +55,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
|
||||
title: t('dashboard.viz-panel.options.title-option', 'Title'),
|
||||
value: panel.state.title,
|
||||
popularRank: 1,
|
||||
render: () => <PanelFrameTitleInput panel={panel} />,
|
||||
render: () => <PanelFrameTitleInput panel={panel} isNewElement={isNewElement} />,
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
@@ -71,7 +71,7 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
|
||||
render: () => <PanelBackgroundSwitch panel={panel} />,
|
||||
})
|
||||
);
|
||||
}, [panel]);
|
||||
}, [panel, isNewElement]);
|
||||
|
||||
const layoutCategories = useMemo(
|
||||
() => (isDashboardLayoutItem(layoutElement) && layoutElement.getOptions ? layoutElement.getOptions() : []),
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { useSessionStorage } from 'react-use';
|
||||
|
||||
import { BusEventWithPayload } from '@grafana/data';
|
||||
import { SceneGridRow, SceneObject, SceneVariable, VizPanel } from '@grafana/scenes';
|
||||
import { SceneGridRow, SceneObject, SceneVariable, SceneVariableSet, VizPanel } from '@grafana/scenes';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { SceneGridRowEditableElement } from '../scene/layout-default/SceneGridRowEditableElement';
|
||||
import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types/EditableDashboardElement';
|
||||
import { VariableEditableElement } from '../settings/variables/VariableEditableElement';
|
||||
import { VariableSetEditableElement } from '../settings/variables/VariableSetEditableElement';
|
||||
|
||||
import { DashboardEditableElement } from './DashboardEditableElement';
|
||||
import { VizPanelEditableElement } from './VizPanelEditableElement';
|
||||
@@ -36,6 +37,10 @@ export function getEditableElementFor(sceneObj: SceneObject | undefined): Editab
|
||||
return new DashboardEditableElement(sceneObj);
|
||||
}
|
||||
|
||||
if (sceneObj instanceof SceneVariableSet) {
|
||||
return new VariableSetEditableElement(sceneObj);
|
||||
}
|
||||
|
||||
if (isSceneVariable(sceneObj)) {
|
||||
return new VariableEditableElement(sceneObj);
|
||||
}
|
||||
@@ -43,23 +48,6 @@ export function getEditableElementFor(sceneObj: SceneObject | undefined): Editab
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function hasEditableElement(sceneObj: SceneObject | undefined): boolean {
|
||||
if (!sceneObj) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
isEditableDashboardElement(sceneObj) ||
|
||||
sceneObj instanceof VizPanel ||
|
||||
sceneObj instanceof SceneGridRow ||
|
||||
sceneObj instanceof DashboardScene
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isSceneVariable(sceneObj: SceneObject): sceneObj is SceneVariable {
|
||||
return 'getValue' in sceneObj;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { SceneTimeRangeLike, VizPanel } from '@grafana/scenes';
|
||||
import { DataLinksInlineEditor, Input, TextArea, Switch } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { GenAIPanelDescriptionButton } from 'app/features/dashboard/components/GenAI/GenAIPanelDescriptionButton';
|
||||
import { GenAIPanelTitleButton } from 'app/features/dashboard/components/GenAI/GenAIPanelTitleButton';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
@@ -109,13 +108,12 @@ function ScenePanelLinksEditor({ panelLinks }: ScenePanelLinksEditorProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export function PanelFrameTitleInput({ panel }: { panel: VizPanel }) {
|
||||
export function PanelFrameTitleInput({ panel, isNewElement }: { panel: VizPanel; isNewElement?: boolean }) {
|
||||
const { title } = panel.useState();
|
||||
const notInPanelEdit = panel.getPanelContext().app !== CoreApp.PanelEditor;
|
||||
const newPanelTitle = t('dashboard.new-panel-title', 'New panel');
|
||||
|
||||
let ref = useEditPaneInputAutoFocus({
|
||||
autoFocus: notInPanelEdit && title === newPanelTitle,
|
||||
autoFocus: notInPanelEdit && isNewElement,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
+2
-1
@@ -27,7 +27,8 @@ export class SceneGridRowEditableElement implements EditableDashboardElement, Bu
|
||||
return {
|
||||
typeName: t('dashboard.edit-pane.elements.row', 'Row'),
|
||||
instanceName: sceneGraph.interpolate(this._row, this._row.state.title, undefined, 'text'),
|
||||
icon: 'line-alt',
|
||||
icon: 'list-ul',
|
||||
isContainer: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { EditableDashboardElement, EditableDashboardElementInfo } from '../types/EditableDashboardElement';
|
||||
import { LayoutParent } from '../types/LayoutParent';
|
||||
|
||||
import { getEditOptions } from './RowItemEditor';
|
||||
import { useEditOptions } from './RowItemEditor';
|
||||
import { RowItemRenderer } from './RowItemRenderer';
|
||||
import { RowItemRepeaterBehavior } from './RowItemRepeaterBehavior';
|
||||
import { RowItems } from './RowItems';
|
||||
@@ -45,7 +45,6 @@ export interface RowItemState extends SceneObjectState {
|
||||
fillScreen?: boolean;
|
||||
isDropTarget?: boolean;
|
||||
conditionalRendering?: ConditionalRendering;
|
||||
isNew?: boolean;
|
||||
}
|
||||
|
||||
export class RowItem
|
||||
@@ -89,6 +88,7 @@ export class RowItem
|
||||
typeName: t('dashboard.edit-pane.elements.row', 'Row'),
|
||||
instanceName: sceneGraph.interpolate(this, this.state.title, undefined, 'text'),
|
||||
icon: 'list-ul',
|
||||
isContainer: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -104,8 +104,8 @@ export class RowItem
|
||||
this.setState({ layout: this._layoutRestorer.getLayout(layout, this.state.layout) });
|
||||
}
|
||||
|
||||
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
|
||||
return getEditOptions(this);
|
||||
public useEditPaneOptions(isNewElement: boolean): OptionsPaneCategoryDescriptor[] {
|
||||
return useEditOptions(this, isNewElement);
|
||||
}
|
||||
|
||||
public onDelete() {
|
||||
@@ -187,7 +187,7 @@ export class RowItem
|
||||
}
|
||||
|
||||
public onChangeTitle(title: string) {
|
||||
this.setState({ title, isNew: false });
|
||||
this.setState({ title });
|
||||
}
|
||||
|
||||
public onHeaderHiddenToggle(hideHeader = !this.state.hideHeader) {
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useEditPaneInputAutoFocus } from '../layouts-shared/utils';
|
||||
|
||||
import { RowItem } from './RowItem';
|
||||
|
||||
export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[] {
|
||||
export function useEditOptions(model: RowItem, isNewElement: boolean): OptionsPaneCategoryDescriptor[] {
|
||||
const { layout } = model.useState();
|
||||
|
||||
const rowCategory = useMemo(
|
||||
@@ -25,7 +25,7 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[]
|
||||
.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.rows-layout.row-options.row.title', 'Title'),
|
||||
render: () => <RowTitleInput row={model} />,
|
||||
render: () => <RowTitleInput row={model} isNewElement={isNewElement} />,
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
@@ -40,7 +40,7 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[]
|
||||
render: () => <RowHeaderSwitch row={model} />,
|
||||
})
|
||||
),
|
||||
[model]
|
||||
[model, isNewElement]
|
||||
);
|
||||
|
||||
const repeatCategory = useMemo(
|
||||
@@ -78,9 +78,9 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[]
|
||||
return editOptions;
|
||||
}
|
||||
|
||||
function RowTitleInput({ row }: { row: RowItem }) {
|
||||
const { title, isNew } = row.useState();
|
||||
const ref = useEditPaneInputAutoFocus({ autoFocus: isNew });
|
||||
function RowTitleInput({ row, isNewElement }: { row: RowItem; isNewElement: boolean }) {
|
||||
const { title } = row.useState();
|
||||
const ref = useEditPaneInputAutoFocus({ autoFocus: isNewElement });
|
||||
const hasUniqueTitle = row.hasUniqueTitle();
|
||||
|
||||
return (
|
||||
|
||||
@@ -95,7 +95,7 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
|
||||
}
|
||||
|
||||
public addNewRow(row?: RowItem): RowItem {
|
||||
const newRow = row ?? new RowItem({ isNew: true });
|
||||
const newRow = row ?? new RowItem({});
|
||||
const existingNames = new Set(this.state.rows.map((row) => row.state.title).filter((title) => title !== undefined));
|
||||
|
||||
const newTitle = generateUniqueTitle(newRow.state.title, existingNames);
|
||||
|
||||
@@ -31,7 +31,7 @@ import { DashboardLayoutManager } from '../types/DashboardLayoutManager';
|
||||
import { EditableDashboardElement, EditableDashboardElementInfo } from '../types/EditableDashboardElement';
|
||||
import { LayoutParent } from '../types/LayoutParent';
|
||||
|
||||
import { getEditOptions } from './TabItemEditor';
|
||||
import { useEditOptions } from './TabItemEditor';
|
||||
import { TabItemRenderer } from './TabItemRenderer';
|
||||
import { TabItems } from './TabItems';
|
||||
import { TabsLayoutManager } from './TabsLayoutManager';
|
||||
@@ -39,10 +39,6 @@ import { TabsLayoutManager } from './TabsLayoutManager';
|
||||
export interface TabItemState extends SceneObjectState {
|
||||
layout: DashboardLayoutManager;
|
||||
title?: string;
|
||||
/**
|
||||
* Used to auto focus the title input
|
||||
*/
|
||||
isNew?: boolean;
|
||||
isDropTarget?: boolean;
|
||||
conditionalRendering?: ConditionalRendering;
|
||||
}
|
||||
@@ -89,6 +85,7 @@ export class TabItem
|
||||
typeName: t('dashboard.edit-pane.elements.tab', 'Tab'),
|
||||
instanceName: sceneGraph.interpolate(this, this.state.title, undefined, 'text'),
|
||||
icon: 'layers',
|
||||
isContainer: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -104,8 +101,8 @@ export class TabItem
|
||||
this.setState({ layout: this._layoutRestorer.getLayout(layout, this.state.layout) });
|
||||
}
|
||||
|
||||
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
|
||||
return getEditOptions(this);
|
||||
public useEditPaneOptions(isNewElement: boolean): OptionsPaneCategoryDescriptor[] {
|
||||
return useEditOptions(this, isNewElement);
|
||||
}
|
||||
|
||||
public onDelete() {
|
||||
@@ -172,7 +169,7 @@ export class TabItem
|
||||
}
|
||||
|
||||
public onChangeTitle(title: string) {
|
||||
this.setState({ title, isNew: false });
|
||||
this.setState({ title });
|
||||
}
|
||||
|
||||
public setIsDropTarget(isDropTarget: boolean) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useEditPaneInputAutoFocus } from '../layouts-shared/utils';
|
||||
|
||||
import { TabItem } from './TabItem';
|
||||
|
||||
export function getEditOptions(model: TabItem): OptionsPaneCategoryDescriptor[] {
|
||||
export function useEditOptions(model: TabItem, isNewElement: boolean): OptionsPaneCategoryDescriptor[] {
|
||||
const { layout } = model.useState();
|
||||
|
||||
const tabCategory = useMemo(
|
||||
@@ -19,10 +19,10 @@ export function getEditOptions(model: TabItem): OptionsPaneCategoryDescriptor[]
|
||||
new OptionsPaneCategoryDescriptor({ title: '', id: 'tab-item-options' }).addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard.tabs-layout.tab-options.title-option', 'Title'),
|
||||
render: () => <TabTitleInput tab={model} />,
|
||||
render: () => <TabTitleInput tab={model} isNewElement={isNewElement} />,
|
||||
})
|
||||
),
|
||||
[model]
|
||||
[model, isNewElement]
|
||||
);
|
||||
|
||||
const layoutCategory = useLayoutCategory(layout);
|
||||
@@ -41,9 +41,9 @@ export function getEditOptions(model: TabItem): OptionsPaneCategoryDescriptor[]
|
||||
return editOptions;
|
||||
}
|
||||
|
||||
function TabTitleInput({ tab }: { tab: TabItem }) {
|
||||
const { title, isNew } = tab.useState();
|
||||
const ref = useEditPaneInputAutoFocus({ autoFocus: isNew });
|
||||
function TabTitleInput({ tab, isNewElement }: { tab: TabItem; isNewElement: boolean }) {
|
||||
const { title } = tab.useState();
|
||||
const ref = useEditPaneInputAutoFocus({ autoFocus: isNewElement });
|
||||
const hasUniqueTitle = tab.hasUniqueTitle();
|
||||
|
||||
return (
|
||||
|
||||
@@ -126,7 +126,7 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
|
||||
}
|
||||
|
||||
public addNewTab(tab?: TabItem) {
|
||||
const newTab = tab ?? new TabItem({ isNew: true });
|
||||
const newTab = tab ?? new TabItem({});
|
||||
const existingNames = new Set(this.state.tabs.map((tab) => tab.state.title).filter((title) => title !== undefined));
|
||||
const newTitle = generateUniqueTitle(newTab.state.title, existingNames);
|
||||
if (newTitle !== newTab.state.title) {
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface EditableDashboardElement {
|
||||
/**
|
||||
* Hook that returns edit pane options
|
||||
*/
|
||||
useEditPaneOptions(): OptionsPaneCategoryDescriptor[];
|
||||
useEditPaneOptions(isNewElement: boolean): OptionsPaneCategoryDescriptor[];
|
||||
|
||||
/**
|
||||
* Panel Actions
|
||||
@@ -70,6 +70,11 @@ export interface EditableDashboardElementInfo {
|
||||
instanceName: string;
|
||||
typeName: string;
|
||||
icon: IconName;
|
||||
/**
|
||||
* Mark it as a container of other editable elements
|
||||
*/
|
||||
isContainer?: boolean;
|
||||
isHidden?: boolean;
|
||||
}
|
||||
|
||||
export function isEditableDashboardElement(obj: object): obj is EditableDashboardElement {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
import { ObjectRemovedFromCanvasEvent } from '../../edit-pane/shared';
|
||||
import { useEditPaneInputAutoFocus } from '../../scene/layouts-shared/utils';
|
||||
import { BulkActionElement } from '../../scene/types/BulkActionElement';
|
||||
import { EditableDashboardElement, EditableDashboardElementInfo } from '../../scene/types/EditableDashboardElement';
|
||||
import { VariableHideSelect } from '../../settings/variables/components/VariableHideSelect';
|
||||
@@ -23,12 +24,13 @@ export class VariableEditableElement implements EditableDashboardElement, BulkAc
|
||||
public getEditableElementInfo(): EditableDashboardElementInfo {
|
||||
return {
|
||||
typeName: t('dashboard.edit-pane.elements.variable', 'Variable'),
|
||||
icon: 'chart-line',
|
||||
icon: 'dollar-alt',
|
||||
instanceName: this.variable.state.name,
|
||||
isHidden: this.variable.state.hide === VariableHide.hideVariable,
|
||||
};
|
||||
}
|
||||
|
||||
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
|
||||
public useEditPaneOptions(isNewElement: boolean): OptionsPaneCategoryDescriptor[] {
|
||||
const variable = this.variable;
|
||||
|
||||
const options = useMemo(() => {
|
||||
@@ -37,7 +39,7 @@ export class VariableEditableElement implements EditableDashboardElement, BulkAc
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: t('dashboard-scene.variable-editor-form.name', 'Name'),
|
||||
popularRank: 1,
|
||||
render: () => <VariableNameInput variable={variable} />,
|
||||
render: () => <VariableNameInput variable={variable} isNewElement={isNewElement} />,
|
||||
})
|
||||
)
|
||||
.addItem(
|
||||
@@ -69,7 +71,7 @@ export class VariableEditableElement implements EditableDashboardElement, BulkAc
|
||||
render: () => <VariableTypeSelect variable={variable} />,
|
||||
})
|
||||
);
|
||||
}, [variable]);
|
||||
}, [variable, isNewElement]);
|
||||
|
||||
return [options];
|
||||
}
|
||||
@@ -87,9 +89,10 @@ interface VariableInputProps {
|
||||
variable: SceneVariable;
|
||||
}
|
||||
|
||||
function VariableNameInput({ variable }: VariableInputProps) {
|
||||
function VariableNameInput({ variable, isNewElement }: { variable: SceneVariable; isNewElement: boolean }) {
|
||||
const { name } = variable.useState();
|
||||
return <Input value={name} onChange={(e) => variable.setState({ name: e.currentTarget.value })} />;
|
||||
const ref = useEditPaneInputAutoFocus({ autoFocus: isNewElement });
|
||||
return <Input ref={ref} value={name} onChange={(e) => variable.setState({ name: e.currentTarget.value })} />;
|
||||
}
|
||||
|
||||
function VariableLabelInput({ variable }: VariableInputProps) {
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { SceneVariable, SceneVariableSet } from '@grafana/scenes';
|
||||
import { Stack, Button, useStyles2, Text, Box } from '@grafana/ui';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||
|
||||
import { NewObjectAddedToCanvasEvent } from '../../edit-pane/shared';
|
||||
import { EditableDashboardElement, EditableDashboardElementInfo } from '../../scene/types/EditableDashboardElement';
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
|
||||
import { getVariableDefault } from './utils';
|
||||
|
||||
export class VariableSetEditableElement implements EditableDashboardElement {
|
||||
public readonly isEditableDashboardElement = true;
|
||||
public readonly typeName = 'Variable';
|
||||
|
||||
public constructor(private set: SceneVariableSet) {}
|
||||
|
||||
public getEditableElementInfo(): EditableDashboardElementInfo {
|
||||
return {
|
||||
typeName: t('dashboard.edit-pane.elements.variable-set', 'Variables'),
|
||||
icon: 'x',
|
||||
instanceName: t('dashboard.edit-pane.elements.variable-set', 'Variables'),
|
||||
isContainer: true,
|
||||
};
|
||||
}
|
||||
|
||||
public useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
|
||||
const set = this.set;
|
||||
|
||||
const options = useMemo(() => {
|
||||
return new OptionsPaneCategoryDescriptor({ title: '', id: 'variables' }).addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: '',
|
||||
skipField: true,
|
||||
render: () => <VariableList set={set} />,
|
||||
})
|
||||
);
|
||||
}, [set]);
|
||||
|
||||
return [options];
|
||||
}
|
||||
}
|
||||
|
||||
function VariableList({ set }: { set: SceneVariableSet }) {
|
||||
const { variables } = set.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const onEditVariable = (variable: SceneVariable) => {
|
||||
const { editPane } = getDashboardSceneFor(set).state;
|
||||
editPane.selectObject(variable, variable.state.key!);
|
||||
};
|
||||
|
||||
const onAddVariable = () => {
|
||||
//add the new variable to the end of the array
|
||||
const newVariable = getVariableDefault(variables);
|
||||
set.setState({ variables: [...variables, newVariable] });
|
||||
set.publishEvent(new NewObjectAddedToCanvasEvent(newVariable), true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={0}>
|
||||
{variables.map((variable) => (
|
||||
<div className={styles.variableItem} key={variable.state.name} onClick={() => onEditVariable(variable)}>
|
||||
{/* eslint-disable-next-line @grafana/no-untranslated-strings */}
|
||||
<Text>${variable.state.name}</Text>
|
||||
<Stack direction="row" gap={1} alignItems="center">
|
||||
<Button variant="primary" size="sm" fill="outline">
|
||||
<Trans i18nKey="dashboard.edit-pane.variables.select-variable">Select</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
</div>
|
||||
))}
|
||||
<Box paddingBottom={1} display={'flex'}>
|
||||
<Button fullWidth icon="plus" size="sm" variant="secondary" onClick={onAddVariable}>
|
||||
<Trans i18nKey="dashboard.edit-pane.variables.add-variable">Add variable</Trans>
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
variableItem: css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
gap: theme.spacing(1),
|
||||
padding: theme.spacing(0.5),
|
||||
borderRadius: theme.shape.radius.default,
|
||||
cursor: 'pointer',
|
||||
[theme.transitions.handleMotion('no-preference', 'reduce')]: {
|
||||
transition: theme.transitions.create(['color'], {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
},
|
||||
'&:last-child': {
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
button: {
|
||||
visibility: 'hidden',
|
||||
},
|
||||
'&:hover': {
|
||||
color: theme.colors.text.link,
|
||||
button: {
|
||||
visibility: 'visible',
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -2786,7 +2786,8 @@
|
||||
"rows": "Rows",
|
||||
"tab": "Tab",
|
||||
"tabs": "Tabs",
|
||||
"variable": "Variable"
|
||||
"variable": "Variable",
|
||||
"variable-set": "Variables"
|
||||
},
|
||||
"open": "Open options pane",
|
||||
"row": {
|
||||
@@ -2794,6 +2795,10 @@
|
||||
"hide": "Hide",
|
||||
"title": "Row header"
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
"add-variable": "Add variable",
|
||||
"select-variable": "Select"
|
||||
}
|
||||
},
|
||||
"empty": {
|
||||
@@ -2915,10 +2920,9 @@
|
||||
"Recent options-title-recent-options": "Recent options"
|
||||
},
|
||||
"outline": {
|
||||
"tree": {
|
||||
"item": {
|
||||
"empty": "(empty)"
|
||||
}
|
||||
"tree-item": {
|
||||
"empty": "(empty)",
|
||||
"no-title": "<no title>"
|
||||
}
|
||||
},
|
||||
"override-category-title": {
|
||||
|
||||
Reference in New Issue
Block a user