mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Canvas: Add Pan and Zoom (#76705)
* Canvas: Add Zoom * Scale selecto components based on zoom state * Fix pan by reverting to 3.1.0 for zoom-pan * Update to latest library that fixes pan regression * Add mini map to canvas pan zoom * Fix selecto and anchors on hover * Update naming to be more clear * Switch back to contentComponent * Apply transformScale to drag and resize * Update connection source and target scaling * Add option to display mini map * Update yarn lock * Revert "Update yarn lock" This reverts commit3d1dd65d57
. * Set yarn lock to main * Revert "Set yarn lock to main" This reverts commit64bc50557e
. * Update to Yarn 4 * Add react-zoom-pan-pinch * Update react-zoom-pan checksum * Revert changes to json files * Remove last line of api merged * Remove last lines of all impacted jsons * Update home json * Update coordinate calc function to include scale * Fix types in coordinate calc function * Fix util calculation for transform * Fix arrow anchor shift behavior * Fix scale offset when adding elements during zoom * Fix drag of selected group during zoom * Add feature flag for canvas pan zoom * Revert "Add feature flag for canvas pan zoom" This reverts commitb026e31d8d
. * Regenerate feature flag after merge * Apply feature flag to enable pan zoom wrappers * Add mini map toggle behind feature flag * Simplify minimap behavior * Update feature flag registry * Set minimap to false by default * fix gen-cue * Set toggles gen to main Add blank line to toggle gen csv * Add canvas pan zoom to csv * Remove old comment * Change ref parameter to be more descriptive * Rename visibleFun to be more descriptive * Consolidate transformScale transformRef in util * Remove non-null assertion on connection parentRect * Consolidate parentRect null coalescing into object * Remove minimap and change toggle * Add controls inline help for pan and zoom * Clean up mouse events * Pull scale out of ref and isolate transform * Remove transform ref from scene div * Fix context menu visible behavior * Fix connections and update util functions * Move transform component instance to util * fix backend test * minor updates * Clean up connections / fix minor bug where offset of arrow wasn't being calculated correctly * missed connection code cleanup * cleanup scene code a bit more * actually fix backend test * move eslint disable line closer to actual issue --------- Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
This commit is contained in:
parent
c598306523
commit
2502fe4d19
@ -142,6 +142,7 @@ It extends [BaseDimensionConfig](#basedimensionconfig).
|
|||||||
| Property | Type | Required | Default | Description |
|
| Property | Type | Required | Default | Description |
|
||||||
|---------------------|-----------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------|
|
|---------------------|-----------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `inlineEditing` | boolean | **Yes** | `true` | Enable inline editing |
|
| `inlineEditing` | boolean | **Yes** | `true` | Enable inline editing |
|
||||||
|
| `panZoom` | boolean | **Yes** | `true` | Enable pan and zoom |
|
||||||
| `root` | [object](#root) | **Yes** | | The root element of canvas (frame), where all canvas elements are nested<br/>TODO: Figure out how to define a default value for this |
|
| `root` | [object](#root) | **Yes** | | The root element of canvas (frame), where all canvas elements are nested<br/>TODO: Figure out how to define a default value for this |
|
||||||
| `showAdvancedTypes` | boolean | **Yes** | `true` | Show all available element types |
|
| `showAdvancedTypes` | boolean | **Yes** | `true` | Show all available element types |
|
||||||
|
|
||||||
|
@ -160,6 +160,7 @@ Experimental features might be changed or removed without prior notice.
|
|||||||
| `extractFieldsNameDeduplication` | Make sure extracted field names are unique in the dataframe |
|
| `extractFieldsNameDeduplication` | Make sure extracted field names are unique in the dataframe |
|
||||||
| `dashboardSceneForViewers` | Enables dashboard rendering using Scenes for viewer roles |
|
| `dashboardSceneForViewers` | Enables dashboard rendering using Scenes for viewer roles |
|
||||||
| `dashboardScene` | Enables dashboard rendering using scenes for all roles |
|
| `dashboardScene` | Enables dashboard rendering using scenes for all roles |
|
||||||
|
| `canvasPanelPanZoom` | Allow pan and zoom in canvas panel |
|
||||||
| `logsInfiniteScrolling` | Enables infinite scrolling for the Logs panel in Explore and Dashboards |
|
| `logsInfiniteScrolling` | Enables infinite scrolling for the Logs panel in Explore and Dashboards |
|
||||||
| `flameGraphItemCollapsing` | Allow collapsing of flame graph items |
|
| `flameGraphItemCollapsing` | Allow collapsing of flame graph items |
|
||||||
| `logRowsPopoverMenu` | Enable filtering menu displayed when text of a log line is selected |
|
| `logRowsPopoverMenu` | Enable filtering menu displayed when text of a log line is selected |
|
||||||
|
@ -397,6 +397,7 @@
|
|||||||
"react-virtualized-auto-sizer": "1.0.7",
|
"react-virtualized-auto-sizer": "1.0.7",
|
||||||
"react-window": "1.8.9",
|
"react-window": "1.8.9",
|
||||||
"react-window-infinite-loader": "1.0.9",
|
"react-window-infinite-loader": "1.0.9",
|
||||||
|
"react-zoom-pan-pinch": "^3.3.0",
|
||||||
"redux": "4.2.1",
|
"redux": "4.2.1",
|
||||||
"redux-thunk": "2.4.2",
|
"redux-thunk": "2.4.2",
|
||||||
"regenerator-runtime": "0.14.0",
|
"regenerator-runtime": "0.14.0",
|
||||||
|
@ -158,6 +158,7 @@ export interface FeatureToggles {
|
|||||||
panelFilterVariable?: boolean;
|
panelFilterVariable?: boolean;
|
||||||
pdfTables?: boolean;
|
pdfTables?: boolean;
|
||||||
ssoSettingsApi?: boolean;
|
ssoSettingsApi?: boolean;
|
||||||
|
canvasPanelPanZoom?: boolean;
|
||||||
logsInfiniteScrolling?: boolean;
|
logsInfiniteScrolling?: boolean;
|
||||||
flameGraphItemCollapsing?: boolean;
|
flameGraphItemCollapsing?: boolean;
|
||||||
alertingDetailsViewV2?: boolean;
|
alertingDetailsViewV2?: boolean;
|
||||||
|
@ -109,6 +109,10 @@ export interface Options {
|
|||||||
* Enable inline editing
|
* Enable inline editing
|
||||||
*/
|
*/
|
||||||
inlineEditing: boolean;
|
inlineEditing: boolean;
|
||||||
|
/**
|
||||||
|
* Enable pan and zoom
|
||||||
|
*/
|
||||||
|
panZoom: boolean;
|
||||||
/**
|
/**
|
||||||
* The root element of canvas (frame), where all canvas elements are nested
|
* The root element of canvas (frame), where all canvas elements are nested
|
||||||
* TODO: Figure out how to define a default value for this
|
* TODO: Figure out how to define a default value for this
|
||||||
@ -135,5 +139,6 @@ export interface Options {
|
|||||||
|
|
||||||
export const defaultOptions: Partial<Options> = {
|
export const defaultOptions: Partial<Options> = {
|
||||||
inlineEditing: true,
|
inlineEditing: true,
|
||||||
|
panZoom: true,
|
||||||
showAdvancedTypes: true,
|
showAdvancedTypes: true,
|
||||||
};
|
};
|
||||||
|
@ -1178,6 +1178,14 @@ var (
|
|||||||
Owner: identityAccessTeam,
|
Owner: identityAccessTeam,
|
||||||
Created: time.Date(2023, time.November, 8, 12, 0, 0, 0, time.UTC),
|
Created: time.Date(2023, time.November, 8, 12, 0, 0, 0, time.UTC),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "canvasPanelPanZoom",
|
||||||
|
Description: "Allow pan and zoom in canvas panel",
|
||||||
|
Stage: FeatureStageExperimental,
|
||||||
|
FrontendOnly: true,
|
||||||
|
Owner: grafanaDatavizSquad,
|
||||||
|
Created: time.Date(2023, time.December, 27, 12, 0, 0, 0, time.UTC),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "logsInfiniteScrolling",
|
Name: "logsInfiniteScrolling",
|
||||||
Description: "Enables infinite scrolling for the Logs panel in Explore and Dashboards",
|
Description: "Enables infinite scrolling for the Logs panel in Explore and Dashboards",
|
||||||
|
@ -139,6 +139,7 @@ dashboardScene,experimental,@grafana/dashboards-squad,2023-11-13,false,false,fal
|
|||||||
panelFilterVariable,experimental,@grafana/dashboards-squad,2023-11-03,false,false,false,true
|
panelFilterVariable,experimental,@grafana/dashboards-squad,2023-11-03,false,false,false,true
|
||||||
pdfTables,privatePreview,@grafana/sharing-squad,2023-11-06,false,false,false,false
|
pdfTables,privatePreview,@grafana/sharing-squad,2023-11-06,false,false,false,false
|
||||||
ssoSettingsApi,experimental,@grafana/identity-access-team,2023-11-08,true,false,false,false
|
ssoSettingsApi,experimental,@grafana/identity-access-team,2023-11-08,true,false,false,false
|
||||||
|
canvasPanelPanZoom,experimental,@grafana/dataviz-squad,2023-12-27,false,false,false,true
|
||||||
logsInfiniteScrolling,experimental,@grafana/observability-logs,2023-11-09,false,false,false,true
|
logsInfiniteScrolling,experimental,@grafana/observability-logs,2023-11-09,false,false,false,true
|
||||||
flameGraphItemCollapsing,experimental,@grafana/observability-traces-and-profiling,2023-11-09,false,false,false,true
|
flameGraphItemCollapsing,experimental,@grafana/observability-traces-and-profiling,2023-11-09,false,false,false,true
|
||||||
alertingDetailsViewV2,experimental,@grafana/alerting-squad,2023-11-09,false,false,false,true
|
alertingDetailsViewV2,experimental,@grafana/alerting-squad,2023-11-09,false,false,false,true
|
||||||
|
|
@ -567,6 +567,10 @@ const (
|
|||||||
// Enables the SSO settings API
|
// Enables the SSO settings API
|
||||||
FlagSsoSettingsApi = "ssoSettingsApi"
|
FlagSsoSettingsApi = "ssoSettingsApi"
|
||||||
|
|
||||||
|
// FlagCanvasPanelPanZoom
|
||||||
|
// Allow pan and zoom in canvas panel
|
||||||
|
FlagCanvasPanelPanZoom = "canvasPanelPanZoom"
|
||||||
|
|
||||||
// FlagLogsInfiniteScrolling
|
// FlagLogsInfiniteScrolling
|
||||||
// Enables infinite scrolling for the Logs panel in Explore and Dashboards
|
// Enables infinite scrolling for the Logs panel in Explore and Dashboards
|
||||||
FlagLogsInfiniteScrolling = "logsInfiniteScrolling"
|
FlagLogsInfiniteScrolling = "logsInfiniteScrolling"
|
||||||
|
37
public/app/features/canvas/runtime/SceneTransformWrapper.tsx
Normal file
37
public/app/features/canvas/runtime/SceneTransformWrapper.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { TransformWrapper, TransformComponent, ReactZoomPanPinchRef } from 'react-zoom-pan-pinch';
|
||||||
|
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { Scene } from './scene';
|
||||||
|
|
||||||
|
type SceneTransformWrapperProps = {
|
||||||
|
scene: Scene;
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SceneTransformWrapper = ({ scene, children: sceneDiv }: SceneTransformWrapperProps) => {
|
||||||
|
const onZoom = (zoomPanPinchRef: ReactZoomPanPinchRef) => {
|
||||||
|
const scale = zoomPanPinchRef.state.scale;
|
||||||
|
if (scene.moveable && scale > 0) {
|
||||||
|
scene.moveable.zoom = 1 / scale;
|
||||||
|
}
|
||||||
|
scene.scale = scale;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TransformWrapper
|
||||||
|
doubleClick={{ mode: 'reset' }}
|
||||||
|
ref={scene.transformComponentRef}
|
||||||
|
onZoom={onZoom}
|
||||||
|
onTransformed={(_, state) => {
|
||||||
|
scene.scale = state.scale;
|
||||||
|
}}
|
||||||
|
limitToBounds={true}
|
||||||
|
disabled={!config.featureToggles.canvasPanelPanZoom || !scene.shouldPanZoom}
|
||||||
|
panning={{ allowLeftClickPan: false }}
|
||||||
|
>
|
||||||
|
<TransformComponent>{sceneDiv}</TransformComponent>
|
||||||
|
</TransformWrapper>
|
||||||
|
);
|
||||||
|
};
|
@ -197,7 +197,7 @@ export class ElementState implements LayerElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setPlacementFromConstraint(elementContainer?: DOMRect, parentContainer?: DOMRect) {
|
setPlacementFromConstraint(elementContainer?: DOMRect, parentContainer?: DOMRect, transformScale = 1) {
|
||||||
const { constraint } = this.options;
|
const { constraint } = this.options;
|
||||||
const { vertical, horizontal } = constraint ?? {};
|
const { vertical, horizontal } = constraint ?? {};
|
||||||
|
|
||||||
@ -214,25 +214,25 @@ export class ElementState implements LayerElement {
|
|||||||
|
|
||||||
const relativeTop =
|
const relativeTop =
|
||||||
elementContainer && parentContainer
|
elementContainer && parentContainer
|
||||||
? Math.round(elementContainer.top - parentContainer.top - parentBorderWidth)
|
? Math.round(elementContainer.top - parentContainer.top - parentBorderWidth) / transformScale
|
||||||
: 0;
|
: 0;
|
||||||
const relativeBottom =
|
const relativeBottom =
|
||||||
elementContainer && parentContainer
|
elementContainer && parentContainer
|
||||||
? Math.round(parentContainer.bottom - parentBorderWidth - elementContainer.bottom)
|
? Math.round(parentContainer.bottom - parentBorderWidth - elementContainer.bottom) / transformScale
|
||||||
: 0;
|
: 0;
|
||||||
const relativeLeft =
|
const relativeLeft =
|
||||||
elementContainer && parentContainer
|
elementContainer && parentContainer
|
||||||
? Math.round(elementContainer.left - parentContainer.left - parentBorderWidth)
|
? Math.round(elementContainer.left - parentContainer.left - parentBorderWidth) / transformScale
|
||||||
: 0;
|
: 0;
|
||||||
const relativeRight =
|
const relativeRight =
|
||||||
elementContainer && parentContainer
|
elementContainer && parentContainer
|
||||||
? Math.round(parentContainer.right - parentBorderWidth - elementContainer.right)
|
? Math.round(parentContainer.right - parentBorderWidth - elementContainer.right) / transformScale
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
const placement: Placement = {};
|
const placement: Placement = {};
|
||||||
|
|
||||||
const width = elementContainer?.width ?? 100;
|
const width = (elementContainer?.width ?? 100) / transformScale;
|
||||||
const height = elementContainer?.height ?? 100;
|
const height = (elementContainer?.height ?? 100) / transformScale;
|
||||||
|
|
||||||
switch (vertical) {
|
switch (vertical) {
|
||||||
case VerticalConstraint.Top:
|
case VerticalConstraint.Top:
|
||||||
@ -427,12 +427,12 @@ export class ElementState implements LayerElement {
|
|||||||
|
|
||||||
// kinda like:
|
// kinda like:
|
||||||
// https://github.com/grafana/grafana-edge-app/blob/main/src/panels/draw/WrapItem.tsx#L44
|
// https://github.com/grafana/grafana-edge-app/blob/main/src/panels/draw/WrapItem.tsx#L44
|
||||||
applyResize = (event: OnResize) => {
|
applyResize = (event: OnResize, transformScale = 1) => {
|
||||||
const placement = this.options.placement!;
|
const placement = this.options.placement!;
|
||||||
|
|
||||||
const style = event.target.style;
|
const style = event.target.style;
|
||||||
const deltaX = event.delta[0];
|
const deltaX = event.delta[0] / transformScale;
|
||||||
const deltaY = event.delta[1];
|
const deltaY = event.delta[1] / transformScale;
|
||||||
const dirLR = event.direction[0];
|
const dirLR = event.direction[0];
|
||||||
const dirTB = event.direction[1];
|
const dirTB = event.direction[1];
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import Moveable from 'moveable';
|
import Moveable from 'moveable';
|
||||||
import React, { CSSProperties } from 'react';
|
import React, { createRef, CSSProperties, RefObject } from 'react';
|
||||||
|
import { ReactZoomPanPinchContentRef } from 'react-zoom-pan-pinch';
|
||||||
import { BehaviorSubject, ReplaySubject, Subject, Subscription } from 'rxjs';
|
import { BehaviorSubject, ReplaySubject, Subject, Subscription } from 'rxjs';
|
||||||
import { first } from 'rxjs/operators';
|
import { first } from 'rxjs/operators';
|
||||||
import Selecto from 'selecto';
|
import Selecto from 'selecto';
|
||||||
@ -30,11 +31,13 @@ import { CanvasTooltip } from 'app/plugins/panel/canvas/components/CanvasTooltip
|
|||||||
import { CONNECTION_ANCHOR_DIV_ID } from 'app/plugins/panel/canvas/components/connections/ConnectionAnchors';
|
import { CONNECTION_ANCHOR_DIV_ID } from 'app/plugins/panel/canvas/components/connections/ConnectionAnchors';
|
||||||
import { Connections } from 'app/plugins/panel/canvas/components/connections/Connections';
|
import { Connections } from 'app/plugins/panel/canvas/components/connections/Connections';
|
||||||
import { AnchorPoint, CanvasTooltipPayload, LayerActionID } from 'app/plugins/panel/canvas/types';
|
import { AnchorPoint, CanvasTooltipPayload, LayerActionID } from 'app/plugins/panel/canvas/types';
|
||||||
|
import { getParent, getTransformInstance } from 'app/plugins/panel/canvas/utils';
|
||||||
|
|
||||||
import appEvents from '../../../core/app_events';
|
import appEvents from '../../../core/app_events';
|
||||||
import { CanvasPanel } from '../../../plugins/panel/canvas/CanvasPanel';
|
import { CanvasPanel } from '../../../plugins/panel/canvas/CanvasPanel';
|
||||||
import { HorizontalConstraint, Placement, VerticalConstraint } from '../types';
|
import { HorizontalConstraint, Placement, VerticalConstraint } from '../types';
|
||||||
|
|
||||||
|
import { SceneTransformWrapper } from './SceneTransformWrapper';
|
||||||
import { constraintViewable, dimensionViewable, settingsViewable } from './ables';
|
import { constraintViewable, dimensionViewable, settingsViewable } from './ables';
|
||||||
import { ElementState } from './element';
|
import { ElementState } from './element';
|
||||||
import { FrameState } from './frame';
|
import { FrameState } from './frame';
|
||||||
@ -57,6 +60,7 @@ export class Scene {
|
|||||||
|
|
||||||
width = 0;
|
width = 0;
|
||||||
height = 0;
|
height = 0;
|
||||||
|
scale = 1;
|
||||||
style: CSSProperties = {};
|
style: CSSProperties = {};
|
||||||
data?: PanelData;
|
data?: PanelData;
|
||||||
selecto?: Selecto;
|
selecto?: Selecto;
|
||||||
@ -66,9 +70,22 @@ export class Scene {
|
|||||||
currentLayer?: FrameState;
|
currentLayer?: FrameState;
|
||||||
isEditingEnabled?: boolean;
|
isEditingEnabled?: boolean;
|
||||||
shouldShowAdvancedTypes?: boolean;
|
shouldShowAdvancedTypes?: boolean;
|
||||||
|
shouldPanZoom?: boolean;
|
||||||
skipNextSelectionBroadcast = false;
|
skipNextSelectionBroadcast = false;
|
||||||
ignoreDataUpdate = false;
|
ignoreDataUpdate = false;
|
||||||
panel: CanvasPanel;
|
panel: CanvasPanel;
|
||||||
|
contextMenuVisible?: boolean;
|
||||||
|
contextMenuOnVisibilityChange = (visible: boolean) => {
|
||||||
|
this.contextMenuVisible = visible;
|
||||||
|
const transformInstance = getTransformInstance(this);
|
||||||
|
if (transformInstance) {
|
||||||
|
if (visible) {
|
||||||
|
transformInstance.setup.disabled = true;
|
||||||
|
} else {
|
||||||
|
transformInstance.setup.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
isPanelEditing = locationService.getSearchObject().editPanel !== undefined;
|
isPanelEditing = locationService.getSearchObject().editPanel !== undefined;
|
||||||
|
|
||||||
@ -84,15 +101,17 @@ export class Scene {
|
|||||||
subscription: Subscription;
|
subscription: Subscription;
|
||||||
|
|
||||||
targetsToSelect = new Set<HTMLDivElement>();
|
targetsToSelect = new Set<HTMLDivElement>();
|
||||||
|
transformComponentRef: RefObject<ReactZoomPanPinchContentRef> | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
cfg: CanvasFrameOptions,
|
cfg: CanvasFrameOptions,
|
||||||
enableEditing: boolean,
|
enableEditing: boolean,
|
||||||
showAdvancedTypes: boolean,
|
showAdvancedTypes: boolean,
|
||||||
|
panZoom: boolean,
|
||||||
public onSave: (cfg: CanvasFrameOptions) => void,
|
public onSave: (cfg: CanvasFrameOptions) => void,
|
||||||
panel: CanvasPanel
|
panel: CanvasPanel
|
||||||
) {
|
) {
|
||||||
this.root = this.load(cfg, enableEditing, showAdvancedTypes);
|
this.root = this.load(cfg, enableEditing, showAdvancedTypes, panZoom);
|
||||||
|
|
||||||
this.subscription = this.editModeEnabled.subscribe((open) => {
|
this.subscription = this.editModeEnabled.subscribe((open) => {
|
||||||
if (!this.moveable || !this.isEditingEnabled) {
|
if (!this.moveable || !this.isEditingEnabled) {
|
||||||
@ -103,6 +122,7 @@ export class Scene {
|
|||||||
|
|
||||||
this.panel = panel;
|
this.panel = panel;
|
||||||
this.connections = new Connections(this);
|
this.connections = new Connections(this);
|
||||||
|
this.transformComponentRef = createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
getNextElementName = (isFrame = false) => {
|
getNextElementName = (isFrame = false) => {
|
||||||
@ -124,7 +144,7 @@ export class Scene {
|
|||||||
return !this.byName.has(v);
|
return !this.byName.has(v);
|
||||||
};
|
};
|
||||||
|
|
||||||
load(cfg: CanvasFrameOptions, enableEditing: boolean, showAdvancedTypes: boolean) {
|
load(cfg: CanvasFrameOptions, enableEditing: boolean, showAdvancedTypes: boolean, panZoom: boolean) {
|
||||||
this.root = new RootElement(
|
this.root = new RootElement(
|
||||||
cfg ?? {
|
cfg ?? {
|
||||||
type: 'frame',
|
type: 'frame',
|
||||||
@ -136,6 +156,7 @@ export class Scene {
|
|||||||
|
|
||||||
this.isEditingEnabled = enableEditing;
|
this.isEditingEnabled = enableEditing;
|
||||||
this.shouldShowAdvancedTypes = showAdvancedTypes;
|
this.shouldShowAdvancedTypes = showAdvancedTypes;
|
||||||
|
this.shouldPanZoom = panZoom;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.div) {
|
if (this.div) {
|
||||||
@ -369,7 +390,7 @@ export class Scene {
|
|||||||
|
|
||||||
this.selecto = new Selecto({
|
this.selecto = new Selecto({
|
||||||
container: this.div,
|
container: this.div,
|
||||||
rootContainer: this.div,
|
rootContainer: getParent(this),
|
||||||
selectableTargets: targetElements,
|
selectableTargets: targetElements,
|
||||||
toggleContinueSelect: 'shift',
|
toggleContinueSelect: 'shift',
|
||||||
selectFromInside: false,
|
selectFromInside: false,
|
||||||
@ -439,7 +460,9 @@ export class Scene {
|
|||||||
e.events.forEach((event) => {
|
e.events.forEach((event) => {
|
||||||
const targetedElement = this.findElementByTarget(event.target);
|
const targetedElement = this.findElementByTarget(event.target);
|
||||||
if (targetedElement) {
|
if (targetedElement) {
|
||||||
targetedElement.setPlacementFromConstraint();
|
if (targetedElement) {
|
||||||
|
targetedElement.setPlacementFromConstraint(undefined, undefined, this.scale);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -449,7 +472,7 @@ export class Scene {
|
|||||||
.on('dragEnd', (event) => {
|
.on('dragEnd', (event) => {
|
||||||
const targetedElement = this.findElementByTarget(event.target);
|
const targetedElement = this.findElementByTarget(event.target);
|
||||||
if (targetedElement) {
|
if (targetedElement) {
|
||||||
targetedElement.setPlacementFromConstraint();
|
targetedElement.setPlacementFromConstraint(undefined, undefined, this.scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.moved.next(Date.now());
|
this.moved.next(Date.now());
|
||||||
@ -465,13 +488,13 @@ export class Scene {
|
|||||||
vertical: VerticalConstraint.Top,
|
vertical: VerticalConstraint.Top,
|
||||||
horizontal: HorizontalConstraint.Left,
|
horizontal: HorizontalConstraint.Left,
|
||||||
};
|
};
|
||||||
targetedElement.setPlacementFromConstraint();
|
targetedElement.setPlacementFromConstraint(undefined, undefined, this.scale);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('resize', (event) => {
|
.on('resize', (event) => {
|
||||||
const targetedElement = this.findElementByTarget(event.target);
|
const targetedElement = this.findElementByTarget(event.target);
|
||||||
if (targetedElement) {
|
if (targetedElement) {
|
||||||
targetedElement.applyResize(event);
|
targetedElement.applyResize(event, this.scale);
|
||||||
|
|
||||||
if (this.connections.connectionsNeedUpdate(targetedElement) && this.moveableActionCallback) {
|
if (this.connections.connectionsNeedUpdate(targetedElement) && this.moveableActionCallback) {
|
||||||
this.moveableActionCallback(true);
|
this.moveableActionCallback(true);
|
||||||
@ -507,7 +530,7 @@ export class Scene {
|
|||||||
targetedElement.tempConstraint = undefined;
|
targetedElement.tempConstraint = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
targetedElement.setPlacementFromConstraint();
|
targetedElement.setPlacementFromConstraint(undefined, undefined, this.scale);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -646,13 +669,36 @@ export class Scene {
|
|||||||
const isTooltipValid = (this.tooltip?.element?.data?.links?.length ?? 0) > 0;
|
const isTooltipValid = (this.tooltip?.element?.data?.links?.length ?? 0) > 0;
|
||||||
const canShowElementTooltip = !this.isEditingEnabled && isTooltipValid;
|
const canShowElementTooltip = !this.isEditingEnabled && isTooltipValid;
|
||||||
|
|
||||||
return (
|
const sceneDiv = (
|
||||||
<div key={this.revId} className={this.styles.wrap} style={this.style} ref={this.setRef}>
|
// TODO: Address this eslint error
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||||
|
<div
|
||||||
|
key={this.revId}
|
||||||
|
className={this.styles.wrap}
|
||||||
|
style={this.style}
|
||||||
|
ref={this.setRef}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
// If pan and zoom is disabled and middle mouse or ctrl + right mouse, don't pan
|
||||||
|
if ((!this.shouldPanZoom || this.contextMenuVisible) && (e.button === 1 || (e.button === 2 && e.ctrlKey))) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
// If context menu is hidden, ignore left mouse or non-ctrl right mouse for pan
|
||||||
|
if (!this.contextMenuVisible && e.button === 2 && !e.ctrlKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
{this.connections.render()}
|
{this.connections.render()}
|
||||||
{this.root.render()}
|
{this.root.render()}
|
||||||
{canShowContextMenu && (
|
{canShowContextMenu && (
|
||||||
<Portal>
|
<Portal>
|
||||||
<CanvasContextMenu scene={this} panel={this.panel} />
|
<CanvasContextMenu
|
||||||
|
scene={this}
|
||||||
|
panel={this.panel}
|
||||||
|
onVisibilityChange={this.contextMenuOnVisibilityChange}
|
||||||
|
/>
|
||||||
</Portal>
|
</Portal>
|
||||||
)}
|
)}
|
||||||
{canShowElementTooltip && (
|
{canShowElementTooltip && (
|
||||||
@ -662,6 +708,12 @@ export class Scene {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return config.featureToggles.canvasPanelPanZoom ? (
|
||||||
|
<SceneTransformWrapper scene={this}>{sceneDiv}</SceneTransformWrapper>
|
||||||
|
) : (
|
||||||
|
sceneDiv
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,6 +67,7 @@ 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.props.options.panZoom,
|
||||||
this.onUpdateScene,
|
this.onUpdateScene,
|
||||||
this
|
this
|
||||||
);
|
);
|
||||||
@ -227,14 +228,20 @@ export class CanvasPanel extends Component<Props, State> {
|
|||||||
const inlineEditingSwitched = this.props.options.inlineEditing !== nextProps.options.inlineEditing;
|
const inlineEditingSwitched = this.props.options.inlineEditing !== nextProps.options.inlineEditing;
|
||||||
const shouldShowAdvancedTypesSwitched =
|
const shouldShowAdvancedTypesSwitched =
|
||||||
this.props.options.showAdvancedTypes !== nextProps.options.showAdvancedTypes;
|
this.props.options.showAdvancedTypes !== nextProps.options.showAdvancedTypes;
|
||||||
if (this.needsReload || inlineEditingSwitched || shouldShowAdvancedTypesSwitched) {
|
const panZoomSwitched = this.props.options.panZoom !== nextProps.options.panZoom;
|
||||||
|
if (this.needsReload || inlineEditingSwitched || shouldShowAdvancedTypesSwitched || panZoomSwitched) {
|
||||||
if (inlineEditingSwitched) {
|
if (inlineEditingSwitched) {
|
||||||
// Replace scene div to prevent selecto instance leaks
|
// Replace scene div to prevent selecto instance leaks
|
||||||
this.scene.revId++;
|
this.scene.revId++;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.needsReload = false;
|
this.needsReload = false;
|
||||||
this.scene.load(nextProps.options.root, nextProps.options.inlineEditing, nextProps.options.showAdvancedTypes);
|
this.scene.load(
|
||||||
|
nextProps.options.root,
|
||||||
|
nextProps.options.inlineEditing,
|
||||||
|
nextProps.options.showAdvancedTypes,
|
||||||
|
nextProps.options.panZoom
|
||||||
|
);
|
||||||
this.scene.updateSize(nextProps.width, nextProps.height);
|
this.scene.updateSize(nextProps.width, nextProps.height);
|
||||||
this.scene.updateData(nextProps.data);
|
this.scene.updateData(nextProps.data);
|
||||||
changed = true;
|
changed = true;
|
||||||
|
@ -15,9 +15,10 @@ import { getElementTypes, onAddItem } from '../utils';
|
|||||||
type Props = {
|
type Props = {
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
panel: CanvasPanel;
|
panel: CanvasPanel;
|
||||||
|
onVisibilityChange: (v: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CanvasContextMenu = ({ scene, panel }: Props) => {
|
export const CanvasContextMenu = ({ scene, panel, onVisibilityChange }: Props) => {
|
||||||
const inlineEditorOpen = panel.state.openInlineEdit;
|
const inlineEditorOpen = 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 });
|
||||||
@ -29,7 +30,7 @@ export const CanvasContextMenu = ({ scene, panel }: Props) => {
|
|||||||
|
|
||||||
const handleContextMenu = useCallback(
|
const handleContextMenu = useCallback(
|
||||||
(event: Event) => {
|
(event: Event) => {
|
||||||
if (!(event instanceof MouseEvent)) {
|
if (!(event instanceof MouseEvent) || event.ctrlKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,8 +46,9 @@ export const CanvasContextMenu = ({ scene, panel }: Props) => {
|
|||||||
}
|
}
|
||||||
setAnchorPoint({ x: event.pageX, y: event.pageY });
|
setAnchorPoint({ x: event.pageX, y: event.pageY });
|
||||||
setIsMenuVisible(true);
|
setIsMenuVisible(true);
|
||||||
|
onVisibilityChange(true);
|
||||||
},
|
},
|
||||||
[scene, panel]
|
[scene, panel, onVisibilityChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -65,6 +67,7 @@ export const CanvasContextMenu = ({ scene, panel }: Props) => {
|
|||||||
|
|
||||||
const closeContextMenu = () => {
|
const closeContextMenu = () => {
|
||||||
setIsMenuVisible(false);
|
setIsMenuVisible(false);
|
||||||
|
onVisibilityChange(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderMenuItems = () => {
|
const renderMenuItems = () => {
|
||||||
@ -114,9 +117,10 @@ export const CanvasContextMenu = ({ scene, panel }: Props) => {
|
|||||||
let offsetY = anchorPoint.y;
|
let offsetY = anchorPoint.y;
|
||||||
let offsetX = anchorPoint.x;
|
let offsetX = anchorPoint.x;
|
||||||
if (scene.div) {
|
if (scene.div) {
|
||||||
|
const transformScale = scene.scale;
|
||||||
const sceneContainerDimensions = scene.div.getBoundingClientRect();
|
const sceneContainerDimensions = scene.div.getBoundingClientRect();
|
||||||
offsetY = offsetY - sceneContainerDimensions.top;
|
offsetY = (offsetY - sceneContainerDimensions.top) / transformScale;
|
||||||
offsetX = offsetX - sceneContainerDimensions.left;
|
offsetX = (offsetX - sceneContainerDimensions.left) / transformScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddItem(option, rootLayer, {
|
onAddItem(option, rootLayer, {
|
||||||
|
@ -7,7 +7,7 @@ import { config } from 'app/core/config';
|
|||||||
import { Scene } from 'app/features/canvas/runtime/scene';
|
import { Scene } from 'app/features/canvas/runtime/scene';
|
||||||
|
|
||||||
import { ConnectionState } from '../../types';
|
import { ConnectionState } from '../../types';
|
||||||
import { calculateCoordinates, getConnectionStyles } from '../../utils';
|
import { calculateCoordinates, getConnectionStyles, getParentBoundingClientRect } from '../../utils';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
setSVGRef: (anchorElement: SVGSVGElement) => void;
|
setSVGRef: (anchorElement: SVGSVGElement) => void;
|
||||||
@ -104,13 +104,14 @@ export const ConnectionSVG = ({ setSVGRef, setLineRef, scene }: Props) => {
|
|||||||
const { source, target, info } = v;
|
const { source, target, info } = v;
|
||||||
const sourceRect = source.div?.getBoundingClientRect();
|
const sourceRect = source.div?.getBoundingClientRect();
|
||||||
const parent = source.div?.parentElement;
|
const parent = source.div?.parentElement;
|
||||||
const parentRect = parent?.getBoundingClientRect();
|
const transformScale = scene.scale;
|
||||||
|
const parentRect = getParentBoundingClientRect(scene);
|
||||||
|
|
||||||
if (!sourceRect || !parent || !parentRect) {
|
if (!sourceRect || !parent || !parentRect) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { x1, y1, x2, y2 } = calculateCoordinates(sourceRect, parentRect, info, target);
|
const { x1, y1, x2, y2 } = calculateCoordinates(sourceRect, parentRect, info, target, transformScale);
|
||||||
|
|
||||||
const { strokeColor, strokeWidth } = getConnectionStyles(info, scene, defaultArrowSize);
|
const { strokeColor, strokeWidth } = getConnectionStyles(info, scene, defaultArrowSize);
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import { ElementState } from 'app/features/canvas/runtime/element';
|
|||||||
import { Scene } from 'app/features/canvas/runtime/scene';
|
import { Scene } from 'app/features/canvas/runtime/scene';
|
||||||
|
|
||||||
import { ConnectionState } from '../../types';
|
import { ConnectionState } from '../../types';
|
||||||
import { getConnections, isConnectionSource, isConnectionTarget } from '../../utils';
|
import { getConnections, getParentBoundingClientRect, isConnectionSource, isConnectionTarget } from '../../utils';
|
||||||
|
|
||||||
import { CONNECTION_ANCHOR_ALT, ConnectionAnchors, CONNECTION_ANCHOR_HIGHLIGHT_OFFSET } from './ConnectionAnchors';
|
import { CONNECTION_ANCHOR_ALT, ConnectionAnchors, CONNECTION_ANCHOR_HIGHLIGHT_OFFSET } from './ConnectionAnchors';
|
||||||
import { ConnectionSVG } from './ConnectionSVG';
|
import { ConnectionSVG } from './ConnectionSVG';
|
||||||
@ -103,7 +103,8 @@ export class Connections {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const elementBoundingRect = element.div!.getBoundingClientRect();
|
const elementBoundingRect = element.div!.getBoundingClientRect();
|
||||||
const parentBoundingRect = this.scene.div?.getBoundingClientRect();
|
const transformScale = this.scene.scale;
|
||||||
|
const parentBoundingRect = getParentBoundingClientRect(this.scene);
|
||||||
|
|
||||||
const relativeTop = elementBoundingRect.top - (parentBoundingRect?.top ?? 0);
|
const relativeTop = elementBoundingRect.top - (parentBoundingRect?.top ?? 0);
|
||||||
const relativeLeft = elementBoundingRect.left - (parentBoundingRect?.left ?? 0);
|
const relativeLeft = elementBoundingRect.left - (parentBoundingRect?.left ?? 0);
|
||||||
@ -111,10 +112,10 @@ export class Connections {
|
|||||||
if (this.connectionAnchorDiv) {
|
if (this.connectionAnchorDiv) {
|
||||||
this.connectionAnchorDiv.style.display = 'none';
|
this.connectionAnchorDiv.style.display = 'none';
|
||||||
this.connectionAnchorDiv.style.display = 'block';
|
this.connectionAnchorDiv.style.display = 'block';
|
||||||
this.connectionAnchorDiv.style.top = `${relativeTop}px`;
|
this.connectionAnchorDiv.style.top = `${relativeTop / transformScale}px`;
|
||||||
this.connectionAnchorDiv.style.left = `${relativeLeft}px`;
|
this.connectionAnchorDiv.style.left = `${relativeLeft / transformScale}px`;
|
||||||
this.connectionAnchorDiv.style.height = `${elementBoundingRect.height}px`;
|
this.connectionAnchorDiv.style.height = `${elementBoundingRect.height / transformScale}px`;
|
||||||
this.connectionAnchorDiv.style.width = `${elementBoundingRect.width}px`;
|
this.connectionAnchorDiv.style.width = `${elementBoundingRect.width / transformScale}px`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -140,12 +141,18 @@ export class Connections {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentBoundingRect = this.scene.div.parentElement.getBoundingClientRect();
|
const transformScale = this.scene.scale;
|
||||||
const x = event.pageX - parentBoundingRect.x;
|
const parentBoundingRect = getParentBoundingClientRect(this.scene);
|
||||||
const y = event.pageY - parentBoundingRect.y;
|
|
||||||
|
|
||||||
this.connectionLine.setAttribute('x2', `${x}`);
|
if (!parentBoundingRect) {
|
||||||
this.connectionLine.setAttribute('y2', `${y}`);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = event.pageX - parentBoundingRect.x ?? 0;
|
||||||
|
const y = event.pageY - parentBoundingRect.y ?? 0;
|
||||||
|
|
||||||
|
this.connectionLine.setAttribute('x2', `${x / transformScale}`);
|
||||||
|
this.connectionLine.setAttribute('y2', `${y / transformScale}`);
|
||||||
|
|
||||||
const connectionLineX1 = this.connectionLine.x1.baseVal.value;
|
const connectionLineX1 = this.connectionLine.x1.baseVal.value;
|
||||||
const connectionLineY1 = this.connectionLine.y1.baseVal.value;
|
const connectionLineY1 = this.connectionLine.y1.baseVal.value;
|
||||||
@ -161,15 +168,21 @@ export class Connections {
|
|||||||
if (!event.buttons) {
|
if (!event.buttons) {
|
||||||
if (this.connectionSource && this.connectionSource.div && this.connectionSource.div.parentElement) {
|
if (this.connectionSource && this.connectionSource.div && this.connectionSource.div.parentElement) {
|
||||||
const sourceRect = this.connectionSource.div.getBoundingClientRect();
|
const sourceRect = this.connectionSource.div.getBoundingClientRect();
|
||||||
const parentRect = this.connectionSource.div.parentElement.getBoundingClientRect();
|
|
||||||
|
|
||||||
const sourceVerticalCenter = sourceRect.top - parentRect.top + sourceRect.height / 2;
|
const transformScale = this.scene.scale;
|
||||||
const sourceHorizontalCenter = sourceRect.left - parentRect.left + sourceRect.width / 2;
|
const parentRect = getParentBoundingClientRect(this.scene);
|
||||||
|
|
||||||
|
if (!parentRect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceVerticalCenter = (sourceRect.top - parentRect.top + sourceRect.height / 2) / transformScale;
|
||||||
|
const sourceHorizontalCenter = (sourceRect.left - parentRect.left + sourceRect.width / 2) / transformScale;
|
||||||
|
|
||||||
// Convert from DOM coords to connection coords
|
// Convert from DOM coords to connection coords
|
||||||
// TODO: Break this out into util function and add tests
|
// TODO: Break this out into util function and add tests
|
||||||
const sourceX = (connectionLineX1 - sourceHorizontalCenter) / (sourceRect.width / 2);
|
const sourceX = (connectionLineX1 - sourceHorizontalCenter) / (sourceRect.width / 2 / transformScale);
|
||||||
const sourceY = (sourceVerticalCenter - connectionLineY1) / (sourceRect.height / 2);
|
const sourceY = (sourceVerticalCenter - connectionLineY1) / (sourceRect.height / 2 / transformScale);
|
||||||
|
|
||||||
let targetX;
|
let targetX;
|
||||||
let targetY;
|
let targetY;
|
||||||
@ -242,10 +255,20 @@ export class Connections {
|
|||||||
this.scene.selecto!.rootContainer!.style.cursor = 'crosshair';
|
this.scene.selecto!.rootContainer!.style.cursor = 'crosshair';
|
||||||
if (this.connectionSVG && this.connectionLine && this.scene.div && this.scene.div.parentElement) {
|
if (this.connectionSVG && this.connectionLine && this.scene.div && this.scene.div.parentElement) {
|
||||||
const connectionStartTargetBox = selectedTarget.getBoundingClientRect();
|
const connectionStartTargetBox = selectedTarget.getBoundingClientRect();
|
||||||
const parentBoundingRect = this.scene.div.parentElement.getBoundingClientRect();
|
|
||||||
|
|
||||||
const x = connectionStartTargetBox.x - parentBoundingRect.x + CONNECTION_ANCHOR_HIGHLIGHT_OFFSET;
|
const transformScale = this.scene.scale;
|
||||||
const y = connectionStartTargetBox.y - parentBoundingRect.y + CONNECTION_ANCHOR_HIGHLIGHT_OFFSET;
|
const parentBoundingRect = getParentBoundingClientRect(this.scene);
|
||||||
|
|
||||||
|
if (!parentBoundingRect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply by transform scale to calculate the correct scaled offset
|
||||||
|
const connectionAnchorOffsetX = CONNECTION_ANCHOR_HIGHLIGHT_OFFSET * transformScale;
|
||||||
|
const connectionAnchorOffsetY = CONNECTION_ANCHOR_HIGHLIGHT_OFFSET * transformScale;
|
||||||
|
|
||||||
|
const x = (connectionStartTargetBox.x - parentBoundingRect.x + connectionAnchorOffsetX) / transformScale;
|
||||||
|
const y = (connectionStartTargetBox.y - parentBoundingRect.y + connectionAnchorOffsetY) / transformScale;
|
||||||
|
|
||||||
const mouseX = clientX - parentBoundingRect.x;
|
const mouseX = clientX - parentBoundingRect.x;
|
||||||
const mouseY = clientY - parentBoundingRect.y;
|
const mouseY = clientY - parentBoundingRect.y;
|
||||||
|
57
public/app/plugins/panel/canvas/editor/panZoomHelp.tsx
Normal file
57
public/app/plugins/panel/canvas/editor/panZoomHelp.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { StandardEditorProps, GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { Alert, HorizontalGroup, Icon, VerticalGroup, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
|
const helpUrl = 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/canvas/';
|
||||||
|
|
||||||
|
export const PanZoomHelp = ({}: StandardEditorProps<string, unknown, unknown, unknown>) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HorizontalGroup className={styles.hGroup}>
|
||||||
|
<Alert
|
||||||
|
title="Pan and zoom controls"
|
||||||
|
severity="info"
|
||||||
|
buttonContent={<Icon name="question-circle" size="xl" />}
|
||||||
|
className={styles.alert}
|
||||||
|
onRemove={() => {
|
||||||
|
const newWindow = window.open(helpUrl, '_blank', 'noopener,noreferrer');
|
||||||
|
if (newWindow) {
|
||||||
|
newWindow.opener = null;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VerticalGroup>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Pan:
|
||||||
|
<ul>
|
||||||
|
<li>Middle mouse</li>
|
||||||
|
<li>CTRL + right mouse</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>Zoom: Scroll wheel</li>
|
||||||
|
<li>Reset: Double click</li>
|
||||||
|
</ul>
|
||||||
|
</VerticalGroup>
|
||||||
|
</Alert>
|
||||||
|
</HorizontalGroup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
alert: css({
|
||||||
|
'& div': { padding: '4px', alignItems: 'start' },
|
||||||
|
marginBottom: '0px',
|
||||||
|
marginTop: '5px',
|
||||||
|
padding: '2px',
|
||||||
|
'ul > li': { marginLeft: '10px' },
|
||||||
|
}),
|
||||||
|
hGroup: css({
|
||||||
|
'& div': { width: '100%' },
|
||||||
|
}),
|
||||||
|
});
|
@ -1,10 +1,12 @@
|
|||||||
import { FieldConfigProperty, PanelOptionsEditorBuilder, PanelPlugin } from '@grafana/data';
|
import { FieldConfigProperty, PanelOptionsEditorBuilder, PanelPlugin } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import { FrameState } from 'app/features/canvas/runtime/frame';
|
import { FrameState } from 'app/features/canvas/runtime/frame';
|
||||||
|
|
||||||
import { CanvasPanel, InstanceState } from './CanvasPanel';
|
import { CanvasPanel, InstanceState } from './CanvasPanel';
|
||||||
import { getConnectionEditor } from './editor/connectionEditor';
|
import { getConnectionEditor } from './editor/connectionEditor';
|
||||||
import { getElementEditor } from './editor/element/elementEditor';
|
import { getElementEditor } from './editor/element/elementEditor';
|
||||||
import { getLayerEditor } from './editor/layer/layerEditor';
|
import { getLayerEditor } from './editor/layer/layerEditor';
|
||||||
|
import { PanZoomHelp } from './editor/panZoomHelp';
|
||||||
import { canvasMigrationHandler } from './migrations';
|
import { canvasMigrationHandler } from './migrations';
|
||||||
import { Options } from './panelcfg.gen';
|
import { Options } from './panelcfg.gen';
|
||||||
|
|
||||||
@ -22,6 +24,21 @@ export const addStandardCanvasEditorOptions = (builder: PanelOptionsEditorBuilde
|
|||||||
description: 'Enable selection of experimental element types',
|
description: 'Enable selection of experimental element types',
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.addBooleanSwitch({
|
||||||
|
path: 'panZoom',
|
||||||
|
name: 'Pan and zoom',
|
||||||
|
description: 'Enable pan and zoom',
|
||||||
|
defaultValue: false,
|
||||||
|
showIf: (opts) => config.featureToggles.canvasPanelPanZoom,
|
||||||
|
});
|
||||||
|
builder.addCustomEditor({
|
||||||
|
id: 'panZoomHelp',
|
||||||
|
path: 'panZoomHelp',
|
||||||
|
name: '',
|
||||||
|
editor: PanZoomHelp,
|
||||||
|
showIf: (opts) => config.featureToggles.canvasPanelPanZoom && opts.panZoom,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const plugin = new PanelPlugin<Options>(CanvasPanel)
|
export const plugin = new PanelPlugin<Options>(CanvasPanel)
|
||||||
|
@ -88,6 +88,8 @@ composableKinds: PanelCfg: {
|
|||||||
inlineEditing: bool | *true
|
inlineEditing: bool | *true
|
||||||
// Show all available element types
|
// Show all available element types
|
||||||
showAdvancedTypes: bool | *true
|
showAdvancedTypes: bool | *true
|
||||||
|
// Enable pan and zoom
|
||||||
|
panZoom: bool | *true
|
||||||
// The root element of canvas (frame), where all canvas elements are nested
|
// The root element of canvas (frame), where all canvas elements are nested
|
||||||
// TODO: Figure out how to define a default value for this
|
// TODO: Figure out how to define a default value for this
|
||||||
root: {
|
root: {
|
||||||
|
@ -106,6 +106,10 @@ export interface Options {
|
|||||||
* Enable inline editing
|
* Enable inline editing
|
||||||
*/
|
*/
|
||||||
inlineEditing: boolean;
|
inlineEditing: boolean;
|
||||||
|
/**
|
||||||
|
* Enable pan and zoom
|
||||||
|
*/
|
||||||
|
panZoom: boolean;
|
||||||
/**
|
/**
|
||||||
* The root element of canvas (frame), where all canvas elements are nested
|
* The root element of canvas (frame), where all canvas elements are nested
|
||||||
* TODO: Figure out how to define a default value for this
|
* TODO: Figure out how to define a default value for this
|
||||||
@ -132,5 +136,6 @@ export interface Options {
|
|||||||
|
|
||||||
export const defaultOptions: Partial<Options> = {
|
export const defaultOptions: Partial<Options> = {
|
||||||
inlineEditing: true,
|
inlineEditing: true,
|
||||||
|
panZoom: true,
|
||||||
showAdvancedTypes: true,
|
showAdvancedTypes: true,
|
||||||
};
|
};
|
||||||
|
@ -190,27 +190,25 @@ export const calculateCoordinates = (
|
|||||||
sourceRect: DOMRect,
|
sourceRect: DOMRect,
|
||||||
parentRect: DOMRect,
|
parentRect: DOMRect,
|
||||||
info: CanvasConnection,
|
info: CanvasConnection,
|
||||||
target: ElementState
|
target: ElementState,
|
||||||
|
transformScale: number
|
||||||
) => {
|
) => {
|
||||||
const sourceHorizontalCenter = sourceRect.left - parentRect.left + sourceRect.width / 2;
|
const sourceHorizontalCenter = sourceRect.left - parentRect.left + sourceRect.width / 2;
|
||||||
const sourceVerticalCenter = sourceRect.top - parentRect.top + sourceRect.height / 2;
|
const sourceVerticalCenter = sourceRect.top - parentRect.top + sourceRect.height / 2;
|
||||||
|
|
||||||
// Convert from connection coords to DOM coords
|
// Convert from connection coords to DOM coords
|
||||||
const x1 = sourceHorizontalCenter + (info.source.x * sourceRect.width) / 2;
|
const x1 = (sourceHorizontalCenter + (info.source.x * sourceRect.width) / 2) / transformScale;
|
||||||
const y1 = sourceVerticalCenter - (info.source.y * sourceRect.height) / 2;
|
const y1 = (sourceVerticalCenter - (info.source.y * sourceRect.height) / 2) / transformScale;
|
||||||
|
|
||||||
let x2;
|
let x2: number;
|
||||||
let y2;
|
let y2: number;
|
||||||
|
|
||||||
if (info.targetName) {
|
|
||||||
const targetRect = target.div?.getBoundingClientRect();
|
const targetRect = target.div?.getBoundingClientRect();
|
||||||
if (targetRect) {
|
if (info.targetName && targetRect) {
|
||||||
const targetHorizontalCenter = targetRect.left - parentRect.left + targetRect.width / 2;
|
const targetHorizontalCenter = targetRect.left - parentRect.left + targetRect.width / 2;
|
||||||
const targetVerticalCenter = targetRect.top - parentRect.top + targetRect.height / 2;
|
const targetVerticalCenter = targetRect.top - parentRect.top + targetRect.height / 2;
|
||||||
|
|
||||||
x2 = targetHorizontalCenter + (info.target.x * targetRect.width) / 2;
|
x2 = targetHorizontalCenter + (info.target.x * targetRect.width) / 2;
|
||||||
y2 = targetVerticalCenter - (info.target.y * targetRect.height) / 2;
|
y2 = targetVerticalCenter - (info.target.y * targetRect.height) / 2;
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const parentHorizontalCenter = parentRect.width / 2;
|
const parentHorizontalCenter = parentRect.width / 2;
|
||||||
const parentVerticalCenter = parentRect.height / 2;
|
const parentVerticalCenter = parentRect.height / 2;
|
||||||
@ -218,6 +216,8 @@ export const calculateCoordinates = (
|
|||||||
x2 = parentHorizontalCenter + (info.target.x * parentRect.width) / 2;
|
x2 = parentHorizontalCenter + (info.target.x * parentRect.width) / 2;
|
||||||
y2 = parentVerticalCenter - (info.target.y * parentRect.height) / 2;
|
y2 = parentVerticalCenter - (info.target.y * parentRect.height) / 2;
|
||||||
}
|
}
|
||||||
|
x2 /= transformScale;
|
||||||
|
y2 /= transformScale;
|
||||||
return { x1, y1, x2, y2 };
|
return { x1, y1, x2, y2 };
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -239,3 +239,26 @@ export const getConnectionStyles = (info: CanvasConnection, scene: Scene, defaul
|
|||||||
const strokeWidth = info.size ? scene.context.getScale(info.size).get(lastRowIndex) : defaultArrowSize;
|
const strokeWidth = info.size ? scene.context.getScale(info.size).get(lastRowIndex) : defaultArrowSize;
|
||||||
return { strokeColor, strokeWidth };
|
return { strokeColor, strokeWidth };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getParentBoundingClientRect = (scene: Scene) => {
|
||||||
|
if (config.featureToggles.canvasPanelPanZoom) {
|
||||||
|
const transformRef = scene.transformComponentRef?.current;
|
||||||
|
return transformRef?.instance.contentComponent?.getBoundingClientRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
return scene.div?.getBoundingClientRect();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTransformInstance = (scene: Scene) => {
|
||||||
|
if (config.featureToggles.canvasPanelPanZoom) {
|
||||||
|
return scene.transformComponentRef?.current?.instance;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getParent = (scene: Scene) => {
|
||||||
|
if (config.featureToggles.canvasPanelPanZoom) {
|
||||||
|
return scene.transformComponentRef?.current?.instance.contentComponent;
|
||||||
|
}
|
||||||
|
return scene.div;
|
||||||
|
};
|
||||||
|
11
yarn.lock
11
yarn.lock
@ -17613,6 +17613,7 @@ __metadata:
|
|||||||
react-virtualized-auto-sizer: "npm:1.0.7"
|
react-virtualized-auto-sizer: "npm:1.0.7"
|
||||||
react-window: "npm:1.8.9"
|
react-window: "npm:1.8.9"
|
||||||
react-window-infinite-loader: "npm:1.0.9"
|
react-window-infinite-loader: "npm:1.0.9"
|
||||||
|
react-zoom-pan-pinch: "npm:^3.3.0"
|
||||||
redux: "npm:4.2.1"
|
redux: "npm:4.2.1"
|
||||||
redux-mock-store: "npm:1.5.4"
|
redux-mock-store: "npm:1.5.4"
|
||||||
redux-thunk: "npm:2.4.2"
|
redux-thunk: "npm:2.4.2"
|
||||||
@ -26217,6 +26218,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"react-zoom-pan-pinch@npm:^3.3.0":
|
||||||
|
version: 3.3.0
|
||||||
|
resolution: "react-zoom-pan-pinch@npm:3.3.0"
|
||||||
|
peerDependencies:
|
||||||
|
react: "*"
|
||||||
|
react-dom: "*"
|
||||||
|
checksum: 56e102f603dc5d0dbcff2effe403a1f46b718bde0bbad395fc990db0ef48f25fefb6969fec465948e90471a8e8e702cddaaa93742f77075cb1f4f32b10c1ab43
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"react@npm:18.2.0":
|
"react@npm:18.2.0":
|
||||||
version: 18.2.0
|
version: 18.2.0
|
||||||
resolution: "react@npm:18.2.0"
|
resolution: "react@npm:18.2.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user