diff --git a/package.json b/package.json index 723e3824b50..8775eb55545 100644 --- a/package.json +++ b/package.json @@ -295,6 +295,7 @@ "monaco-promql": "^1.7.2", "mousetrap": "1.6.5", "mousetrap-global-bind": "1.1.0", + "moveable": "0.26.0", "ol": "6.7.0", "papaparse": "5.3.0", "pluralize": "^8.0.0", @@ -331,6 +332,7 @@ "rst2html": "github:thoward/rst2html#990cb89f2a300cdd9151790be377c4c0840df809", "rxjs": "7.3.0", "search-query-parser": "1.5.4", + "selecto": "1.13.0", "semver": "^7.1.3", "slate": "0.47.8", "slate-plain-serializer": "0.7.10", diff --git a/public/app/features/canvas/runtime/element.tsx b/public/app/features/canvas/runtime/element.tsx index 706a56f5f8d..b74bfa33543 100644 --- a/public/app/features/canvas/runtime/element.tsx +++ b/public/app/features/canvas/runtime/element.tsx @@ -1,4 +1,6 @@ import React, { CSSProperties } from 'react'; +import { OnDrag, OnResize } from 'react-moveable/declaration/types'; + import { BackgroundImageSize, CanvasElementItem, @@ -15,7 +17,11 @@ export class ElementState { readonly UID = counter++; revId = 0; - style: CSSProperties = {}; + sizeStyle: CSSProperties = {}; + dataStyle: CSSProperties = {}; + + // Filled in by ref + div?: HTMLDivElement; // Calculated width = 100; @@ -34,10 +40,9 @@ export class ElementState { this.height = height; // Update the CSS position - this.style = { - ...this.style, - width, - height, + this.sizeStyle = { + ...this.options.placement, + position: 'absolute', }; } @@ -96,18 +101,14 @@ export class ElementState { } } - css.width = this.width; - css.height = this.height; - - this.style = css; + this.dataStyle = css; } - /** Recursivly visit all nodes */ + /** Recursively visit all nodes */ visit(visitor: (v: ElementState) => void) { visitor(this); } - // Something changed onChange(options: CanvasElementOptions) { if (this.item.id !== options.type) { this.item = canvasElementRegistry.getIfExists(options.type) ?? notFoundItem; @@ -126,10 +127,41 @@ export class ElementState { return { ...this.options }; } + initElement = (target: HTMLDivElement) => { + this.div = target; + + let placement = this.options.placement; + if (!placement) { + placement = { + left: 0, + top: 0, + }; + this.options.placement = placement; + } + }; + + applyDrag = (event: OnDrag) => { + const placement = this.options.placement; + placement!.top = event.top; + placement!.left = event.left; + + event.target.style.top = `${event.top}px`; + event.target.style.left = `${event.left}px`; + }; + + applyResize = (event: OnResize) => { + const placement = this.options.placement; + placement!.height = event.height; + placement!.width = event.width; + + event.target.style.height = `${event.height}px`; + event.target.style.width = `${event.width}px`; + }; + render() { const { item } = this; return ( -
+
); diff --git a/public/app/features/canvas/runtime/group.tsx b/public/app/features/canvas/runtime/group.tsx index 5b109e47c92..6758d59d78d 100644 --- a/public/app/features/canvas/runtime/group.tsx +++ b/public/app/features/canvas/runtime/group.tsx @@ -48,6 +48,11 @@ export class GroupState extends ElementState { for (const elem of this.elements) { elem.updateSize(this.width, this.height); } + + // The group forced to full width (for now) + this.sizeStyle.width = width; + this.sizeStyle.height = height; + this.sizeStyle.position = 'absolute'; } updateData(ctx: DimensionContext) { @@ -59,13 +64,13 @@ export class GroupState extends ElementState { render() { return ( -
+
{this.elements.map((v) => v.render())}
); } - /** Recursivly visit all nodes */ + /** Recursively visit all nodes */ visit(visitor: (v: ElementState) => void) { super.visit(visitor); for (const e of this.elements) { diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx index db5cdde330a..72ddc84bba4 100644 --- a/public/app/features/canvas/runtime/scene.tsx +++ b/public/app/features/canvas/runtime/scene.tsx @@ -1,5 +1,9 @@ import React, { CSSProperties } from 'react'; import { css } from '@emotion/css'; +import { ReplaySubject } from 'rxjs'; +import Moveable from 'moveable'; +import Selecto from 'selecto'; + import { config } from 'app/core/config'; import { GrafanaTheme2, PanelData } from '@grafana/data'; import { stylesFactory } from '@grafana/ui'; @@ -17,7 +21,6 @@ import { getResourceDimensionFromData, getTextDimensionFromData, } from 'app/features/dimensions/utils'; -import { ReplaySubject } from 'rxjs'; import { GroupState } from './group'; import { ElementState } from './element'; @@ -93,10 +96,83 @@ export class Scene { this.onSave(this.root.getSaveModel()); } + private findElementByTarget = (target: HTMLElement | SVGElement): ElementState | undefined => { + return this.root.elements.find((element) => element.div === target); + }; + + initMoveable = (sceneContainer: HTMLDivElement) => { + const targetElements: HTMLDivElement[] = []; + this.root.elements.forEach((element: ElementState) => { + targetElements.push(element.div!); + }); + + const selecto = new Selecto({ + container: sceneContainer, + selectableTargets: targetElements, + }); + + const moveable = new Moveable(sceneContainer, { + draggable: true, + resizable: true, + }) + .on('clickGroup', (event) => { + selecto.clickTarget(event.inputEvent, event.inputTarget); + }) + .on('drag', (event) => { + const targetedElement = this.findElementByTarget(event.target); + targetedElement!.applyDrag(event); + }) + .on('dragGroup', (e) => { + e.events.forEach((event) => { + const targetedElement = this.findElementByTarget(event.target); + targetedElement!.applyDrag(event); + }); + }) + .on('resize', (event) => { + const targetedElement = this.findElementByTarget(event.target); + targetedElement!.applyResize(event); + }) + .on('resizeGroup', (e) => { + e.events.forEach((event) => { + const targetedElement = this.findElementByTarget(event.target); + targetedElement!.applyResize(event); + }); + }); + + let targets: Array = []; + selecto + .on('dragStart', (event) => { + const selectedTarget = event.inputEvent.target; + + const isTargetMoveableElement = + moveable.isMoveableElement(selectedTarget) || + targets.some((target) => target === selectedTarget || target.contains(selectedTarget)); + + if (isTargetMoveableElement) { + // Prevent drawing selection box when selected target is a moveable element + event.stop(); + } + }) + .on('selectEnd', (event) => { + targets = event.selected; + moveable.target = targets; + + if (event.isDragStart) { + event.inputEvent.preventDefault(); + + setTimeout(() => { + moveable.dragStart(event.inputEvent); + }); + } + }); + }; + render() { return ( -
- {this.root.render()} +
+
+ {this.root.render()} +
); } diff --git a/public/app/plugins/panel/canvas/CanvasPanel.tsx b/public/app/plugins/panel/canvas/CanvasPanel.tsx index ef6328242f8..fe0de16f724 100644 --- a/public/app/plugins/panel/canvas/CanvasPanel.tsx +++ b/public/app/plugins/panel/canvas/CanvasPanel.tsx @@ -34,7 +34,7 @@ export class CanvasPanel extends Component { }; // Only the initial options are ever used. - // later changs are all controled by the scene + // later changes are all controlled by the scene this.scene = new Scene(this.props.options.root, this.onUpdateScene); this.scene.updateSize(props.width, props.height); this.scene.updateData(props.data); diff --git a/public/app/plugins/panel/canvas/plugin.json b/public/app/plugins/panel/canvas/plugin.json index 0dedb017b82..aaca95caa4e 100644 --- a/public/app/plugins/panel/canvas/plugin.json +++ b/public/app/plugins/panel/canvas/plugin.json @@ -5,7 +5,7 @@ "state": "alpha", "info": { - "description": "Explict element placement", + "description": "Explicit element placement", "author": { "name": "Grafana Labs", "url": "https://grafana.com" diff --git a/yarn.lock b/yarn.lock index f7467420cc0..f245dfe43e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1932,6 +1932,13 @@ __metadata: languageName: node linkType: hard +"@daybrush/utils@npm:^1.0.0, @daybrush/utils@npm:^1.1.1, @daybrush/utils@npm:^1.3.1, @daybrush/utils@npm:^1.4.0, @daybrush/utils@npm:^1.6.0": + version: 1.6.0 + resolution: "@daybrush/utils@npm:1.6.0" + checksum: 2579bc17e0d53ffbcb01c14c1f6baf32294f42b370e597d855e11cc5ab4de975502e91114c3e5bd05f76c8a6f83352c40ba60ac74c2fd1c3a741ce2e8899b137 + languageName: node + linkType: hard + "@discoveryjs/json-ext@npm:^0.5.0": version: 0.5.5 resolution: "@discoveryjs/json-ext@npm:0.5.5" @@ -1939,6 +1946,29 @@ __metadata: languageName: node linkType: hard +"@egjs/agent@npm:^2.2.1": + version: 2.3.0 + resolution: "@egjs/agent@npm:2.3.0" + checksum: 2506e8feff559ae256d5bf6ad15be5e207eb1b22d0c19aabe8292c41288ad7a0fe0527ca8a4c9227e16381a0e71237c6d061b5337d460a2cfad04859b41b0f20 + languageName: node + linkType: hard + +"@egjs/children-differ@npm:^1.0.1": + version: 1.0.1 + resolution: "@egjs/children-differ@npm:1.0.1" + dependencies: + "@egjs/list-differ": ^1.0.0 + checksum: 087f286822a93cc5ac2ba0beb9f332f3828a8870fea0e9d0a177078c96fc265ce74979be827a78dab674a833e3a22d4fe60636112a1aae93252e0a38f58ff754 + languageName: node + linkType: hard + +"@egjs/list-differ@npm:^1.0.0": + version: 1.0.0 + resolution: "@egjs/list-differ@npm:1.0.0" + checksum: d1827d134ddab12a024358367cb8a3b70d2ba0f286e5beb6ce2a6bbd021594f26b02c38b2d8b3d1dfd76bd7d1fcda54de3b40decaa9b4fc939230f6e7d531d0c + languageName: node + linkType: hard + "@emotion/babel-plugin@npm:^11.0.0": version: 11.3.0 resolution: "@emotion/babel-plugin@npm:11.3.0" @@ -4977,6 +5007,33 @@ __metadata: languageName: node linkType: hard +"@scena/dragscroll@npm:^1.0.1, @scena/dragscroll@npm:^1.0.2": + version: 1.0.2 + resolution: "@scena/dragscroll@npm:1.0.2" + dependencies: + "@scena/event-emitter": ^1.0.2 + checksum: 1680e30014fb768aa39319b0f2a053962a119249ab81a1d831669af7770409553a4c60c9fdb916c04d5174894f91d4f8c7c77d896e647934a7fee106d80d2683 + languageName: node + linkType: hard + +"@scena/event-emitter@npm:^1.0.2, @scena/event-emitter@npm:^1.0.3, @scena/event-emitter@npm:^1.0.5": + version: 1.0.5 + resolution: "@scena/event-emitter@npm:1.0.5" + dependencies: + "@daybrush/utils": ^1.1.1 + checksum: 400e0f6ab847aff6c1a6a6b62a0187193bdf9dedbe002ac31764b104bddb3c3bd1631df4252f52aa1677879842e96359aca46e025ed78d53c01facf8c76a5559 + languageName: node + linkType: hard + +"@scena/matrix@npm:^1.0.0, @scena/matrix@npm:^1.1.1": + version: 1.1.1 + resolution: "@scena/matrix@npm:1.1.1" + dependencies: + "@daybrush/utils": ^1.4.0 + checksum: e96aeab712e5fac4a437b1c1c2f5b445e47050c8b94dac2354bbece7d298b46bab5ea0049a24b360385f4edef8010538ee8d6f0ead570fbb83fdad9fe7e7b180 + languageName: node + linkType: hard + "@sentry/browser@npm:5.25.0": version: 5.25.0 resolution: "@sentry/browser@npm:5.25.0" @@ -13161,6 +13218,28 @@ __metadata: languageName: node linkType: hard +"css-styled@npm:^1.0.0": + version: 1.0.0 + resolution: "css-styled@npm:1.0.0" + dependencies: + "@daybrush/utils": ^1.0.0 + string-hash: ^1.1.3 + peerDependencies: + "@daybrush/utils": ">=1.0.0" + checksum: 3f2f995938f03db4e691d1aa6e17e902eae8e6462da7f5b17964398c44466d7c799060268114b45de67824144498cafaa2f944c12257910a76c848581b969f44 + languageName: node + linkType: hard + +"css-to-mat@npm:^1.0.3": + version: 1.0.3 + resolution: "css-to-mat@npm:1.0.3" + dependencies: + "@daybrush/utils": ^1.3.1 + "@scena/matrix": ^1.0.0 + checksum: a3fc98bfad2e500187736ad977f8753484c793a0aca453d811e11e778a4e26c320aa6419c1c12d7eb451c975d291d25a38139815a2c4b1a833bf044fc5501a4d + languageName: node + linkType: hard + "css-tree@npm:1.0.0-alpha.37": version: 1.0.0-alpha.37 resolution: "css-tree@npm:1.0.0-alpha.37" @@ -16785,6 +16864,20 @@ __metadata: languageName: node linkType: hard +"framework-utils@npm:^0.3.4": + version: 0.3.4 + resolution: "framework-utils@npm:0.3.4" + checksum: 7edc71c02da231ffc0bd3823c7cc590de205f58771f1829b9e4094a5d18cf0b092fc86f6cbf89d3e1549680451000bf43a5ba3eb67c468486fb620f27cf8fe66 + languageName: node + linkType: hard + +"framework-utils@npm:^1.1.0": + version: 1.1.0 + resolution: "framework-utils@npm:1.1.0" + checksum: 01b61ead1756ad9bb0d281d22d35c83ee9df3e5e3d63b2d19eb9ca4ba9e69439b698c623ac8161df3744233331017bfd79d82c44e1e61fbb30f0ea6b48d883f9 + languageName: node + linkType: hard + "fresh@npm:0.5.2": version: 0.5.2 resolution: "fresh@npm:0.5.2" @@ -17099,6 +17192,16 @@ fsevents@~2.1.2: languageName: node linkType: hard +"gesto@npm:^1.2.1, gesto@npm:^1.4.0": + version: 1.4.0 + resolution: "gesto@npm:1.4.0" + dependencies: + "@daybrush/utils": ^1.0.0 + "@scena/event-emitter": ^1.0.2 + checksum: 2514adc783f42e3012564618d67eae65fc6557703b559bbf6c223975137fd7956d4aea8bbc4e7d988cc1940f78f9ed1a554403c63c4053f3d113506ab84de2be + languageName: node + linkType: hard + "get-caller-file@npm:^1.0.1": version: 1.0.3 resolution: "get-caller-file@npm:1.0.3" @@ -17878,6 +17981,7 @@ fsevents@~2.1.2: monaco-promql: ^1.7.2 mousetrap: 1.6.5 mousetrap-global-bind: 1.1.0 + moveable: 0.26.0 mutationobserver-shim: 0.3.3 ngtemplate-loader: 2.1.0 nodemon: 2.0.2 @@ -17933,6 +18037,7 @@ fsevents@~2.1.2: sass-lint: 1.12.1 sass-loader: 12.1.0 search-query-parser: 1.5.4 + selecto: 1.13.0 semver: ^7.1.3 sinon: 8.1.1 slate: 0.47.8 @@ -21385,6 +21490,24 @@ fsevents@~2.1.2: languageName: node linkType: hard +"keycode@npm:^2.2.0": + version: 2.2.0 + resolution: "keycode@npm:2.2.0" + checksum: cb91c2940a892f1444a41fc08339b8831445a6b095af9103e3061ea7d4bdbfc420135dcb5d9257020e35c374468bb7d4495ea9fcea54e5760196daff3c874fa4 + languageName: node + linkType: hard + +"keycon@npm:^1.1.2": + version: 1.1.2 + resolution: "keycon@npm:1.1.2" + dependencies: + "@daybrush/utils": ^1.0.0 + "@scena/event-emitter": ^1.0.2 + keycode: ^2.2.0 + checksum: 7be74aaf8360824b4aa7bf7a401fa4bc586522ead95ea04d24857ce3320c56d6e3d9a6bb691c5a575f1381124bcbc6b1e5f22a46241d2e2af324ab7b06305d2f + languageName: node + linkType: hard + "kind-of@npm:^3.0.2, kind-of@npm:^3.0.3, kind-of@npm:^3.2.0": version: 3.2.2 resolution: "kind-of@npm:3.2.2" @@ -23187,6 +23310,16 @@ fsevents@~2.1.2: languageName: node linkType: hard +"moveable@npm:0.26.0": + version: 0.26.0 + resolution: "moveable@npm:0.26.0" + dependencies: + "@scena/event-emitter": ^1.0.3 + react-compat-moveable: ~0.14.0 + checksum: f4ee3b8b5562c60534ee7843125234fd549faf7c3be3e09c56e4ec4c12333f3d6715eed60314843bcf0ebe66585024e0dfe129f8d9848040bcbdc8be2da17de3 + languageName: node + linkType: hard + "ms@npm:2.0.0": version: 2.0.0 resolution: "ms@npm:2.0.0" @@ -24300,6 +24433,15 @@ fsevents@~2.1.2: languageName: node linkType: hard +"overlap-area@npm:^1.0.0": + version: 1.0.0 + resolution: "overlap-area@npm:1.0.0" + dependencies: + "@daybrush/utils": ^1.3.1 + checksum: 37d82ea4ea914f7e27c33cf12e93a99e07103bca4180cbfb835328d3fe33a6f31e9500c644f2638a07f478bebc7a261db6d97187c58d967d4736b60cd31230f0 + languageName: node + linkType: hard + "overlayscrollbars@npm:^1.13.1": version: 1.13.1 resolution: "overlayscrollbars@npm:1.13.1" @@ -27452,6 +27594,46 @@ fsevents@~2.1.2: languageName: node linkType: hard +"react-compat-css-styled@npm:^1.0.5": + version: 1.0.6 + resolution: "react-compat-css-styled@npm:1.0.6" + dependencies: + "@daybrush/utils": ^1.0.0 + css-styled: ^1.0.0 + framework-utils: ^0.3.4 + react-css-styled: ^1.0.2 + react-simple-compat: ^1.1.0 + peerDependencies: + framework-utils: ">=0.3.4" + checksum: cb3b9158879fa9ed2d238b1ef3fafd4609414312ad648f71fb8faefb5479ee0633c54835a5a618c4a6467bd8772be2d2d2487c5552c0d65eef8f7b6ac548d9de + languageName: node + linkType: hard + +"react-compat-moveable@npm:~0.14.0": + version: 0.14.0 + resolution: "react-compat-moveable@npm:0.14.0" + dependencies: + "@daybrush/utils": ^1.6.0 + "@scena/dragscroll": ^1.0.1 + react-compat-css-styled: ^1.0.5 + react-moveable: ~0.29.0 + react-simple-compat: ^1.2.1 + checksum: 3aa4dbbeaa5c12d974d84c23f0567476cd14346ccc6b06f9b8062cda4c40bbd1f335bf21f5829ebdacf7e9f27803e6e8ffc5f9cbed35cb008049730b27388bc5 + languageName: node + linkType: hard + +"react-css-styled@npm:^1.0.1, react-css-styled@npm:^1.0.2": + version: 1.0.2 + resolution: "react-css-styled@npm:1.0.2" + dependencies: + css-styled: ^1.0.0 + framework-utils: ^0.3.4 + peerDependencies: + framework-utils: ">=0.3.4" + checksum: a05f3fd46eff98af574f134e58bad0b2b5cafc404878d7142a5870858050169b5148368c9b273f74e25da2f09b79341aecff5a2b7114220a7f5b518755cbe7a1 + languageName: node + linkType: hard + "react-custom-scrollbars-2@npm:4.4.0": version: 4.4.0 resolution: "react-custom-scrollbars-2@npm:4.4.0" @@ -27797,6 +27979,24 @@ fsevents@~2.1.2: languageName: node linkType: hard +"react-moveable@npm:~0.29.0": + version: 0.29.0 + resolution: "react-moveable@npm:0.29.0" + dependencies: + "@daybrush/utils": ^1.6.0 + "@egjs/agent": ^2.2.1 + "@egjs/children-differ": ^1.0.1 + "@scena/dragscroll": ^1.0.1 + "@scena/matrix": ^1.1.1 + css-to-mat: ^1.0.3 + framework-utils: ^1.1.0 + gesto: ^1.2.1 + overlap-area: ^1.0.0 + react-css-styled: ^1.0.1 + checksum: 00c09100017873826f47dab061181529bfa753341cec8e48c809931c5c6d1bbbc6eebf3186f4d7055ad3cf180f93f042b4dc868903984879afeec307af1050ae + languageName: node + linkType: hard + "react-popper-tooltip@npm:^3.1.1": version: 3.1.1 resolution: "react-popper-tooltip@npm:3.1.1" @@ -27989,6 +28189,16 @@ fsevents@~2.1.2: languageName: node linkType: hard +"react-simple-compat@npm:^1.1.0, react-simple-compat@npm:^1.2.1": + version: 1.2.1 + resolution: "react-simple-compat@npm:1.2.1" + dependencies: + "@daybrush/utils": ^1.0.0 + "@egjs/list-differ": ^1.0.0 + checksum: 07a47e750fd0cef65bbdc23007ca7fe1c61283dbd04219a0e46a655f9cb58c13165f1d384c9d4e56efee3a44df111bfb89bf966e1ef8842610e22578359b70c2 + languageName: node + linkType: hard + "react-sizeme@npm:^3.0.1": version: 3.0.2 resolution: "react-sizeme@npm:3.0.2" @@ -29693,6 +29903,24 @@ fsevents@~2.1.2: languageName: node linkType: hard +"selecto@npm:1.13.0": + version: 1.13.0 + resolution: "selecto@npm:1.13.0" + dependencies: + "@daybrush/utils": ^1.4.0 + "@egjs/children-differ": ^1.0.1 + "@scena/dragscroll": ^1.0.2 + "@scena/event-emitter": ^1.0.5 + css-styled: ^1.0.0 + css-to-mat: ^1.0.3 + framework-utils: ^1.1.0 + gesto: ^1.4.0 + keycon: ^1.1.2 + overlap-area: ^1.0.0 + checksum: af8bbf085b6979b48e4bd1282baf31e339f49d719e190e89504cdf4db5e54f62a90ddd766c4635576d428c80857b849888ee0c1c227ff775e9293484153c39d0 + languageName: node + linkType: hard + "selfsigned@npm:^1.10.11": version: 1.10.11 resolution: "selfsigned@npm:1.10.11" @@ -30833,6 +31061,13 @@ fsevents@~2.1.2: languageName: node linkType: hard +"string-hash@npm:^1.1.3": + version: 1.1.3 + resolution: "string-hash@npm:1.1.3" + checksum: 104b8667a5e0dc71bfcd29fee09cb88c6102e27bfb07c55f95535d90587d016731d52299380052e514266f4028a7a5172e0d9ac58e2f8f5001be61dc77c0754d + languageName: node + linkType: hard + "string-length@npm:^4.0.1": version: 4.0.2 resolution: "string-length@npm:4.0.2"