From 56921b205d5073c4ef10ba665964b086d1909c3c Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Thu, 16 Dec 2021 11:05:58 -0800 Subject: [PATCH] Canvas add drone elements (#43122) --- .../features/canvas/elements/droneFront.tsx | 113 ++++++++++++ .../features/canvas/elements/droneSide.tsx | 112 ++++++++++++ .../app/features/canvas/elements/droneTop.tsx | 173 ++++++++++++++++++ public/app/features/canvas/registry.ts | 6 + public/app/features/canvas/runtime/scene.tsx | 3 + public/app/features/dimensions/context.ts | 2 + public/app/features/dimensions/utils.ts | 17 ++ public/app/plugins/panel/icon/IconPanel.tsx | 3 + public/img/icons/iot/drone.svg | 29 +++ 9 files changed, 458 insertions(+) create mode 100644 public/app/features/canvas/elements/droneFront.tsx create mode 100644 public/app/features/canvas/elements/droneSide.tsx create mode 100644 public/app/features/canvas/elements/droneTop.tsx create mode 100644 public/img/icons/iot/drone.svg diff --git a/public/app/features/canvas/elements/droneFront.tsx b/public/app/features/canvas/elements/droneFront.tsx new file mode 100644 index 00000000000..bd5a1ef9348 --- /dev/null +++ b/public/app/features/canvas/elements/droneFront.tsx @@ -0,0 +1,113 @@ +import React, { FC } from 'react'; +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { useStyles2 } from '@grafana/ui'; + +import { ScalarDimensionEditor } from 'app/features/dimensions/editors'; +import { CanvasElementItem, CanvasElementProps } from '../element'; +import { DimensionContext, ScalarDimensionConfig } from 'app/features/dimensions'; + +interface DroneFrontData { + rollAngle?: number; +} + +interface DroneFrontConfig { + rollAngle?: ScalarDimensionConfig; +} + +const DroneFrontDisplay: FC> = (props) => { + const styles = useStyles2(getStyles); + + const { data } = props; + + const droneFrontTransformStyle = `rotate(${data?.rollAngle ? data.rollAngle : 0}deg)`; + + return ( + + + + + + + + + + + + + + + + + + + + + ); +}; + +export const droneFrontItem: CanvasElementItem = { + id: 'droneFront', + name: 'Drone Front', + description: 'Drone front', + + display: DroneFrontDisplay, + + defaultSize: { + width: 100, + height: 100, + }, + + getNewOptions: (options) => ({ + ...options, + }), + + // Called when data changes + prepareData: (ctx: DimensionContext, cfg: DroneFrontConfig) => { + const data: DroneFrontData = { + rollAngle: cfg?.rollAngle ? ctx.getScalar(cfg.rollAngle).value() : 0, + }; + + return data; + }, + + registerOptionsUI: (builder) => { + const category = ['Drone Front']; + builder.addCustomEditor({ + category, + id: 'rollAngle', + path: 'config.rollAngle', + name: 'Roll Angle', + editor: ScalarDimensionEditor, + }); + }, +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + droneFront: css` + transition: transform 0.4s; + `, +}); diff --git a/public/app/features/canvas/elements/droneSide.tsx b/public/app/features/canvas/elements/droneSide.tsx new file mode 100644 index 00000000000..a08577830bd --- /dev/null +++ b/public/app/features/canvas/elements/droneSide.tsx @@ -0,0 +1,112 @@ +import React, { FC } from 'react'; +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { useStyles2 } from '@grafana/ui'; + +import { ScalarDimensionEditor } from 'app/features/dimensions/editors'; +import { CanvasElementItem, CanvasElementProps } from '../element'; +import { DimensionContext, ScalarDimensionConfig } from 'app/features/dimensions'; + +interface DroneSideData { + pitchAngle?: number; +} + +interface DroneSideConfig { + pitchAngle?: ScalarDimensionConfig; +} + +const DroneSideDisplay: FC> = (props) => { + const styles = useStyles2(getStyles); + + const { data } = props; + + const droneSidePitchTransformStyle = `rotate(${data?.pitchAngle ? data.pitchAngle : 0}deg)`; + + return ( + + + + + + + + + + + + + + + + + + + + ); +}; + +export const droneSideItem: CanvasElementItem = { + id: 'droneSide', + name: 'Drone Side', + description: 'Drone Side', + + display: DroneSideDisplay, + + defaultSize: { + width: 100, + height: 100, + }, + + getNewOptions: (options) => ({ + ...options, + }), + + // Called when data changes + prepareData: (ctx: DimensionContext, cfg: DroneSideConfig) => { + const data: DroneSideData = { + pitchAngle: cfg?.pitchAngle ? ctx.getScalar(cfg.pitchAngle).value() : 0, + }; + + return data; + }, + + registerOptionsUI: (builder) => { + const category = ['Drone Side']; + builder.addCustomEditor({ + category, + id: 'pitchAngle', + path: 'config.pitchAngle', + name: 'Pitch Angle', + editor: ScalarDimensionEditor, + }); + }, +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + droneSide: css` + transition: transform 0.4s; + `, +}); diff --git a/public/app/features/canvas/elements/droneTop.tsx b/public/app/features/canvas/elements/droneTop.tsx new file mode 100644 index 00000000000..5d8eb7f6214 --- /dev/null +++ b/public/app/features/canvas/elements/droneTop.tsx @@ -0,0 +1,173 @@ +import React, { FC } from 'react'; +import { css } from '@emotion/css'; +import { GrafanaTheme2 } from '@grafana/data'; +import { useStyles2 } from '@grafana/ui'; + +import { ScalarDimensionEditor } from 'app/features/dimensions/editors'; +import { CanvasElementItem, CanvasElementProps } from '../element'; +import { DimensionContext, ScalarDimensionConfig } from 'app/features/dimensions'; + +interface DroneTopData { + bRightRotorRPM?: number; + bLeftRotorRPM?: number; + fRightRotorRPM?: number; + fLeftRotorRPM?: number; + yawAngle?: number; +} + +interface DroneTopConfig { + bRightRotorRPM?: ScalarDimensionConfig; + bLeftRotorRPM?: ScalarDimensionConfig; + fRightRotorRPM?: ScalarDimensionConfig; + fLeftRotorRPM?: ScalarDimensionConfig; + yawAngle?: ScalarDimensionConfig; +} + +const DroneTopDisplay: FC> = (props) => { + const styles = useStyles2(getStyles); + + const { data } = props; + + const fRightRotorAnimation = `spin ${data?.fRightRotorRPM ? 60 / Math.abs(data.fRightRotorRPM) : 0}s linear infinite`; + + const fLeftRotorAnimation = `spin ${data?.fLeftRotorRPM ? 60 / Math.abs(data.fLeftRotorRPM) : 0}s linear infinite`; + + const bRightRotorAnimation = `spin ${data?.bRightRotorRPM ? 60 / Math.abs(data.bRightRotorRPM) : 0}s linear infinite`; + + const bLeftRotorAnimation = `spin ${data?.bLeftRotorRPM ? 60 / Math.abs(data.bLeftRotorRPM) : 0}s linear infinite`; + + const droneTopTransformStyle = `rotate(${data?.yawAngle ? data.yawAngle : 0}deg)`; + + return ( + + + + + + + + + + + ); +}; + +export const droneTopItem: CanvasElementItem = { + id: 'droneTop', + name: 'Drone Top', + description: 'Drone top', + + display: DroneTopDisplay, + + defaultSize: { + width: 100, + height: 100, + }, + + getNewOptions: (options) => ({ + ...options, + }), + + // Called when data changes + prepareData: (ctx: DimensionContext, cfg: DroneTopConfig) => { + const data: DroneTopData = { + bRightRotorRPM: cfg?.bRightRotorRPM ? ctx.getScalar(cfg.bRightRotorRPM).value() : 0, + bLeftRotorRPM: cfg?.bLeftRotorRPM ? ctx.getScalar(cfg.bLeftRotorRPM).value() : 0, + fRightRotorRPM: cfg?.fRightRotorRPM ? ctx.getScalar(cfg.fRightRotorRPM).value() : 0, + fLeftRotorRPM: cfg?.fLeftRotorRPM ? ctx.getScalar(cfg.fLeftRotorRPM).value() : 0, + yawAngle: cfg?.yawAngle ? ctx.getScalar(cfg.yawAngle).value() : 0, + }; + + return data; + }, + + registerOptionsUI: (builder) => { + const category = ['Drone Top']; + builder + .addCustomEditor({ + category, + id: 'yawAngle', + path: 'config.yawAngle', + name: 'Yaw Angle', + editor: ScalarDimensionEditor, + }) + .addCustomEditor({ + category, + id: 'fRightRotorRPM', + path: 'config.fRightRotorRPM', + name: 'Front Right Rotor RPM', + editor: ScalarDimensionEditor, + }) + .addCustomEditor({ + category, + id: 'fLeftRotorRPM', + path: 'config.fLeftRotorRPM', + name: 'Front Left Rotor RPM', + editor: ScalarDimensionEditor, + }) + .addCustomEditor({ + category, + id: 'bRightRotorRPM', + path: 'config.bRightRotorRPM', + name: 'Back Right Rotor RPM', + editor: ScalarDimensionEditor, + }) + .addCustomEditor({ + category, + id: 'bLeftRotorRPM', + path: 'config.bLeftRotorRPM', + name: 'Back Left Rotor RPM', + editor: ScalarDimensionEditor, + }); + }, +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + propeller: css` + transform-origin: 50% 50%; + transform-box: fill-box; + display: block; + @keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + `, + propellerCW: css` + animation-direction: normal; + `, + propellerCCW: css` + animation-direction: reverse; + `, +}); diff --git a/public/app/features/canvas/registry.ts b/public/app/features/canvas/registry.ts index cc718db565b..90084152048 100644 --- a/public/app/features/canvas/registry.ts +++ b/public/app/features/canvas/registry.ts @@ -1,5 +1,8 @@ import { Registry } from '@grafana/data'; import { CanvasElementItem, CanvasElementOptions } from './element'; +import { droneFrontItem } from './elements/droneFront'; +import { droneSideItem } from './elements/droneSide'; +import { droneTopItem } from './elements/droneTop'; import { iconItem } from './elements/icon'; import { textBoxItem } from './elements/textBox'; @@ -12,4 +15,7 @@ export const DEFAULT_CANVAS_ELEMENT_CONFIG: CanvasElementOptions = { export const canvasElementRegistry = new Registry(() => [ iconItem, // default for now textBoxItem, + droneTopItem, + droneFrontItem, + droneSideItem, ]); diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx index 852e218bfc3..da2ad559db8 100644 --- a/public/app/features/canvas/runtime/scene.tsx +++ b/public/app/features/canvas/runtime/scene.tsx @@ -15,12 +15,14 @@ import { ScaleDimensionConfig, TextDimensionConfig, DimensionContext, + ScalarDimensionConfig, } from 'app/features/dimensions'; import { getColorDimensionFromData, getScaleDimensionFromData, getResourceDimensionFromData, getTextDimensionFromData, + getScalarDimensionFromData, } from 'app/features/dimensions/utils'; import { ElementState } from './element'; import { RootElement } from './root'; @@ -96,6 +98,7 @@ export class Scene { context: DimensionContext = { getColor: (color: ColorDimensionConfig) => getColorDimensionFromData(this.data, color), getScale: (scale: ScaleDimensionConfig) => getScaleDimensionFromData(this.data, scale), + getScalar: (scalar: ScalarDimensionConfig) => getScalarDimensionFromData(this.data, scalar), getText: (text: TextDimensionConfig) => getTextDimensionFromData(this.data, text), getResource: (res: ResourceDimensionConfig) => getResourceDimensionFromData(this.data, res), }; diff --git a/public/app/features/dimensions/context.ts b/public/app/features/dimensions/context.ts index f6a88777ccd..d9415c2809d 100644 --- a/public/app/features/dimensions/context.ts +++ b/public/app/features/dimensions/context.ts @@ -2,6 +2,7 @@ import { ColorDimensionConfig, DimensionSupplier, ResourceDimensionConfig, + ScalarDimensionConfig, ScaleDimensionConfig, TextDimensionConfig, } from './types'; @@ -9,6 +10,7 @@ import { export interface DimensionContext { getColor(color: ColorDimensionConfig): DimensionSupplier; getScale(scale: ScaleDimensionConfig): DimensionSupplier; + getScalar(scalar: ScalarDimensionConfig): DimensionSupplier; getText(text: TextDimensionConfig): DimensionSupplier; getResource(resource: ResourceDimensionConfig): DimensionSupplier; } diff --git a/public/app/features/dimensions/utils.ts b/public/app/features/dimensions/utils.ts index 6859781be1d..19bc7342d64 100644 --- a/public/app/features/dimensions/utils.ts +++ b/public/app/features/dimensions/utils.ts @@ -9,8 +9,10 @@ import { ResourceDimensionConfig, ScaleDimensionConfig, TextDimensionConfig, + ScalarDimensionConfig, } from 'app/features/dimensions'; import { config } from '@grafana/runtime'; +import { getScalarDimension } from './scalar'; export function getColorDimensionFromData( data: PanelData | undefined, @@ -42,6 +44,21 @@ export function getScaleDimensionFromData( return getScaledDimension(undefined, cfg); } +export function getScalarDimensionFromData( + data: PanelData | undefined, + cfg: ScalarDimensionConfig +): DimensionSupplier { + if (data?.series && cfg.field) { + for (const frame of data.series) { + const d = getScalarDimension(frame, cfg); + if (!d.isAssumed || data.series.length === 1) { + return d; + } + } + } + return getScalarDimension(undefined, cfg); +} + export function getResourceDimensionFromData( data: PanelData | undefined, cfg: ResourceDimensionConfig diff --git a/public/app/plugins/panel/icon/IconPanel.tsx b/public/app/plugins/panel/icon/IconPanel.tsx index 896a99e65e8..41f244db6b8 100644 --- a/public/app/plugins/panel/icon/IconPanel.tsx +++ b/public/app/plugins/panel/icon/IconPanel.tsx @@ -8,9 +8,11 @@ import { DimensionContext, getColorDimensionFromData, getResourceDimensionFromData, + getScalarDimensionFromData, getScaleDimensionFromData, getTextDimensionFromData, ResourceDimensionConfig, + ScalarDimensionConfig, ScaleDimensionConfig, TextDimensionConfig, } from 'app/features/dimensions'; @@ -50,6 +52,7 @@ export class IconPanel extends Component { dims: DimensionContext = { getColor: (color: ColorDimensionConfig) => getColorDimensionFromData(this.props.data, color), getScale: (scale: ScaleDimensionConfig) => getScaleDimensionFromData(this.props.data, scale), + getScalar: (scalar: ScalarDimensionConfig) => getScalarDimensionFromData(this.props.data, scalar), getText: (text: TextDimensionConfig) => getTextDimensionFromData(this.props.data, text), getResource: (res: ResourceDimensionConfig) => getResourceDimensionFromData(this.props.data, res), }; diff --git a/public/img/icons/iot/drone.svg b/public/img/icons/iot/drone.svg new file mode 100644 index 00000000000..d2663405b9a --- /dev/null +++ b/public/img/icons/iot/drone.svg @@ -0,0 +1,29 @@ + + + + + + + + + + \ No newline at end of file