diff --git a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts index f8badb02569..c3086d322ea 100644 --- a/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/canvas/panelcfg/x/CanvasPanelCfg_types.gen.ts @@ -111,6 +111,10 @@ export const defaultCanvasElementOptions: Partial = { }; export interface Options { + /** + * Enable infinite pan + */ + infinitePan: boolean; /** * Enable inline editing */ @@ -144,6 +148,7 @@ export interface Options { } export const defaultOptions: Partial = { + infinitePan: true, inlineEditing: true, panZoom: true, showAdvancedTypes: true, diff --git a/public/app/features/canvas/runtime/SceneTransformWrapper.tsx b/public/app/features/canvas/runtime/SceneTransformWrapper.tsx index c868cd3d497..52ae32fa5f2 100644 --- a/public/app/features/canvas/runtime/SceneTransformWrapper.tsx +++ b/public/app/features/canvas/runtime/SceneTransformWrapper.tsx @@ -14,6 +14,15 @@ export const SceneTransformWrapper = ({ scene, children: sceneDiv }: SceneTransf const onZoom = (zoomPanPinchRef: ReactZoomPanPinchRef) => { const scale = zoomPanPinchRef.state.scale; scene.scale = scale; + + if (scene.shouldInfinitePan) { + const isScaleZoomedOut = scale < 1; + + if (isScaleZoomedOut) { + scene.updateSize(scene.width / scale, scene.height / scale); + scene.panel.forceUpdate(); + } + } }; const onZoomStop = (zoomPanPinchRef: ReactZoomPanPinchRef) => { @@ -46,6 +55,25 @@ export const SceneTransformWrapper = ({ scene, children: sceneDiv }: SceneTransf } }; + const onPanning = (_: ReactZoomPanPinchRef, event: MouseEvent | TouchEvent) => { + if (scene.shouldInfinitePan && event instanceof MouseEvent) { + // Get deltaX and deltaY from pan event and add it to current canvas dimensions + let deltaX = event.movementX; + let deltaY = event.movementY; + if (deltaX > 0) { + deltaX = 0; + } + if (deltaY > 0) { + deltaY = 0; + } + + // TODO: Consider bounding to the scene elements instead of allowing "infinite" panning + // TODO: Consider making scene grow in all directions vs just down to the right / bottom + scene.updateSize(scene.width - deltaX, scene.height - deltaY); + scene.panel.forceUpdate(); + } + }; + const onSceneContainerMouseDown = (e: React.MouseEvent) => { // If pan and zoom is disabled or context menu is visible, don't pan if ((!scene.shouldPanZoom || scene.contextMenuVisible) && (e.button === 1 || (e.button === 2 && e.ctrlKey))) { @@ -60,6 +88,9 @@ export const SceneTransformWrapper = ({ scene, children: sceneDiv }: SceneTransf } }; + // Set panel content overflow to hidden to prevent canvas content from overflowing + scene.div?.parentElement?.parentElement?.parentElement?.parentElement?.setAttribute('style', `overflow: hidden`); + return ( {/* The
element has child elements that allow for mouse events, so we need to disable the linter rule */} diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx index 83aede44d51..5e1608551aa 100644 --- a/public/app/features/canvas/runtime/scene.tsx +++ b/public/app/features/canvas/runtime/scene.tsx @@ -75,6 +75,7 @@ export class Scene { isEditingEnabled?: boolean; shouldShowAdvancedTypes?: boolean; shouldPanZoom?: boolean; + shouldInfinitePan?: boolean; skipNextSelectionBroadcast = false; ignoreDataUpdate = false; panel: CanvasPanel; @@ -112,10 +113,11 @@ export class Scene { enableEditing: boolean, showAdvancedTypes: boolean, panZoom: boolean, + infinitePan: boolean, public onSave: (cfg: CanvasFrameOptions) => void, panel: CanvasPanel ) { - this.root = this.load(cfg, enableEditing, showAdvancedTypes, panZoom); + this.root = this.load(cfg, enableEditing, showAdvancedTypes, panZoom, infinitePan); this.subscription = this.editModeEnabled.subscribe((open) => { if (!this.moveable || !this.isEditingEnabled) { @@ -148,7 +150,13 @@ export class Scene { return !this.byName.has(v); }; - load(cfg: CanvasFrameOptions, enableEditing: boolean, showAdvancedTypes: boolean, panZoom: boolean) { + load( + cfg: CanvasFrameOptions, + enableEditing: boolean, + showAdvancedTypes: boolean, + panZoom: boolean, + infinitePan: boolean + ) { this.root = new RootElement( cfg ?? { type: 'frame', @@ -161,6 +169,7 @@ export class Scene { this.isEditingEnabled = enableEditing; this.shouldShowAdvancedTypes = showAdvancedTypes; this.shouldPanZoom = panZoom; + this.shouldInfinitePan = infinitePan; setTimeout(() => { if (this.div) { diff --git a/public/app/plugins/panel/canvas/CanvasPanel.tsx b/public/app/plugins/panel/canvas/CanvasPanel.tsx index 88cdd6f30a0..195838a4231 100644 --- a/public/app/plugins/panel/canvas/CanvasPanel.tsx +++ b/public/app/plugins/panel/canvas/CanvasPanel.tsx @@ -68,6 +68,7 @@ export class CanvasPanel extends Component { this.props.options.inlineEditing, this.props.options.showAdvancedTypes, this.props.options.panZoom, + this.props.options.infinitePan, this.onUpdateScene, this ); @@ -229,7 +230,14 @@ export class CanvasPanel extends Component { const shouldShowAdvancedTypesSwitched = this.props.options.showAdvancedTypes !== nextProps.options.showAdvancedTypes; const panZoomSwitched = this.props.options.panZoom !== nextProps.options.panZoom; - if (this.needsReload || inlineEditingSwitched || shouldShowAdvancedTypesSwitched || panZoomSwitched) { + const infinitePanSwitched = this.props.options.infinitePan !== nextProps.options.infinitePan; + if ( + this.needsReload || + inlineEditingSwitched || + shouldShowAdvancedTypesSwitched || + panZoomSwitched || + infinitePanSwitched + ) { if (inlineEditingSwitched) { // Replace scene div to prevent selecto instance leaks this.scene.revId++; @@ -240,7 +248,8 @@ export class CanvasPanel extends Component { nextProps.options.root, nextProps.options.inlineEditing, nextProps.options.showAdvancedTypes, - nextProps.options.panZoom + nextProps.options.panZoom, + nextProps.options.infinitePan ); this.scene.updateSize(nextProps.width, nextProps.height); this.scene.updateData(nextProps.data); diff --git a/public/app/plugins/panel/canvas/module.tsx b/public/app/plugins/panel/canvas/module.tsx index 96ef9f144db..75566872d8b 100644 --- a/public/app/plugins/panel/canvas/module.tsx +++ b/public/app/plugins/panel/canvas/module.tsx @@ -39,6 +39,14 @@ export const addStandardCanvasEditorOptions = (builder: PanelOptionsEditorBuilde editor: PanZoomHelp, showIf: (opts) => config.featureToggles.canvasPanelPanZoom && opts.panZoom, }); + builder.addBooleanSwitch({ + path: 'infinitePan', + name: 'Infinite panning', + description: + 'Enable infinite panning - useful for expansive canvases. Warning: this an experimental feature and currently only works well with elements that are top / left constrained', + defaultValue: false, + showIf: (opts) => config.featureToggles.canvasPanelPanZoom && opts.panZoom, + }); }; export const plugin = new PanelPlugin(CanvasPanel) diff --git a/public/app/plugins/panel/canvas/panelcfg.cue b/public/app/plugins/panel/canvas/panelcfg.cue index 5c2941e2c1c..f16ee227568 100644 --- a/public/app/plugins/panel/canvas/panelcfg.cue +++ b/public/app/plugins/panel/canvas/panelcfg.cue @@ -94,6 +94,8 @@ composableKinds: PanelCfg: { showAdvancedTypes: bool | *true // Enable pan and zoom panZoom: bool | *true + // Enable infinite pan + infinitePan: bool | *true // The root element of canvas (frame), where all canvas elements are nested // TODO: Figure out how to define a default value for this root: { diff --git a/public/app/plugins/panel/canvas/panelcfg.gen.ts b/public/app/plugins/panel/canvas/panelcfg.gen.ts index 6efd9bedc8b..81008dd5d06 100644 --- a/public/app/plugins/panel/canvas/panelcfg.gen.ts +++ b/public/app/plugins/panel/canvas/panelcfg.gen.ts @@ -109,6 +109,10 @@ export const defaultCanvasElementOptions: Partial = { }; export interface Options { + /** + * Enable infinite pan + */ + infinitePan: boolean; /** * Enable inline editing */ @@ -142,6 +146,7 @@ export interface Options { } export const defaultOptions: Partial = { + infinitePan: true, inlineEditing: true, panZoom: true, showAdvancedTypes: true,