2023-04-26 01:31:45 +08:00
import { isNumber , isString } from 'lodash' ;
2023-08-17 00:32:20 +08:00
import { AppEvents , Field , getFieldDisplayName , LinkModel , PluginState , SelectableValue } from '@grafana/data' ;
2023-05-25 00:32:36 +08:00
import appEvents from 'app/core/app_events' ;
2023-11-03 01:09:07 +08:00
import { hasAlphaPanels , config } from 'app/core/config' ;
2022-11-04 01:30:12 +08:00
import {
2023-05-25 00:32:36 +08:00
defaultElementItems ,
2022-11-04 01:30:12 +08:00
advancedElementItems ,
CanvasElementItem ,
canvasElementRegistry ,
2023-05-25 00:32:36 +08:00
CanvasElementOptions ,
2023-11-03 01:09:07 +08:00
CanvasConnection ,
2023-05-25 00:32:36 +08:00
} from 'app/features/canvas' ;
import { notFoundItem } from 'app/features/canvas/elements/notFound' ;
import { ElementState } from 'app/features/canvas/runtime/element' ;
import { FrameState } from 'app/features/canvas/runtime/frame' ;
import { Scene , SelectionParams } from 'app/features/canvas/runtime/scene' ;
import { DimensionContext } from 'app/features/dimensions' ;
2022-07-12 21:31:02 +08:00
2024-03-20 00:56:10 +08:00
import { AnchorPoint , ConnectionState , LineStyle , StrokeDasharray } from './types' ;
2023-01-06 06:09:04 +08:00
2022-07-12 21:31:02 +08:00
export function doSelect ( scene : Scene , element : ElementState | FrameState ) {
try {
let selection : SelectionParams = { targets : [ ] } ;
if ( element instanceof FrameState ) {
const targetElements : HTMLDivElement [ ] = [ ] ;
targetElements . push ( element ? . div ! ) ;
selection . targets = targetElements ;
selection . frame = element ;
scene . select ( selection ) ;
} else {
scene . currentLayer = element . parent ;
selection . targets = [ element ? . div ! ] ;
scene . select ( selection ) ;
}
} catch ( error ) {
appEvents . emit ( AppEvents . alertError , [ 'Unable to select element, try selecting element in panel instead' ] ) ;
}
}
2022-08-02 04:34:05 +08:00
2022-10-24 08:39:10 +08:00
export function getElementTypes ( shouldShowAdvancedTypes : boolean | undefined , current? : string ) : RegistrySelectInfo {
if ( shouldShowAdvancedTypes ) {
return getElementTypesOptions ( [ . . . defaultElementItems , . . . advancedElementItems ] , current ) ;
}
return getElementTypesOptions ( [ . . . defaultElementItems ] , current ) ;
}
interface RegistrySelectInfo {
options : Array < SelectableValue < string > > ;
current : Array < SelectableValue < string > > ;
}
2022-11-24 00:54:57 +08:00
export function getElementTypesOptions ( items : CanvasElementItem [ ] , current : string | undefined ) : RegistrySelectInfo {
2022-10-24 08:39:10 +08:00
const selectables : RegistrySelectInfo = { options : [ ] , current : [ ] } ;
const alpha : Array < SelectableValue < string > > = [ ] ;
for ( const item of items ) {
const option : SelectableValue < string > = { label : item.name , value : item.id , description : item.description } ;
if ( item . state === PluginState . alpha ) {
if ( ! hasAlphaPanels ) {
continue ;
}
option . label = ` ${ item . name } (Alpha) ` ;
alpha . push ( option ) ;
} else {
selectables . options . push ( option ) ;
}
if ( item . id === current ) {
selectables . current . push ( option ) ;
}
}
for ( const a of alpha ) {
selectables . options . push ( a ) ;
}
return selectables ;
2022-08-02 04:34:05 +08:00
}
2022-11-04 01:30:12 +08:00
2023-01-06 06:09:04 +08:00
export function onAddItem ( sel : SelectableValue < string > , rootLayer : FrameState | undefined , anchorPoint? : AnchorPoint ) {
2022-11-04 01:30:12 +08:00
const newItem = canvasElementRegistry . getIfExists ( sel . value ) ? ? notFoundItem ;
2023-05-22 18:53:58 +08:00
const newElementOptions : CanvasElementOptions = {
. . . newItem . getNewOptions ( ) ,
type : newItem . id ,
name : '' ,
} ;
2023-01-06 06:09:04 +08:00
if ( anchorPoint ) {
newElementOptions . placement = { . . . newElementOptions . placement , top : anchorPoint.y , left : anchorPoint.x } ;
}
2022-11-04 01:30:12 +08:00
if ( newItem . defaultSize ) {
newElementOptions . placement = { . . . newElementOptions . placement , . . . newItem . defaultSize } ;
}
if ( rootLayer ) {
const newElement = new ElementState ( newItem , newElementOptions , rootLayer ) ;
newElement . updateData ( rootLayer . scene . context ) ;
rootLayer . elements . push ( newElement ) ;
rootLayer . scene . save ( ) ;
rootLayer . reinitializeMoveable ( ) ;
2023-01-10 23:26:53 +08:00
setTimeout ( ( ) = > doSelect ( rootLayer . scene , newElement ) ) ;
2022-11-04 01:30:12 +08:00
}
}
2023-01-21 00:26:51 +08:00
2024-03-14 01:30:12 +08:00
/ *
* Provided a given field add any matching data links
* Mutates the links object in place which is then returned by the ` getDataLinks ` function downstream
* /
const addDataLinkForField = (
field : Field < unknown > ,
data : string | undefined ,
linkLookup : Set < string > ,
links : Array < LinkModel < Field > >
) : void = > {
if ( field ? . getLinks ) {
const disp = field . display ? field . display ( data ) : { text : ` ${ data } ` , numeric : + data ! } ;
field . getLinks ( { calculatedValue : disp } ) . forEach ( ( link ) = > {
const key = ` ${ link . title } / ${ link . href } ` ;
if ( ! linkLookup . has ( key ) ) {
links . push ( link ) ;
linkLookup . add ( key ) ;
}
} ) ;
}
} ;
// TODO: This could be refactored a fair amount, ideally the element specific config code should be owned by each element and not in this shared util file
export function getDataLinks (
dimensionContext : DimensionContext ,
elementOptions : CanvasElementOptions ,
data : string | undefined
) : LinkModel [ ] {
const panelData = dimensionContext . getPanelData ( ) ;
2023-01-21 00:26:51 +08:00
const frames = panelData ? . series ;
const links : Array < LinkModel < Field > > = [ ] ;
const linkLookup = new Set < string > ( ) ;
2024-03-14 01:30:12 +08:00
const elementConfig = elementOptions . config ;
2023-01-21 00:26:51 +08:00
frames ? . forEach ( ( frame ) = > {
const visibleFields = frame . fields . filter ( ( field ) = > ! Boolean ( field . config . custom ? . hideFrom ? . tooltip ) ) ;
2024-03-14 01:30:12 +08:00
// Text config
const isTextTiedToFieldData =
elementConfig . text ? . field &&
visibleFields . some ( ( field ) = > getFieldDisplayName ( field , frame ) === elementConfig . text ? . field ) ;
const isTextColorTiedToFieldData =
elementConfig . color ? . field &&
visibleFields . some ( ( field ) = > getFieldDisplayName ( field , frame ) === elementConfig . color ? . field ) ;
// General element config
const isElementBackgroundColorTiedToFieldData =
elementOptions ? . background ? . color ? . field &&
visibleFields . some ( ( field ) = > getFieldDisplayName ( field , frame ) === elementOptions ? . background ? . color ? . field ) ;
const isElementBackgroundImageTiedToFieldData =
elementOptions ? . background ? . image ? . field &&
visibleFields . some ( ( field ) = > getFieldDisplayName ( field , frame ) === elementOptions ? . background ? . image ? . field ) ;
const isElementBorderColorTiedToFieldData =
elementOptions ? . border ? . color ? . field &&
visibleFields . some ( ( field ) = > getFieldDisplayName ( field , frame ) === elementOptions ? . border ? . color ? . field ) ;
// Icon config
const isIconSVGTiedToFieldData =
elementConfig . path ? . field &&
visibleFields . some ( ( field ) = > getFieldDisplayName ( field , frame ) === elementConfig . path ? . field ) ;
const isIconColorTiedToFieldData =
elementConfig . fill ? . field &&
visibleFields . some ( ( field ) = > getFieldDisplayName ( field , frame ) === elementConfig . fill ? . field ) ;
// Wind turbine config (maybe remove / not support this?)
const isWindTurbineRPMTiedToFieldData =
elementConfig . rpm ? . field &&
visibleFields . some ( ( field ) = > getFieldDisplayName ( field , frame ) === elementConfig . rpm ? . field ) ;
// Server config
const isServerBlinkRateTiedToFieldData =
elementConfig . blinkRate ? . field &&
visibleFields . some ( ( field ) = > getFieldDisplayName ( field , frame ) === elementConfig . blinkRate ? . field ) ;
const isServerStatusColorTiedToFieldData =
elementConfig . statusColor ? . field &&
visibleFields . some ( ( field ) = > getFieldDisplayName ( field , frame ) === elementConfig . statusColor ? . field ) ;
const isServerBulbColorTiedToFieldData =
elementConfig . bulbColor ? . field &&
visibleFields . some ( ( field ) = > getFieldDisplayName ( field , frame ) === elementConfig . bulbColor ? . field ) ;
if ( isTextTiedToFieldData ) {
const field = visibleFields . filter ( ( field ) = > getFieldDisplayName ( field , frame ) === elementConfig . text ? . field ) [ 0 ] ;
addDataLinkForField ( field , data , linkLookup , links ) ;
}
if ( isTextColorTiedToFieldData ) {
const field = visibleFields . filter (
( field ) = > getFieldDisplayName ( field , frame ) === elementConfig . color ? . field
) [ 0 ] ;
addDataLinkForField ( field , data , linkLookup , links ) ;
}
if ( isElementBackgroundColorTiedToFieldData ) {
const field = visibleFields . filter (
( field ) = > getFieldDisplayName ( field , frame ) === elementOptions ? . background ? . color ? . field
) [ 0 ] ;
addDataLinkForField ( field , data , linkLookup , links ) ;
}
if ( isElementBackgroundImageTiedToFieldData ) {
const field = visibleFields . filter (
( field ) = > getFieldDisplayName ( field , frame ) === elementOptions ? . background ? . image ? . field
) [ 0 ] ;
addDataLinkForField ( field , data , linkLookup , links ) ;
}
if ( isElementBorderColorTiedToFieldData ) {
const field = visibleFields . filter (
( field ) = > getFieldDisplayName ( field , frame ) === elementOptions ? . border ? . color ? . field
) [ 0 ] ;
addDataLinkForField ( field , data , linkLookup , links ) ;
}
if ( isIconSVGTiedToFieldData ) {
const field = visibleFields . filter ( ( field ) = > getFieldDisplayName ( field , frame ) === elementConfig . path ? . field ) [ 0 ] ;
addDataLinkForField ( field , data , linkLookup , links ) ;
}
if ( isIconColorTiedToFieldData ) {
const field = visibleFields . filter ( ( field ) = > getFieldDisplayName ( field , frame ) === elementConfig . fill ? . field ) [ 0 ] ;
addDataLinkForField ( field , data , linkLookup , links ) ;
}
if ( isWindTurbineRPMTiedToFieldData ) {
const field = visibleFields . filter ( ( field ) = > getFieldDisplayName ( field , frame ) === elementConfig . rpm ? . field ) [ 0 ] ;
addDataLinkForField ( field , data , linkLookup , links ) ;
}
if ( isServerBlinkRateTiedToFieldData ) {
const field = visibleFields . filter (
( field ) = > getFieldDisplayName ( field , frame ) === elementConfig . blinkRate ? . field
) [ 0 ] ;
addDataLinkForField ( field , data , linkLookup , links ) ;
}
if ( isServerStatusColorTiedToFieldData ) {
const field = visibleFields . filter (
( field ) = > getFieldDisplayName ( field , frame ) === elementConfig . statusColor ? . field
) [ 0 ] ;
addDataLinkForField ( field , data , linkLookup , links ) ;
}
if ( isServerBulbColorTiedToFieldData ) {
const field = visibleFields . filter (
( field ) = > getFieldDisplayName ( field , frame ) === elementConfig . bulbColor ? . field
) [ 0 ] ;
addDataLinkForField ( field , data , linkLookup , links ) ;
2023-01-21 00:26:51 +08:00
}
} ) ;
return links ;
}
2023-01-31 05:50:10 +08:00
export function isConnectionSource ( element : ElementState ) {
return element . options . connections && element . options . connections . length > 0 ;
}
export function isConnectionTarget ( element : ElementState , sceneByName : Map < string , ElementState > ) {
const connections = getConnections ( sceneByName ) ;
return connections . some ( ( connection ) = > connection . target === element ) ;
}
export function getConnections ( sceneByName : Map < string , ElementState > ) {
2023-04-26 01:31:45 +08:00
const connections : ConnectionState [ ] = [ ] ;
2023-01-31 05:50:10 +08:00
for ( let v of sceneByName . values ( ) ) {
if ( v . options . connections ) {
2023-04-26 01:31:45 +08:00
v . options . connections . forEach ( ( c , index ) = > {
// @TODO Remove after v10.x
if ( isString ( c . color ) ) {
c . color = { fixed : c.color } ;
}
if ( isNumber ( c . size ) ) {
c . size = { fixed : 2 , min : 1 , max : 10 } ;
}
2023-01-31 05:50:10 +08:00
const target = c . targetName ? sceneByName . get ( c . targetName ) : v . parent ;
if ( target ) {
connections . push ( {
2023-04-26 01:31:45 +08:00
index ,
2023-01-31 05:50:10 +08:00
source : v ,
target ,
info : c ,
2024-03-16 00:35:07 +08:00
vertices : c.vertices ? ? undefined ,
2023-01-31 05:50:10 +08:00
} ) ;
}
2023-04-26 01:31:45 +08:00
} ) ;
2023-01-31 05:50:10 +08:00
}
}
return connections ;
}
2023-01-31 12:36:57 +08:00
export function getConnectionsByTarget ( element : ElementState , scene : Scene ) {
2023-04-26 01:31:45 +08:00
return scene . connections . state . filter ( ( connection ) = > connection . target === element ) ;
2023-01-31 12:36:57 +08:00
}
2023-03-28 00:51:56 +08:00
export function updateConnectionsForSource ( element : ElementState , scene : Scene ) {
const targetConnections = getConnectionsByTarget ( element , scene ) ;
targetConnections . forEach ( ( connection ) = > {
const sourceConnections = connection . source . options . connections ? . splice ( 0 ) ? ? [ ] ;
const connections = sourceConnections . filter ( ( con ) = > con . targetName !== element . getName ( ) ) ;
connection . source . onChange ( { . . . connection . source . options , connections } ) ;
} ) ;
}
2023-11-03 01:09:07 +08:00
export const calculateCoordinates = (
sourceRect : DOMRect ,
parentRect : DOMRect ,
info : CanvasConnection ,
2024-01-03 03:52:21 +08:00
target : ElementState ,
transformScale : number
2023-11-03 01:09:07 +08:00
) = > {
const sourceHorizontalCenter = sourceRect . left - parentRect . left + sourceRect . width / 2 ;
const sourceVerticalCenter = sourceRect . top - parentRect . top + sourceRect . height / 2 ;
// Convert from connection coords to DOM coords
2024-01-03 03:52:21 +08:00
const x1 = ( sourceHorizontalCenter + ( info . source . x * sourceRect . width ) / 2 ) / transformScale ;
const y1 = ( sourceVerticalCenter - ( info . source . y * sourceRect . height ) / 2 ) / transformScale ;
let x2 : number ;
let y2 : number ;
const targetRect = target . div ? . getBoundingClientRect ( ) ;
if ( info . targetName && targetRect ) {
const targetHorizontalCenter = targetRect . left - parentRect . left + targetRect . width / 2 ;
const targetVerticalCenter = targetRect . top - parentRect . top + targetRect . height / 2 ;
x2 = targetHorizontalCenter + ( info . target . x * targetRect . width ) / 2 ;
y2 = targetVerticalCenter - ( info . target . y * targetRect . height ) / 2 ;
2023-11-03 01:09:07 +08:00
} else {
const parentHorizontalCenter = parentRect . width / 2 ;
const parentVerticalCenter = parentRect . height / 2 ;
x2 = parentHorizontalCenter + ( info . target . x * parentRect . width ) / 2 ;
y2 = parentVerticalCenter - ( info . target . y * parentRect . height ) / 2 ;
}
2024-01-03 03:52:21 +08:00
x2 /= transformScale ;
y2 /= transformScale ;
2023-11-03 01:09:07 +08:00
return { x1 , y1 , x2 , y2 } ;
} ;
2024-03-16 00:35:07 +08:00
export const calculateMidpoint = ( x1 : number , y1 : number , x2 : number , y2 : number ) = > {
return { x : ( x1 + x2 ) / 2 , y : ( y1 + y2 ) / 2 } ;
} ;
export const calculateAbsoluteCoords = (
x1 : number ,
y1 : number ,
x2 : number ,
y2 : number ,
valueX : number ,
valueY : number
) = > {
return { x : valueX * ( x2 - x1 ) + x1 , y : valueY * ( y2 - y1 ) + y1 } ;
} ;
2024-03-21 14:49:28 +08:00
export const calculateAngle = ( x1 : number , y1 : number , x2 : number , y2 : number ) = > {
return ( Math . atan2 ( y2 - y1 , x2 - x1 ) * 180 ) / Math . PI ;
} ;
2023-11-03 01:09:07 +08:00
// @TODO revisit, currently returning last row index for field
export const getRowIndex = ( fieldName : string | undefined , scene : Scene ) = > {
if ( fieldName ) {
const series = scene . context . getPanelData ( ) ? . series [ 0 ] ;
const field = series ? . fields . find ( ( f ) = > ( f . name = fieldName ) ) ;
const data = field ? . values ;
return data ? data . length - 1 : 0 ;
}
return 0 ;
} ;
export const getConnectionStyles = ( info : CanvasConnection , scene : Scene , defaultArrowSize : number ) = > {
const defaultArrowColor = config . theme2 . colors . text . primary ;
const lastRowIndex = getRowIndex ( info . size ? . field , scene ) ;
const strokeColor = info . color ? scene . context . getColor ( info . color ) . value ( ) : defaultArrowColor ;
const strokeWidth = info . size ? scene . context . getScale ( info . size ) . get ( lastRowIndex ) : defaultArrowSize ;
2024-03-20 00:56:10 +08:00
const lineStyle = info . lineStyle === LineStyle . Dashed ? StrokeDasharray.Dashed : StrokeDasharray.Solid ;
return { strokeColor , strokeWidth , lineStyle } ;
2023-11-03 01:09:07 +08:00
} ;
2024-01-03 03:52:21 +08:00
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 ;
} ;