mirror of
https://github.com/grafana/grafana.git
synced 2025-01-09 23:53:25 -06:00
Canvas: Extend root context menu (#58097)
This commit is contained in:
parent
b799be3052
commit
9c7b6b1ce8
@ -7142,11 +7142,10 @@ exports[`better eslint`] = {
|
||||
"public/app/plugins/panel/canvas/InlineEditBody.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
|
||||
],
|
||||
"public/app/plugins/panel/canvas/editor/APIEditor.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
@ -7159,8 +7158,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/canvas/editor/TreeNavigationEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/canvas/editor/elementEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
@ -7176,7 +7174,8 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
|
||||
],
|
||||
"public/app/plugins/panel/canvas/utils.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/plugins/panel/dashlist/module.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { ReactElement, useCallback, useState, useRef, useImperativeHandle } from 'react';
|
||||
import React, { ReactElement, useCallback, useState, useRef, useImperativeHandle, CSSProperties } from 'react';
|
||||
|
||||
import { GrafanaTheme2, LinkTarget } from '@grafana/data';
|
||||
|
||||
@ -40,6 +40,8 @@ export interface MenuItemProps<T = any> {
|
||||
tabIndex?: number;
|
||||
/** List of menu items for the subMenu */
|
||||
childItems?: Array<ReactElement<MenuItemProps>>;
|
||||
/** Custom style for SubMenu */
|
||||
customSubMenuContainerStyles?: CSSProperties;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
@ -59,6 +61,7 @@ export const MenuItem = React.memo(
|
||||
childItems,
|
||||
role = 'menuitem',
|
||||
tabIndex = -1,
|
||||
customSubMenuContainerStyles,
|
||||
} = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
const [isActive, setIsActive] = useState(active);
|
||||
@ -138,6 +141,7 @@ export const MenuItem = React.memo(
|
||||
openedWithArrow={openedWithArrow}
|
||||
setOpenedWithArrow={setOpenedWithArrow}
|
||||
close={closeSubMenu}
|
||||
customStyle={customSubMenuContainerStyles}
|
||||
/>
|
||||
)}
|
||||
</ItemElement>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { ReactElement, useRef } from 'react';
|
||||
import React, { CSSProperties, ReactElement, useRef } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
@ -23,11 +23,13 @@ export interface SubMenuProps {
|
||||
setOpenedWithArrow: (openedWithArrow: boolean) => void;
|
||||
/** Closes the subMenu */
|
||||
close: () => void;
|
||||
/** Custom style */
|
||||
customStyle?: CSSProperties;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const SubMenu: React.FC<SubMenuProps> = React.memo(
|
||||
({ items, isOpen, openedWithArrow, setOpenedWithArrow, close }) => {
|
||||
({ items, isOpen, openedWithArrow, setOpenedWithArrow, close, customStyle }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const localRef = useRef<HTMLDivElement>(null);
|
||||
const [handleKeys] = useMenuFocus({
|
||||
@ -48,6 +50,7 @@ export const SubMenu: React.FC<SubMenuProps> = React.memo(
|
||||
ref={localRef}
|
||||
className={styles.subMenu(localRef.current)}
|
||||
aria-label={selectors.components.Menu.SubMenu.container}
|
||||
style={customStyle}
|
||||
>
|
||||
<div tabIndex={-1} className={styles.itemsWrapper} role="menu" onKeyDown={handleKeys}>
|
||||
{items}
|
||||
|
@ -26,8 +26,9 @@ import {
|
||||
getTextDimensionFromData,
|
||||
} from 'app/features/dimensions/utils';
|
||||
import { CanvasContextMenu } from 'app/plugins/panel/canvas/CanvasContextMenu';
|
||||
import { LayerActionID } from 'app/plugins/panel/canvas/types';
|
||||
import { AnchorPoint, LayerActionID } from 'app/plugins/panel/canvas/types';
|
||||
|
||||
import { CanvasPanel } from '../../../plugins/panel/canvas/CanvasPanel';
|
||||
import { HorizontalConstraint, Placement, VerticalConstraint } from '../types';
|
||||
|
||||
import { constraintViewable, dimensionViewable, settingsViewable } from './ables';
|
||||
@ -62,10 +63,12 @@ export class Scene {
|
||||
shouldShowAdvancedTypes?: boolean;
|
||||
skipNextSelectionBroadcast = false;
|
||||
ignoreDataUpdate = false;
|
||||
panel: CanvasPanel;
|
||||
|
||||
isPanelEditing = locationService.getSearchObject().editPanel !== undefined;
|
||||
|
||||
inlineEditingCallback?: () => void;
|
||||
setBackgroundCallback?: (anchorPoint: AnchorPoint) => void;
|
||||
|
||||
readonly editModeEnabled = new BehaviorSubject<boolean>(false);
|
||||
subscription: Subscription;
|
||||
@ -74,7 +77,8 @@ export class Scene {
|
||||
cfg: CanvasFrameOptions,
|
||||
enableEditing: boolean,
|
||||
showAdvancedTypes: boolean,
|
||||
public onSave: (cfg: CanvasFrameOptions) => void
|
||||
public onSave: (cfg: CanvasFrameOptions) => void,
|
||||
panel: CanvasPanel
|
||||
) {
|
||||
this.root = this.load(cfg, enableEditing, showAdvancedTypes);
|
||||
|
||||
@ -84,6 +88,8 @@ export class Scene {
|
||||
}
|
||||
this.moveable.draggable = !open;
|
||||
});
|
||||
|
||||
this.panel = panel;
|
||||
}
|
||||
|
||||
getNextElementName = (isFrame = false) => {
|
||||
@ -566,7 +572,7 @@ export class Scene {
|
||||
{this.root.render()}
|
||||
{canShowContextMenu && (
|
||||
<Portal>
|
||||
<CanvasContextMenu scene={this} />
|
||||
<CanvasContextMenu scene={this} panel={this.panel} />
|
||||
</Portal>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,32 +1,30 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useObservable } from 'react-use';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
import { ContextMenu, MenuItem } from '@grafana/ui';
|
||||
import { ContextMenu, MenuItem, MenuItemProps } from '@grafana/ui';
|
||||
import { Scene } from 'app/features/canvas/runtime/scene';
|
||||
|
||||
import { activePanelSubject } from './CanvasPanel';
|
||||
import { LayerActionID } from './types';
|
||||
import { FrameState } from '../../../features/canvas/runtime/frame';
|
||||
|
||||
import { CanvasPanel } from './CanvasPanel';
|
||||
import { AnchorPoint, LayerActionID } from './types';
|
||||
import { getElementTypes, onAddItem } from './utils';
|
||||
|
||||
type Props = {
|
||||
scene: Scene;
|
||||
panel: CanvasPanel;
|
||||
};
|
||||
|
||||
type AnchorPoint = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export const CanvasContextMenu = ({ scene }: Props) => {
|
||||
const activePanel = useObservable(activePanelSubject);
|
||||
const inlineEditorOpen = activePanel?.panel.state.openInlineEdit;
|
||||
export const CanvasContextMenu = ({ scene, panel }: Props) => {
|
||||
const inlineEditorOpen = panel.state.openInlineEdit;
|
||||
const [isMenuVisible, setIsMenuVisible] = useState<boolean>(false);
|
||||
const [anchorPoint, setAnchorPoint] = useState<AnchorPoint>({ x: 0, y: 0 });
|
||||
|
||||
const styles = getStyles();
|
||||
|
||||
const selectedElements = scene.selecto?.getSelectedTargets();
|
||||
const rootLayer: FrameState | undefined = panel.context?.instanceState?.layer;
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
(event: Event) => {
|
||||
@ -35,6 +33,8 @@ export const CanvasContextMenu = ({ scene }: Props) => {
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
panel.setActivePanel();
|
||||
|
||||
const shouldSelectElement = event.currentTarget !== scene.div;
|
||||
if (shouldSelectElement) {
|
||||
scene.select({ targets: [event.currentTarget as HTMLElement | SVGElement] });
|
||||
@ -42,7 +42,7 @@ export const CanvasContextMenu = ({ scene }: Props) => {
|
||||
setAnchorPoint({ x: event.pageX, y: event.pageY });
|
||||
setIsMenuVisible(true);
|
||||
},
|
||||
[scene]
|
||||
[scene, panel]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -70,7 +70,7 @@ export const CanvasContextMenu = ({ scene }: Props) => {
|
||||
onClick={() => {
|
||||
if (scene.inlineEditingCallback) {
|
||||
if (inlineEditorOpen) {
|
||||
activePanel.panel.closeInlineEdit();
|
||||
panel.closeInlineEdit();
|
||||
} else {
|
||||
scene.inlineEditingCallback();
|
||||
}
|
||||
@ -99,6 +99,47 @@ export const CanvasContextMenu = ({ scene }: Props) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const typeOptions = getElementTypes(scene.shouldShowAdvancedTypes).options;
|
||||
|
||||
const getTypeOptionsSubmenu = () => {
|
||||
const submenuItems: Array<
|
||||
React.ReactElement<MenuItemProps<unknown>, string | React.JSXElementConstructor<unknown>>
|
||||
> = [];
|
||||
typeOptions.map((option) => {
|
||||
submenuItems.push(
|
||||
<MenuItem
|
||||
key={option.value}
|
||||
label={option.label ?? 'Canvas item'}
|
||||
onClick={() => onAddItem(option, rootLayer)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return submenuItems;
|
||||
};
|
||||
|
||||
const addItemMenuItem = !scene.isPanelEditing && (
|
||||
<MenuItem
|
||||
label="Add item"
|
||||
className={styles.menuItem}
|
||||
childItems={getTypeOptionsSubmenu()}
|
||||
customSubMenuContainerStyles={{ maxHeight: '150px', overflowY: 'auto' }}
|
||||
/>
|
||||
);
|
||||
|
||||
const setBackgroundMenuItem = !scene.isPanelEditing && (
|
||||
<MenuItem
|
||||
label={'Set background'}
|
||||
onClick={() => {
|
||||
if (scene.setBackgroundCallback) {
|
||||
scene.setBackgroundCallback(anchorPoint);
|
||||
}
|
||||
closeContextMenu();
|
||||
}}
|
||||
className={styles.menuItem}
|
||||
/>
|
||||
);
|
||||
|
||||
if (selectedElements && selectedElements.length >= 1) {
|
||||
return (
|
||||
<>
|
||||
@ -139,7 +180,13 @@ export const CanvasContextMenu = ({ scene }: Props) => {
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return openCloseEditorMenuItem;
|
||||
return (
|
||||
<>
|
||||
{openCloseEditorMenuItem}
|
||||
{setBackgroundMenuItem}
|
||||
{addItemMenuItem}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -189,7 +236,6 @@ export const CanvasContextMenu = ({ scene }: Props) => {
|
||||
|
||||
const getStyles = () => ({
|
||||
menuItem: css`
|
||||
max-width: 60ch;
|
||||
overflow: hidden;
|
||||
max-width: 200px;
|
||||
`,
|
||||
});
|
||||
|
@ -10,13 +10,17 @@ import { Scene } from 'app/features/canvas/runtime/scene';
|
||||
import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events';
|
||||
|
||||
import { InlineEdit } from './InlineEdit';
|
||||
import { SetBackground } from './SetBackground';
|
||||
import { PanelOptions } from './models.gen';
|
||||
import { AnchorPoint } from './types';
|
||||
|
||||
interface Props extends PanelProps<PanelOptions> {}
|
||||
|
||||
interface State {
|
||||
refresh: number;
|
||||
openInlineEdit: boolean;
|
||||
openSetBackground: boolean;
|
||||
contextMenuAnchorPoint: AnchorPoint;
|
||||
}
|
||||
|
||||
export interface InstanceState {
|
||||
@ -31,6 +35,7 @@ export interface SelectionAction {
|
||||
let canvasInstances: CanvasPanel[] = [];
|
||||
let activeCanvasPanel: CanvasPanel | undefined = undefined;
|
||||
let isInlineEditOpen = false;
|
||||
let isSetBackgroundOpen = false;
|
||||
|
||||
export const activePanelSubject = new ReplaySubject<SelectionAction>(1);
|
||||
|
||||
@ -49,6 +54,8 @@ export class CanvasPanel extends Component<Props, State> {
|
||||
this.state = {
|
||||
refresh: 0,
|
||||
openInlineEdit: false,
|
||||
openSetBackground: false,
|
||||
contextMenuAnchorPoint: { x: 0, y: 0 },
|
||||
};
|
||||
|
||||
// Only the initial options are ever used.
|
||||
@ -57,11 +64,13 @@ export class CanvasPanel extends Component<Props, State> {
|
||||
this.props.options.root,
|
||||
this.props.options.inlineEditing,
|
||||
this.props.options.showAdvancedTypes,
|
||||
this.onUpdateScene
|
||||
this.onUpdateScene,
|
||||
this
|
||||
);
|
||||
this.scene.updateSize(props.width, props.height);
|
||||
this.scene.updateData(props.data);
|
||||
this.scene.inlineEditingCallback = this.openInlineEdit;
|
||||
this.scene.setBackgroundCallback = this.openSetBackground;
|
||||
|
||||
this.subs.add(
|
||||
this.props.eventBus.subscribe(PanelEditEnteredEvent, (evt: PanelEditEnteredEvent) => {
|
||||
@ -126,6 +135,7 @@ export class CanvasPanel extends Component<Props, State> {
|
||||
this.scene.subscription.unsubscribe();
|
||||
this.subs.unsubscribe();
|
||||
isInlineEditOpen = false;
|
||||
isSetBackgroundOpen = false;
|
||||
canvasInstances = canvasInstances.filter((ci) => ci.props.id !== activeCanvasPanel?.props.id);
|
||||
}
|
||||
|
||||
@ -164,6 +174,10 @@ export class CanvasPanel extends Component<Props, State> {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (this.state.openSetBackground !== nextState.openSetBackground) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// After editing, the options are valid, but the scene was in a different panel or inline editing mode has changed
|
||||
const shouldUpdateSceneAndPanel = this.needsReload && this.props.options !== nextProps.options;
|
||||
const inlineEditingSwitched = this.props.options.inlineEditing !== nextProps.options.inlineEditing;
|
||||
@ -197,18 +211,47 @@ export class CanvasPanel extends Component<Props, State> {
|
||||
isInlineEditOpen = true;
|
||||
};
|
||||
|
||||
openSetBackground = (anchorPoint: AnchorPoint) => {
|
||||
if (isSetBackgroundOpen) {
|
||||
this.forceUpdate();
|
||||
this.setActivePanel();
|
||||
return;
|
||||
}
|
||||
|
||||
this.setActivePanel();
|
||||
this.setState({ openSetBackground: true });
|
||||
this.setState({ contextMenuAnchorPoint: anchorPoint });
|
||||
|
||||
isSetBackgroundOpen = true;
|
||||
};
|
||||
|
||||
closeInlineEdit = () => {
|
||||
this.setState({ openInlineEdit: false });
|
||||
isInlineEditOpen = false;
|
||||
};
|
||||
|
||||
closeSetBackground = () => {
|
||||
this.setState({ openSetBackground: false });
|
||||
isSetBackgroundOpen = false;
|
||||
};
|
||||
|
||||
setActivePanel = () => {
|
||||
activeCanvasPanel = this;
|
||||
activePanelSubject.next({ panel: this });
|
||||
};
|
||||
|
||||
renderInlineEdit = () => {
|
||||
return <InlineEdit onClose={() => this.closeInlineEdit()} id={this.props.id} scene={activeCanvasPanel!.scene} />;
|
||||
return <InlineEdit onClose={() => this.closeInlineEdit()} id={this.props.id} scene={this.scene} />;
|
||||
};
|
||||
|
||||
renderSetBackground = () => {
|
||||
return (
|
||||
<SetBackground
|
||||
onClose={() => this.closeSetBackground()}
|
||||
scene={this.scene}
|
||||
anchorPoint={this.state.contextMenuAnchorPoint}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -216,6 +259,7 @@ export class CanvasPanel extends Component<Props, State> {
|
||||
<>
|
||||
{this.scene.render()}
|
||||
{this.state.openInlineEdit && this.renderInlineEdit()}
|
||||
{this.state.openSetBackground && this.renderSetBackground()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ export function InlineEdit({ onClose, id, scene }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const inlineEditKey = 'inlineEditPanel' + id.toString();
|
||||
|
||||
const defaultMeasurements = { width: 350, height: 400 };
|
||||
const defaultMeasurements = { width: 400, height: 400 };
|
||||
const widthOffset = root?.width ?? defaultMeasurements.width + OFFSET_X * 2;
|
||||
const defaultX = root?.x ?? 0 + widthOffset - defaultMeasurements.width - OFFSET_X;
|
||||
const defaultY = root?.y ?? 0 + OFFSET_Y;
|
||||
|
@ -3,34 +3,24 @@ import { get as lodashGet } from 'lodash';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useObservable } from 'react-use';
|
||||
|
||||
import {
|
||||
DataFrame,
|
||||
GrafanaTheme2,
|
||||
PanelOptionsEditorBuilder,
|
||||
SelectableValue,
|
||||
StandardEditorContext,
|
||||
} from '@grafana/data';
|
||||
import { DataFrame, GrafanaTheme2, PanelOptionsEditorBuilder, StandardEditorContext } from '@grafana/data';
|
||||
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
|
||||
import { NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders';
|
||||
import { useStyles2 } from '@grafana/ui/src';
|
||||
import { AddLayerButton } from 'app/core/components/Layers/AddLayerButton';
|
||||
import { notFoundItem } from 'app/features/canvas/elements/notFound';
|
||||
import { ElementState } from 'app/features/canvas/runtime/element';
|
||||
import { FrameState } from 'app/features/canvas/runtime/frame';
|
||||
import { OptionsPaneCategory } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategory';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { fillOptionsPaneItems } from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions';
|
||||
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
|
||||
|
||||
import { CanvasElementOptions, canvasElementRegistry } from '../../../features/canvas';
|
||||
|
||||
import { activePanelSubject, InstanceState } from './CanvasPanel';
|
||||
import { TabsEditor } from './editor/TabsEditor';
|
||||
import { getElementEditor } from './editor/elementEditor';
|
||||
import { getLayerEditor } from './editor/layerEditor';
|
||||
import { addStandardCanvasEditorOptions } from './module';
|
||||
import { InlineEditTabs } from './types';
|
||||
import { getElementTypes } from './utils';
|
||||
import { getElementTypes, onAddItem } from './utils';
|
||||
|
||||
export function InlineEditBody() {
|
||||
const activePanel = useObservable(activePanelSubject);
|
||||
@ -90,23 +80,6 @@ export function InlineEditBody() {
|
||||
const typeOptions = getElementTypes(instanceState?.scene.shouldShowAdvancedTypes).options;
|
||||
const rootLayer: FrameState | undefined = instanceState?.layer;
|
||||
|
||||
const onAddItem = (sel: SelectableValue<string>) => {
|
||||
const newItem = canvasElementRegistry.getIfExists(sel.value) ?? notFoundItem;
|
||||
const newElementOptions = newItem.getNewOptions() as CanvasElementOptions;
|
||||
newElementOptions.type = newItem.id;
|
||||
if (newItem.defaultSize) {
|
||||
newElementOptions.placement = { ...newElementOptions.placement, ...newItem.defaultSize };
|
||||
}
|
||||
if (rootLayer) {
|
||||
const newElement = new ElementState(newItem, newElementOptions, rootLayer);
|
||||
newElement.updateData(rootLayer.scene.context);
|
||||
rootLayer.elements.push(newElement);
|
||||
rootLayer.scene.save();
|
||||
|
||||
rootLayer.reinitializeMoveable();
|
||||
}
|
||||
};
|
||||
|
||||
const noElementSelected =
|
||||
instanceState && activeTab === InlineEditTabs.SelectedElement && instanceState.selected.length === 0;
|
||||
|
||||
@ -114,7 +87,7 @@ export function InlineEditBody() {
|
||||
<>
|
||||
<div style={topLevelItemsContainerStyle}>{pane.items.map((item) => item.render())}</div>
|
||||
<div style={topLevelItemsContainerStyle}>
|
||||
<AddLayerButton onChange={onAddItem} options={typeOptions} label={'Add item'} />
|
||||
<AddLayerButton onChange={(sel) => onAddItem(sel, rootLayer)} options={typeOptions} label={'Add item'} />
|
||||
</div>
|
||||
<div style={topLevelItemsContainerStyle}>
|
||||
<TabsEditor onTabChange={onTabChange} />
|
||||
|
67
public/app/plugins/panel/canvas/SetBackground.tsx
Normal file
67
public/app/plugins/panel/canvas/SetBackground.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Portal, useTheme2 } from '@grafana/ui';
|
||||
import { Scene } from 'app/features/canvas/runtime/scene';
|
||||
|
||||
import { MediaType, ResourceDimensionMode, ResourceFolderName } from '../../../features/dimensions';
|
||||
import { ResourcePickerPopover } from '../../../features/dimensions/editors/ResourcePickerPopover';
|
||||
|
||||
import { AnchorPoint } from './types';
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
scene: Scene;
|
||||
anchorPoint: AnchorPoint;
|
||||
};
|
||||
|
||||
export function SetBackground({ onClose, scene, anchorPoint }: Props) {
|
||||
const defaultValue = scene.root.options.background?.image?.fixed ?? '';
|
||||
|
||||
const [bgImage, setBgImage] = useState(defaultValue);
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme, anchorPoint);
|
||||
|
||||
const onChange = (value: string | undefined) => {
|
||||
if (value) {
|
||||
setBgImage(value);
|
||||
if (scene.root) {
|
||||
scene.root.options.background = {
|
||||
...scene.root.options.background,
|
||||
image: { mode: ResourceDimensionMode.Fixed, fixed: value },
|
||||
};
|
||||
scene.revId++;
|
||||
scene.save();
|
||||
|
||||
scene.root.reinitializeMoveable();
|
||||
}
|
||||
|
||||
// Force a re-render (update scene data after config update)
|
||||
if (scene) {
|
||||
scene.updateData(scene.data!);
|
||||
}
|
||||
}
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Portal className={styles.portalWrapper}>
|
||||
<ResourcePickerPopover
|
||||
onChange={onChange}
|
||||
value={bgImage}
|
||||
mediaType={MediaType.Image}
|
||||
folderName={ResourceFolderName.IOT}
|
||||
/>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2, anchorPoint: AnchorPoint) => ({
|
||||
portalWrapper: css`
|
||||
width: 315px;
|
||||
height: 445px;
|
||||
transform: translate(${anchorPoint.x}px, ${anchorPoint.y - 200}px);
|
||||
`,
|
||||
});
|
@ -3,19 +3,17 @@ import { Global } from '@emotion/react';
|
||||
import Tree, { TreeNodeProps } from 'rc-tree';
|
||||
import React, { Key, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||
import { GrafanaTheme2, StandardEditorProps } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Button, HorizontalGroup, Icon, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { ElementState } from 'app/features/canvas/runtime/element';
|
||||
|
||||
import { AddLayerButton } from '../../../../core/components/Layers/AddLayerButton';
|
||||
import { CanvasElementOptions, canvasElementRegistry } from '../../../../features/canvas';
|
||||
import { notFoundItem } from '../../../../features/canvas/elements/notFound';
|
||||
import { getGlobalStyles } from '../globalStyles';
|
||||
import { PanelOptions } from '../models.gen';
|
||||
import { getTreeData, onNodeDrop, TreeElement } from '../tree';
|
||||
import { DragNode, DropNode } from '../types';
|
||||
import { doSelect, getElementTypes } from '../utils';
|
||||
import { doSelect, getElementTypes, onAddItem } from '../utils';
|
||||
|
||||
import { TreeNodeTitle } from './TreeNodeTitle';
|
||||
import { TreeViewEditorProps } from './elementEditor';
|
||||
@ -109,21 +107,6 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps<any, TreeView
|
||||
allowSelection = allow;
|
||||
};
|
||||
|
||||
const onAddItem = (sel: SelectableValue<string>) => {
|
||||
const newItem = canvasElementRegistry.getIfExists(sel.value) ?? notFoundItem;
|
||||
const newElementOptions = newItem.getNewOptions() as CanvasElementOptions;
|
||||
newElementOptions.type = newItem.id;
|
||||
if (newItem.defaultSize) {
|
||||
newElementOptions.placement = { ...newElementOptions.placement, ...newItem.defaultSize };
|
||||
}
|
||||
const newElement = new ElementState(newItem, newElementOptions, layer);
|
||||
newElement.updateData(layer.scene.context);
|
||||
layer.elements.push(newElement);
|
||||
layer.scene.save();
|
||||
|
||||
layer.reinitializeMoveable();
|
||||
};
|
||||
|
||||
const onClearSelection = () => {
|
||||
layer.scene.clearCurrentSelection();
|
||||
};
|
||||
@ -166,7 +149,7 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps<any, TreeView
|
||||
|
||||
<HorizontalGroup justify="space-between">
|
||||
<div className={styles.addLayerButton}>
|
||||
<AddLayerButton onChange={onAddItem} options={typeOptions} label={'Add item'} />
|
||||
<AddLayerButton onChange={(sel) => onAddItem(sel, layer)} options={typeOptions} label={'Add item'} />
|
||||
</div>
|
||||
{selection.length > 0 && (
|
||||
<Button size="sm" variant="secondary" onClick={onClearSelection}>
|
||||
|
@ -20,3 +20,8 @@ export enum InlineEditTabs {
|
||||
ElementManagement = 'element-management',
|
||||
SelectedElement = 'selected-element',
|
||||
}
|
||||
|
||||
export type AnchorPoint = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
@ -2,7 +2,14 @@ import { AppEvents, PluginState, SelectableValue } from '@grafana/data';
|
||||
import { hasAlphaPanels } from 'app/core/config';
|
||||
|
||||
import appEvents from '../../../core/app_events';
|
||||
import { advancedElementItems, CanvasElementItem, defaultElementItems } from '../../../features/canvas';
|
||||
import {
|
||||
advancedElementItems,
|
||||
CanvasElementItem,
|
||||
CanvasElementOptions,
|
||||
canvasElementRegistry,
|
||||
defaultElementItems,
|
||||
} from '../../../features/canvas';
|
||||
import { notFoundItem } from '../../../features/canvas/elements/notFound';
|
||||
import { ElementState } from '../../../features/canvas/runtime/element';
|
||||
import { FrameState } from '../../../features/canvas/runtime/frame';
|
||||
import { Scene, SelectionParams } from '../../../features/canvas/runtime/scene';
|
||||
@ -69,3 +76,21 @@ export function getElementTypesOptions(
|
||||
|
||||
return selectables;
|
||||
}
|
||||
|
||||
export function onAddItem(sel: SelectableValue<string>, rootLayer: FrameState | undefined) {
|
||||
const newItem = canvasElementRegistry.getIfExists(sel.value) ?? notFoundItem;
|
||||
const newElementOptions = newItem.getNewOptions() as CanvasElementOptions;
|
||||
newElementOptions.type = newItem.id;
|
||||
if (newItem.defaultSize) {
|
||||
newElementOptions.placement = { ...newElementOptions.placement, ...newItem.defaultSize };
|
||||
}
|
||||
|
||||
if (rootLayer) {
|
||||
const newElement = new ElementState(newItem, newElementOptions, rootLayer);
|
||||
newElement.updateData(rootLayer.scene.context);
|
||||
rootLayer.elements.push(newElement);
|
||||
rootLayer.scene.save();
|
||||
|
||||
rootLayer.reinitializeMoveable();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user