mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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": [
|
"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.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[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.", "3"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
[0, 0, 0, "Do not use any type assertions.", "4"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "5"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
|
|
||||||
],
|
],
|
||||||
"public/app/plugins/panel/canvas/editor/APIEditor.tsx:5381": [
|
"public/app/plugins/panel/canvas/editor/APIEditor.tsx:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[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"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/plugins/panel/canvas/editor/TreeNavigationEditor.tsx:5381": [
|
"public/app/plugins/panel/canvas/editor/TreeNavigationEditor.tsx: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/canvas/editor/elementEditor.tsx:5381": [
|
"public/app/plugins/panel/canvas/editor/elementEditor.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[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"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
|
||||||
],
|
],
|
||||||
"public/app/plugins/panel/canvas/utils.ts:5381": [
|
"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": [
|
"public/app/plugins/panel/dashlist/module.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
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';
|
import { GrafanaTheme2, LinkTarget } from '@grafana/data';
|
||||||
|
|
||||||
@ -40,6 +40,8 @@ export interface MenuItemProps<T = any> {
|
|||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
/** List of menu items for the subMenu */
|
/** List of menu items for the subMenu */
|
||||||
childItems?: Array<ReactElement<MenuItemProps>>;
|
childItems?: Array<ReactElement<MenuItemProps>>;
|
||||||
|
/** Custom style for SubMenu */
|
||||||
|
customSubMenuContainerStyles?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
@ -59,6 +61,7 @@ export const MenuItem = React.memo(
|
|||||||
childItems,
|
childItems,
|
||||||
role = 'menuitem',
|
role = 'menuitem',
|
||||||
tabIndex = -1,
|
tabIndex = -1,
|
||||||
|
customSubMenuContainerStyles,
|
||||||
} = props;
|
} = props;
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const [isActive, setIsActive] = useState(active);
|
const [isActive, setIsActive] = useState(active);
|
||||||
@ -138,6 +141,7 @@ export const MenuItem = React.memo(
|
|||||||
openedWithArrow={openedWithArrow}
|
openedWithArrow={openedWithArrow}
|
||||||
setOpenedWithArrow={setOpenedWithArrow}
|
setOpenedWithArrow={setOpenedWithArrow}
|
||||||
close={closeSubMenu}
|
close={closeSubMenu}
|
||||||
|
customStyle={customSubMenuContainerStyles}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ItemElement>
|
</ItemElement>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { ReactElement, useRef } from 'react';
|
import React, { CSSProperties, ReactElement, useRef } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
@ -23,11 +23,13 @@ export interface SubMenuProps {
|
|||||||
setOpenedWithArrow: (openedWithArrow: boolean) => void;
|
setOpenedWithArrow: (openedWithArrow: boolean) => void;
|
||||||
/** Closes the subMenu */
|
/** Closes the subMenu */
|
||||||
close: () => void;
|
close: () => void;
|
||||||
|
/** Custom style */
|
||||||
|
customStyle?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
export const SubMenu: React.FC<SubMenuProps> = React.memo(
|
export const SubMenu: React.FC<SubMenuProps> = React.memo(
|
||||||
({ items, isOpen, openedWithArrow, setOpenedWithArrow, close }) => {
|
({ items, isOpen, openedWithArrow, setOpenedWithArrow, close, customStyle }) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const localRef = useRef<HTMLDivElement>(null);
|
const localRef = useRef<HTMLDivElement>(null);
|
||||||
const [handleKeys] = useMenuFocus({
|
const [handleKeys] = useMenuFocus({
|
||||||
@ -48,6 +50,7 @@ export const SubMenu: React.FC<SubMenuProps> = React.memo(
|
|||||||
ref={localRef}
|
ref={localRef}
|
||||||
className={styles.subMenu(localRef.current)}
|
className={styles.subMenu(localRef.current)}
|
||||||
aria-label={selectors.components.Menu.SubMenu.container}
|
aria-label={selectors.components.Menu.SubMenu.container}
|
||||||
|
style={customStyle}
|
||||||
>
|
>
|
||||||
<div tabIndex={-1} className={styles.itemsWrapper} role="menu" onKeyDown={handleKeys}>
|
<div tabIndex={-1} className={styles.itemsWrapper} role="menu" onKeyDown={handleKeys}>
|
||||||
{items}
|
{items}
|
||||||
|
@ -26,8 +26,9 @@ import {
|
|||||||
getTextDimensionFromData,
|
getTextDimensionFromData,
|
||||||
} from 'app/features/dimensions/utils';
|
} from 'app/features/dimensions/utils';
|
||||||
import { CanvasContextMenu } from 'app/plugins/panel/canvas/CanvasContextMenu';
|
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 { HorizontalConstraint, Placement, VerticalConstraint } from '../types';
|
||||||
|
|
||||||
import { constraintViewable, dimensionViewable, settingsViewable } from './ables';
|
import { constraintViewable, dimensionViewable, settingsViewable } from './ables';
|
||||||
@ -62,10 +63,12 @@ export class Scene {
|
|||||||
shouldShowAdvancedTypes?: boolean;
|
shouldShowAdvancedTypes?: boolean;
|
||||||
skipNextSelectionBroadcast = false;
|
skipNextSelectionBroadcast = false;
|
||||||
ignoreDataUpdate = false;
|
ignoreDataUpdate = false;
|
||||||
|
panel: CanvasPanel;
|
||||||
|
|
||||||
isPanelEditing = locationService.getSearchObject().editPanel !== undefined;
|
isPanelEditing = locationService.getSearchObject().editPanel !== undefined;
|
||||||
|
|
||||||
inlineEditingCallback?: () => void;
|
inlineEditingCallback?: () => void;
|
||||||
|
setBackgroundCallback?: (anchorPoint: AnchorPoint) => void;
|
||||||
|
|
||||||
readonly editModeEnabled = new BehaviorSubject<boolean>(false);
|
readonly editModeEnabled = new BehaviorSubject<boolean>(false);
|
||||||
subscription: Subscription;
|
subscription: Subscription;
|
||||||
@ -74,7 +77,8 @@ export class Scene {
|
|||||||
cfg: CanvasFrameOptions,
|
cfg: CanvasFrameOptions,
|
||||||
enableEditing: boolean,
|
enableEditing: boolean,
|
||||||
showAdvancedTypes: boolean,
|
showAdvancedTypes: boolean,
|
||||||
public onSave: (cfg: CanvasFrameOptions) => void
|
public onSave: (cfg: CanvasFrameOptions) => void,
|
||||||
|
panel: CanvasPanel
|
||||||
) {
|
) {
|
||||||
this.root = this.load(cfg, enableEditing, showAdvancedTypes);
|
this.root = this.load(cfg, enableEditing, showAdvancedTypes);
|
||||||
|
|
||||||
@ -84,6 +88,8 @@ export class Scene {
|
|||||||
}
|
}
|
||||||
this.moveable.draggable = !open;
|
this.moveable.draggable = !open;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.panel = panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
getNextElementName = (isFrame = false) => {
|
getNextElementName = (isFrame = false) => {
|
||||||
@ -566,7 +572,7 @@ export class Scene {
|
|||||||
{this.root.render()}
|
{this.root.render()}
|
||||||
{canShowContextMenu && (
|
{canShowContextMenu && (
|
||||||
<Portal>
|
<Portal>
|
||||||
<CanvasContextMenu scene={this} />
|
<CanvasContextMenu scene={this} panel={this.panel} />
|
||||||
</Portal>
|
</Portal>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,32 +1,30 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useObservable } from 'react-use';
|
|
||||||
import { first } from 'rxjs/operators';
|
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 { Scene } from 'app/features/canvas/runtime/scene';
|
||||||
|
|
||||||
import { activePanelSubject } from './CanvasPanel';
|
import { FrameState } from '../../../features/canvas/runtime/frame';
|
||||||
import { LayerActionID } from './types';
|
|
||||||
|
import { CanvasPanel } from './CanvasPanel';
|
||||||
|
import { AnchorPoint, LayerActionID } from './types';
|
||||||
|
import { getElementTypes, onAddItem } from './utils';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
|
panel: CanvasPanel;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AnchorPoint = {
|
export const CanvasContextMenu = ({ scene, panel }: Props) => {
|
||||||
x: number;
|
const inlineEditorOpen = panel.state.openInlineEdit;
|
||||||
y: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CanvasContextMenu = ({ scene }: Props) => {
|
|
||||||
const activePanel = useObservable(activePanelSubject);
|
|
||||||
const inlineEditorOpen = activePanel?.panel.state.openInlineEdit;
|
|
||||||
const [isMenuVisible, setIsMenuVisible] = useState<boolean>(false);
|
const [isMenuVisible, setIsMenuVisible] = useState<boolean>(false);
|
||||||
const [anchorPoint, setAnchorPoint] = useState<AnchorPoint>({ x: 0, y: 0 });
|
const [anchorPoint, setAnchorPoint] = useState<AnchorPoint>({ x: 0, y: 0 });
|
||||||
|
|
||||||
const styles = getStyles();
|
const styles = getStyles();
|
||||||
|
|
||||||
const selectedElements = scene.selecto?.getSelectedTargets();
|
const selectedElements = scene.selecto?.getSelectedTargets();
|
||||||
|
const rootLayer: FrameState | undefined = panel.context?.instanceState?.layer;
|
||||||
|
|
||||||
const handleContextMenu = useCallback(
|
const handleContextMenu = useCallback(
|
||||||
(event: Event) => {
|
(event: Event) => {
|
||||||
@ -35,6 +33,8 @@ export const CanvasContextMenu = ({ scene }: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
panel.setActivePanel();
|
||||||
|
|
||||||
const shouldSelectElement = event.currentTarget !== scene.div;
|
const shouldSelectElement = event.currentTarget !== scene.div;
|
||||||
if (shouldSelectElement) {
|
if (shouldSelectElement) {
|
||||||
scene.select({ targets: [event.currentTarget as HTMLElement | SVGElement] });
|
scene.select({ targets: [event.currentTarget as HTMLElement | SVGElement] });
|
||||||
@ -42,7 +42,7 @@ export const CanvasContextMenu = ({ scene }: Props) => {
|
|||||||
setAnchorPoint({ x: event.pageX, y: event.pageY });
|
setAnchorPoint({ x: event.pageX, y: event.pageY });
|
||||||
setIsMenuVisible(true);
|
setIsMenuVisible(true);
|
||||||
},
|
},
|
||||||
[scene]
|
[scene, panel]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -70,7 +70,7 @@ export const CanvasContextMenu = ({ scene }: Props) => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (scene.inlineEditingCallback) {
|
if (scene.inlineEditingCallback) {
|
||||||
if (inlineEditorOpen) {
|
if (inlineEditorOpen) {
|
||||||
activePanel.panel.closeInlineEdit();
|
panel.closeInlineEdit();
|
||||||
} else {
|
} else {
|
||||||
scene.inlineEditingCallback();
|
scene.inlineEditingCallback();
|
||||||
}
|
}
|
||||||
@ -99,6 +99,47 @@ export const CanvasContextMenu = ({ scene }: Props) => {
|
|||||||
return null;
|
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) {
|
if (selectedElements && selectedElements.length >= 1) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -139,7 +180,13 @@ export const CanvasContextMenu = ({ scene }: Props) => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return openCloseEditorMenuItem;
|
return (
|
||||||
|
<>
|
||||||
|
{openCloseEditorMenuItem}
|
||||||
|
{setBackgroundMenuItem}
|
||||||
|
{addItemMenuItem}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -189,7 +236,6 @@ export const CanvasContextMenu = ({ scene }: Props) => {
|
|||||||
|
|
||||||
const getStyles = () => ({
|
const getStyles = () => ({
|
||||||
menuItem: css`
|
menuItem: css`
|
||||||
max-width: 60ch;
|
max-width: 200px;
|
||||||
overflow: hidden;
|
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
@ -10,13 +10,17 @@ import { Scene } from 'app/features/canvas/runtime/scene';
|
|||||||
import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events';
|
import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events';
|
||||||
|
|
||||||
import { InlineEdit } from './InlineEdit';
|
import { InlineEdit } from './InlineEdit';
|
||||||
|
import { SetBackground } from './SetBackground';
|
||||||
import { PanelOptions } from './models.gen';
|
import { PanelOptions } from './models.gen';
|
||||||
|
import { AnchorPoint } from './types';
|
||||||
|
|
||||||
interface Props extends PanelProps<PanelOptions> {}
|
interface Props extends PanelProps<PanelOptions> {}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
refresh: number;
|
refresh: number;
|
||||||
openInlineEdit: boolean;
|
openInlineEdit: boolean;
|
||||||
|
openSetBackground: boolean;
|
||||||
|
contextMenuAnchorPoint: AnchorPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InstanceState {
|
export interface InstanceState {
|
||||||
@ -31,6 +35,7 @@ export interface SelectionAction {
|
|||||||
let canvasInstances: CanvasPanel[] = [];
|
let canvasInstances: CanvasPanel[] = [];
|
||||||
let activeCanvasPanel: CanvasPanel | undefined = undefined;
|
let activeCanvasPanel: CanvasPanel | undefined = undefined;
|
||||||
let isInlineEditOpen = false;
|
let isInlineEditOpen = false;
|
||||||
|
let isSetBackgroundOpen = false;
|
||||||
|
|
||||||
export const activePanelSubject = new ReplaySubject<SelectionAction>(1);
|
export const activePanelSubject = new ReplaySubject<SelectionAction>(1);
|
||||||
|
|
||||||
@ -49,6 +54,8 @@ export class CanvasPanel extends Component<Props, State> {
|
|||||||
this.state = {
|
this.state = {
|
||||||
refresh: 0,
|
refresh: 0,
|
||||||
openInlineEdit: false,
|
openInlineEdit: false,
|
||||||
|
openSetBackground: false,
|
||||||
|
contextMenuAnchorPoint: { x: 0, y: 0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only the initial options are ever used.
|
// 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.root,
|
||||||
this.props.options.inlineEditing,
|
this.props.options.inlineEditing,
|
||||||
this.props.options.showAdvancedTypes,
|
this.props.options.showAdvancedTypes,
|
||||||
this.onUpdateScene
|
this.onUpdateScene,
|
||||||
|
this
|
||||||
);
|
);
|
||||||
this.scene.updateSize(props.width, props.height);
|
this.scene.updateSize(props.width, props.height);
|
||||||
this.scene.updateData(props.data);
|
this.scene.updateData(props.data);
|
||||||
this.scene.inlineEditingCallback = this.openInlineEdit;
|
this.scene.inlineEditingCallback = this.openInlineEdit;
|
||||||
|
this.scene.setBackgroundCallback = this.openSetBackground;
|
||||||
|
|
||||||
this.subs.add(
|
this.subs.add(
|
||||||
this.props.eventBus.subscribe(PanelEditEnteredEvent, (evt: PanelEditEnteredEvent) => {
|
this.props.eventBus.subscribe(PanelEditEnteredEvent, (evt: PanelEditEnteredEvent) => {
|
||||||
@ -126,6 +135,7 @@ export class CanvasPanel extends Component<Props, State> {
|
|||||||
this.scene.subscription.unsubscribe();
|
this.scene.subscription.unsubscribe();
|
||||||
this.subs.unsubscribe();
|
this.subs.unsubscribe();
|
||||||
isInlineEditOpen = false;
|
isInlineEditOpen = false;
|
||||||
|
isSetBackgroundOpen = false;
|
||||||
canvasInstances = canvasInstances.filter((ci) => ci.props.id !== activeCanvasPanel?.props.id);
|
canvasInstances = canvasInstances.filter((ci) => ci.props.id !== activeCanvasPanel?.props.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,6 +174,10 @@ export class CanvasPanel extends Component<Props, State> {
|
|||||||
changed = true;
|
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
|
// 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 shouldUpdateSceneAndPanel = this.needsReload && this.props.options !== nextProps.options;
|
||||||
const inlineEditingSwitched = this.props.options.inlineEditing !== nextProps.options.inlineEditing;
|
const inlineEditingSwitched = this.props.options.inlineEditing !== nextProps.options.inlineEditing;
|
||||||
@ -197,18 +211,47 @@ export class CanvasPanel extends Component<Props, State> {
|
|||||||
isInlineEditOpen = true;
|
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 = () => {
|
closeInlineEdit = () => {
|
||||||
this.setState({ openInlineEdit: false });
|
this.setState({ openInlineEdit: false });
|
||||||
isInlineEditOpen = false;
|
isInlineEditOpen = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
closeSetBackground = () => {
|
||||||
|
this.setState({ openSetBackground: false });
|
||||||
|
isSetBackgroundOpen = false;
|
||||||
|
};
|
||||||
|
|
||||||
setActivePanel = () => {
|
setActivePanel = () => {
|
||||||
activeCanvasPanel = this;
|
activeCanvasPanel = this;
|
||||||
activePanelSubject.next({ panel: this });
|
activePanelSubject.next({ panel: this });
|
||||||
};
|
};
|
||||||
|
|
||||||
renderInlineEdit = () => {
|
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() {
|
render() {
|
||||||
@ -216,6 +259,7 @@ export class CanvasPanel extends Component<Props, State> {
|
|||||||
<>
|
<>
|
||||||
{this.scene.render()}
|
{this.scene.render()}
|
||||||
{this.state.openInlineEdit && this.renderInlineEdit()}
|
{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 styles = useStyles2(getStyles);
|
||||||
const inlineEditKey = 'inlineEditPanel' + id.toString();
|
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 widthOffset = root?.width ?? defaultMeasurements.width + OFFSET_X * 2;
|
||||||
const defaultX = root?.x ?? 0 + widthOffset - defaultMeasurements.width - OFFSET_X;
|
const defaultX = root?.x ?? 0 + widthOffset - defaultMeasurements.width - OFFSET_X;
|
||||||
const defaultY = root?.y ?? 0 + OFFSET_Y;
|
const defaultY = root?.y ?? 0 + OFFSET_Y;
|
||||||
|
@ -3,34 +3,24 @@ import { get as lodashGet } from 'lodash';
|
|||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { useObservable } from 'react-use';
|
import { useObservable } from 'react-use';
|
||||||
|
|
||||||
import {
|
import { DataFrame, GrafanaTheme2, PanelOptionsEditorBuilder, StandardEditorContext } from '@grafana/data';
|
||||||
DataFrame,
|
|
||||||
GrafanaTheme2,
|
|
||||||
PanelOptionsEditorBuilder,
|
|
||||||
SelectableValue,
|
|
||||||
StandardEditorContext,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
|
import { PanelOptionsSupplier } from '@grafana/data/src/panel/PanelPlugin';
|
||||||
import { NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders';
|
import { NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders';
|
||||||
import { useStyles2 } from '@grafana/ui/src';
|
import { useStyles2 } from '@grafana/ui/src';
|
||||||
import { AddLayerButton } from 'app/core/components/Layers/AddLayerButton';
|
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 { FrameState } from 'app/features/canvas/runtime/frame';
|
||||||
import { OptionsPaneCategory } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategory';
|
import { OptionsPaneCategory } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategory';
|
||||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||||
import { fillOptionsPaneItems } from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions';
|
import { fillOptionsPaneItems } from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions';
|
||||||
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
|
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
|
||||||
|
|
||||||
import { CanvasElementOptions, canvasElementRegistry } from '../../../features/canvas';
|
|
||||||
|
|
||||||
import { activePanelSubject, InstanceState } from './CanvasPanel';
|
import { activePanelSubject, InstanceState } from './CanvasPanel';
|
||||||
import { TabsEditor } from './editor/TabsEditor';
|
import { TabsEditor } from './editor/TabsEditor';
|
||||||
import { getElementEditor } from './editor/elementEditor';
|
import { getElementEditor } from './editor/elementEditor';
|
||||||
import { getLayerEditor } from './editor/layerEditor';
|
import { getLayerEditor } from './editor/layerEditor';
|
||||||
import { addStandardCanvasEditorOptions } from './module';
|
import { addStandardCanvasEditorOptions } from './module';
|
||||||
import { InlineEditTabs } from './types';
|
import { InlineEditTabs } from './types';
|
||||||
import { getElementTypes } from './utils';
|
import { getElementTypes, onAddItem } from './utils';
|
||||||
|
|
||||||
export function InlineEditBody() {
|
export function InlineEditBody() {
|
||||||
const activePanel = useObservable(activePanelSubject);
|
const activePanel = useObservable(activePanelSubject);
|
||||||
@ -90,23 +80,6 @@ export function InlineEditBody() {
|
|||||||
const typeOptions = getElementTypes(instanceState?.scene.shouldShowAdvancedTypes).options;
|
const typeOptions = getElementTypes(instanceState?.scene.shouldShowAdvancedTypes).options;
|
||||||
const rootLayer: FrameState | undefined = instanceState?.layer;
|
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 =
|
const noElementSelected =
|
||||||
instanceState && activeTab === InlineEditTabs.SelectedElement && instanceState.selected.length === 0;
|
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}>{pane.items.map((item) => item.render())}</div>
|
||||||
<div style={topLevelItemsContainerStyle}>
|
<div style={topLevelItemsContainerStyle}>
|
||||||
<AddLayerButton onChange={onAddItem} options={typeOptions} label={'Add item'} />
|
<AddLayerButton onChange={(sel) => onAddItem(sel, rootLayer)} options={typeOptions} label={'Add item'} />
|
||||||
</div>
|
</div>
|
||||||
<div style={topLevelItemsContainerStyle}>
|
<div style={topLevelItemsContainerStyle}>
|
||||||
<TabsEditor onTabChange={onTabChange} />
|
<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 Tree, { TreeNodeProps } from 'rc-tree';
|
||||||
import React, { Key, useEffect, useMemo, useState } from 'react';
|
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 { config } from '@grafana/runtime';
|
||||||
import { Button, HorizontalGroup, Icon, useStyles2, useTheme2 } from '@grafana/ui';
|
import { Button, HorizontalGroup, Icon, useStyles2, useTheme2 } from '@grafana/ui';
|
||||||
import { ElementState } from 'app/features/canvas/runtime/element';
|
import { ElementState } from 'app/features/canvas/runtime/element';
|
||||||
|
|
||||||
import { AddLayerButton } from '../../../../core/components/Layers/AddLayerButton';
|
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 { getGlobalStyles } from '../globalStyles';
|
||||||
import { PanelOptions } from '../models.gen';
|
import { PanelOptions } from '../models.gen';
|
||||||
import { getTreeData, onNodeDrop, TreeElement } from '../tree';
|
import { getTreeData, onNodeDrop, TreeElement } from '../tree';
|
||||||
import { DragNode, DropNode } from '../types';
|
import { DragNode, DropNode } from '../types';
|
||||||
import { doSelect, getElementTypes } from '../utils';
|
import { doSelect, getElementTypes, onAddItem } from '../utils';
|
||||||
|
|
||||||
import { TreeNodeTitle } from './TreeNodeTitle';
|
import { TreeNodeTitle } from './TreeNodeTitle';
|
||||||
import { TreeViewEditorProps } from './elementEditor';
|
import { TreeViewEditorProps } from './elementEditor';
|
||||||
@ -109,21 +107,6 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps<any, TreeView
|
|||||||
allowSelection = allow;
|
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 = () => {
|
const onClearSelection = () => {
|
||||||
layer.scene.clearCurrentSelection();
|
layer.scene.clearCurrentSelection();
|
||||||
};
|
};
|
||||||
@ -166,7 +149,7 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps<any, TreeView
|
|||||||
|
|
||||||
<HorizontalGroup justify="space-between">
|
<HorizontalGroup justify="space-between">
|
||||||
<div className={styles.addLayerButton}>
|
<div className={styles.addLayerButton}>
|
||||||
<AddLayerButton onChange={onAddItem} options={typeOptions} label={'Add item'} />
|
<AddLayerButton onChange={(sel) => onAddItem(sel, layer)} options={typeOptions} label={'Add item'} />
|
||||||
</div>
|
</div>
|
||||||
{selection.length > 0 && (
|
{selection.length > 0 && (
|
||||||
<Button size="sm" variant="secondary" onClick={onClearSelection}>
|
<Button size="sm" variant="secondary" onClick={onClearSelection}>
|
||||||
|
@ -20,3 +20,8 @@ export enum InlineEditTabs {
|
|||||||
ElementManagement = 'element-management',
|
ElementManagement = 'element-management',
|
||||||
SelectedElement = 'selected-element',
|
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 { hasAlphaPanels } from 'app/core/config';
|
||||||
|
|
||||||
import appEvents from '../../../core/app_events';
|
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 { ElementState } from '../../../features/canvas/runtime/element';
|
||||||
import { FrameState } from '../../../features/canvas/runtime/frame';
|
import { FrameState } from '../../../features/canvas/runtime/frame';
|
||||||
import { Scene, SelectionParams } from '../../../features/canvas/runtime/scene';
|
import { Scene, SelectionParams } from '../../../features/canvas/runtime/scene';
|
||||||
@ -69,3 +76,21 @@ export function getElementTypesOptions(
|
|||||||
|
|
||||||
return selectables;
|
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