diff --git a/.betterer.results b/.betterer.results index 15b93c2da1f..e12718e7ce2 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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"] diff --git a/packages/grafana-ui/src/components/Menu/MenuItem.tsx b/packages/grafana-ui/src/components/Menu/MenuItem.tsx index 559c0edbcd8..84b8c3f0520 100644 --- a/packages/grafana-ui/src/components/Menu/MenuItem.tsx +++ b/packages/grafana-ui/src/components/Menu/MenuItem.tsx @@ -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 { tabIndex?: number; /** List of menu items for the subMenu */ childItems?: Array>; + /** 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} /> )} diff --git a/packages/grafana-ui/src/components/Menu/SubMenu.tsx b/packages/grafana-ui/src/components/Menu/SubMenu.tsx index e0729ba2f9e..7ab86d04bf3 100644 --- a/packages/grafana-ui/src/components/Menu/SubMenu.tsx +++ b/packages/grafana-ui/src/components/Menu/SubMenu.tsx @@ -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 = React.memo( - ({ items, isOpen, openedWithArrow, setOpenedWithArrow, close }) => { + ({ items, isOpen, openedWithArrow, setOpenedWithArrow, close, customStyle }) => { const styles = useStyles2(getStyles); const localRef = useRef(null); const [handleKeys] = useMenuFocus({ @@ -48,6 +50,7 @@ export const SubMenu: React.FC = React.memo( ref={localRef} className={styles.subMenu(localRef.current)} aria-label={selectors.components.Menu.SubMenu.container} + style={customStyle} >
{items} diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx index 5a58a43aa3a..29ee1c39499 100644 --- a/public/app/features/canvas/runtime/scene.tsx +++ b/public/app/features/canvas/runtime/scene.tsx @@ -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(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 && ( - + )}
diff --git a/public/app/plugins/panel/canvas/CanvasContextMenu.tsx b/public/app/plugins/panel/canvas/CanvasContextMenu.tsx index 896eee787d7..8e299e8446d 100644 --- a/public/app/plugins/panel/canvas/CanvasContextMenu.tsx +++ b/public/app/plugins/panel/canvas/CanvasContextMenu.tsx @@ -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(false); const [anchorPoint, setAnchorPoint] = useState({ 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, string | React.JSXElementConstructor> + > = []; + typeOptions.map((option) => { + submenuItems.push( + onAddItem(option, rootLayer)} + /> + ); + }); + + return submenuItems; + }; + + const addItemMenuItem = !scene.isPanelEditing && ( + + ); + + const setBackgroundMenuItem = !scene.isPanelEditing && ( + { + 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; `, }); diff --git a/public/app/plugins/panel/canvas/CanvasPanel.tsx b/public/app/plugins/panel/canvas/CanvasPanel.tsx index b0f184782f6..e2f66a6665a 100644 --- a/public/app/plugins/panel/canvas/CanvasPanel.tsx +++ b/public/app/plugins/panel/canvas/CanvasPanel.tsx @@ -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 {} 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(1); @@ -49,6 +54,8 @@ export class CanvasPanel extends Component { 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 { 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 { 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 { 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 { 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 this.closeInlineEdit()} id={this.props.id} scene={activeCanvasPanel!.scene} />; + return this.closeInlineEdit()} id={this.props.id} scene={this.scene} />; + }; + + renderSetBackground = () => { + return ( + this.closeSetBackground()} + scene={this.scene} + anchorPoint={this.state.contextMenuAnchorPoint} + /> + ); }; render() { @@ -216,6 +259,7 @@ export class CanvasPanel extends Component { <> {this.scene.render()} {this.state.openInlineEdit && this.renderInlineEdit()} + {this.state.openSetBackground && this.renderSetBackground()} ); } diff --git a/public/app/plugins/panel/canvas/InlineEdit.tsx b/public/app/plugins/panel/canvas/InlineEdit.tsx index c8aa293cda1..3d3bc0f06ff 100644 --- a/public/app/plugins/panel/canvas/InlineEdit.tsx +++ b/public/app/plugins/panel/canvas/InlineEdit.tsx @@ -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; diff --git a/public/app/plugins/panel/canvas/InlineEditBody.tsx b/public/app/plugins/panel/canvas/InlineEditBody.tsx index 5f49051f5b4..b6344125491 100644 --- a/public/app/plugins/panel/canvas/InlineEditBody.tsx +++ b/public/app/plugins/panel/canvas/InlineEditBody.tsx @@ -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) => { - 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() { <>
{pane.items.map((item) => item.render())}
- + onAddItem(sel, rootLayer)} options={typeOptions} label={'Add item'} />
diff --git a/public/app/plugins/panel/canvas/SetBackground.tsx b/public/app/plugins/panel/canvas/SetBackground.tsx new file mode 100644 index 00000000000..c5984c6df0d --- /dev/null +++ b/public/app/plugins/panel/canvas/SetBackground.tsx @@ -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 ( + + + + ); +} + +const getStyles = (theme: GrafanaTheme2, anchorPoint: AnchorPoint) => ({ + portalWrapper: css` + width: 315px; + height: 445px; + transform: translate(${anchorPoint.x}px, ${anchorPoint.y - 200}px); + `, +}); diff --git a/public/app/plugins/panel/canvas/editor/TreeNavigationEditor.tsx b/public/app/plugins/panel/canvas/editor/TreeNavigationEditor.tsx index 251ff2a1b87..53af0e107d1 100644 --- a/public/app/plugins/panel/canvas/editor/TreeNavigationEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/TreeNavigationEditor.tsx @@ -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) => { - 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
- + onAddItem(sel, layer)} options={typeOptions} label={'Add item'} />
{selection.length > 0 && (