Canvas: Field driven layout
This commit is contained in:
+13
@@ -105,6 +105,19 @@ export interface TextDimensionConfig extends BaseDimensionConfig {
|
||||
mode: TextDimensionMode;
|
||||
}
|
||||
|
||||
export enum PositionDimensionMode {
|
||||
Field = 'field',
|
||||
Fixed = 'fixed',
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple position/coordinate dimension - just fixed value or field value, no scaling/clamping
|
||||
*/
|
||||
export interface PositionDimensionConfig extends BaseDimensionConfig {
|
||||
fixed?: number;
|
||||
mode: PositionDimensionMode;
|
||||
}
|
||||
|
||||
export enum ResourceDimensionMode {
|
||||
Field = 'field',
|
||||
Fixed = 'fixed',
|
||||
|
||||
@@ -38,6 +38,15 @@ TextDimensionConfig: {
|
||||
fixed?: string
|
||||
}@cuetsy(kind="interface")
|
||||
|
||||
PositionDimensionMode: "fixed" | "field" @cuetsy(kind="enum")
|
||||
|
||||
// Simple position/coordinate dimension - just fixed value or field value, no scaling/clamping
|
||||
PositionDimensionConfig: {
|
||||
BaseDimensionConfig
|
||||
mode: PositionDimensionMode
|
||||
fixed?: number
|
||||
}@cuetsy(kind="interface")
|
||||
|
||||
ResourceDimensionMode: "fixed" | "field" | "mapping" @cuetsy(kind="enum")
|
||||
|
||||
// Links to a resource (image/svg path)
|
||||
|
||||
+7
-7
@@ -34,13 +34,13 @@ export interface Constraint {
|
||||
}
|
||||
|
||||
export interface Placement {
|
||||
bottom?: number;
|
||||
height?: number;
|
||||
left?: number;
|
||||
right?: number;
|
||||
rotation?: number;
|
||||
top?: number;
|
||||
width?: number;
|
||||
bottom?: ui.PositionDimensionConfig;
|
||||
height?: ui.PositionDimensionConfig;
|
||||
left?: ui.PositionDimensionConfig;
|
||||
right?: ui.PositionDimensionConfig;
|
||||
rotation?: ui.ScalarDimensionConfig;
|
||||
top?: ui.PositionDimensionConfig;
|
||||
width?: ui.PositionDimensionConfig;
|
||||
}
|
||||
|
||||
export enum BackgroundImageSize {
|
||||
|
||||
@@ -29,6 +29,10 @@ export interface ScalarDimensionConfig extends BaseDimensionConfig<number>, Omit
|
||||
|
||||
export interface TextDimensionConfig extends BaseDimensionConfig<string>, Omit<raw.TextDimensionConfig, 'fixed'> {}
|
||||
|
||||
export interface PositionDimensionConfig
|
||||
extends BaseDimensionConfig<number>,
|
||||
Omit<raw.PositionDimensionConfig, 'fixed'> {}
|
||||
|
||||
export interface ColorDimensionConfig extends BaseDimensionConfig<string>, Omit<raw.ColorDimensionConfig, 'fixed'> {}
|
||||
|
||||
export interface ColorDimensionConfig extends BaseDimensionConfig<string>, Omit<raw.ColorDimensionConfig, 'fixed'> {}
|
||||
|
||||
@@ -81,6 +81,12 @@ export interface CanvasElementProps<TConfig = unknown, TData = unknown> {
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
/** Simple numeric size for element defaults - not persisted, just for initial sizing */
|
||||
export interface DefaultElementSize {
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Canvas item builder
|
||||
*
|
||||
@@ -89,7 +95,7 @@ export interface CanvasElementProps<TConfig = unknown, TData = unknown> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export interface CanvasElementItem<TConfig = any, TData = any> extends RegistryItem {
|
||||
/** The default width/height to use when adding */
|
||||
defaultSize?: Placement;
|
||||
defaultSize?: DefaultElementSize;
|
||||
|
||||
prepareData?: (dimensionContext: DimensionContext, elementOptions: CanvasElementOptions<TConfig>) => TData;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2, PluginState } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { TextDimensionMode } from '@grafana/schema';
|
||||
import { ScalarDimensionMode, PositionDimensionMode, TextDimensionMode } from '@grafana/schema';
|
||||
import { Button, Spinner, useStyles2 } from '@grafana/ui';
|
||||
import { DimensionContext } from 'app/features/dimensions/context';
|
||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||
@@ -122,11 +122,11 @@ export const buttonItem: CanvasElementItem<ButtonConfig, ButtonData> = {
|
||||
},
|
||||
},
|
||||
placement: {
|
||||
width: options?.placement?.width ?? 32,
|
||||
height: options?.placement?.height ?? 78,
|
||||
top: options?.placement?.top ?? 100,
|
||||
left: options?.placement?.left ?? 100,
|
||||
rotation: options?.placement?.rotation ?? 0,
|
||||
width: options?.placement?.width ?? { fixed: 32, mode: PositionDimensionMode.Fixed },
|
||||
height: options?.placement?.height ?? { fixed: 78, mode: PositionDimensionMode.Fixed },
|
||||
top: options?.placement?.top ?? { fixed: 100, mode: PositionDimensionMode.Fixed },
|
||||
left: options?.placement?.left ?? { fixed: 100, mode: PositionDimensionMode.Fixed },
|
||||
rotation: options?.placement?.rotation ?? { fixed: 0, min: 0, max: 360, mode: ScalarDimensionMode.Clamped },
|
||||
},
|
||||
}),
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { ScalarDimensionMode, PositionDimensionMode } from '@grafana/schema';
|
||||
import { config } from 'app/core/config';
|
||||
import { DimensionContext } from 'app/features/dimensions/context';
|
||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||
@@ -94,11 +95,11 @@ export const cloudItem: CanvasElementItem = {
|
||||
},
|
||||
},
|
||||
placement: {
|
||||
width: options?.placement?.width ?? 110,
|
||||
height: options?.placement?.height ?? 70,
|
||||
top: options?.placement?.top,
|
||||
left: options?.placement?.left,
|
||||
rotation: options?.placement?.rotation ?? 0,
|
||||
width: options?.placement?.width ?? { fixed: 110, mode: PositionDimensionMode.Fixed },
|
||||
height: options?.placement?.height ?? { fixed: 70, mode: PositionDimensionMode.Fixed },
|
||||
top: options?.placement?.top ?? { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
left: options?.placement?.left ?? { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
rotation: options?.placement?.rotation ?? { fixed: 0, min: 0, max: 360, mode: ScalarDimensionMode.Clamped },
|
||||
},
|
||||
links: options?.links ?? [],
|
||||
}),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { ScalarDimensionConfig } from '@grafana/schema';
|
||||
import { ScalarDimensionConfig, ScalarDimensionMode, PositionDimensionMode } from '@grafana/schema';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { DimensionContext } from 'app/features/dimensions/context';
|
||||
import { ScalarDimensionEditor } from 'app/features/dimensions/editors/ScalarDimensionEditor';
|
||||
@@ -89,11 +89,11 @@ export const droneFrontItem: CanvasElementItem = {
|
||||
},
|
||||
},
|
||||
placement: {
|
||||
width: options?.placement?.width ?? 100,
|
||||
height: options?.placement?.height ?? 26,
|
||||
top: options?.placement?.top,
|
||||
left: options?.placement?.left,
|
||||
rotation: options?.placement?.rotation ?? 0,
|
||||
width: options?.placement?.width ?? { fixed: 100, mode: PositionDimensionMode.Fixed },
|
||||
height: options?.placement?.height ?? { fixed: 26, mode: PositionDimensionMode.Fixed },
|
||||
top: options?.placement?.top ?? { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
left: options?.placement?.left ?? { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
rotation: options?.placement?.rotation ?? { fixed: 0, min: 0, max: 360, mode: ScalarDimensionMode.Clamped },
|
||||
},
|
||||
links: options?.links ?? [],
|
||||
}),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { ScalarDimensionConfig } from '@grafana/schema';
|
||||
import { ScalarDimensionConfig, ScalarDimensionMode, PositionDimensionMode } from '@grafana/schema';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { DimensionContext } from 'app/features/dimensions/context';
|
||||
import { ScalarDimensionEditor } from 'app/features/dimensions/editors/ScalarDimensionEditor';
|
||||
@@ -88,11 +88,11 @@ export const droneSideItem: CanvasElementItem = {
|
||||
},
|
||||
},
|
||||
placement: {
|
||||
width: options?.placement?.width ?? 100,
|
||||
height: options?.placement?.height ?? 26,
|
||||
top: options?.placement?.top,
|
||||
left: options?.placement?.left,
|
||||
rotation: options?.placement?.rotation ?? 0,
|
||||
width: options?.placement?.width ?? { fixed: 100, mode: PositionDimensionMode.Fixed },
|
||||
height: options?.placement?.height ?? { fixed: 26, mode: PositionDimensionMode.Fixed },
|
||||
top: options?.placement?.top ?? { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
left: options?.placement?.left ?? { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
rotation: options?.placement?.rotation ?? { fixed: 0, min: 0, max: 360, mode: ScalarDimensionMode.Clamped },
|
||||
},
|
||||
links: options?.links ?? [],
|
||||
}),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { ScalarDimensionMode, PositionDimensionMode } from '@grafana/schema';
|
||||
import { config } from 'app/core/config';
|
||||
import { DimensionContext } from 'app/features/dimensions/context';
|
||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||
@@ -101,11 +102,11 @@ export const ellipseItem: CanvasElementItem<CanvasElementConfig, CanvasElementDa
|
||||
},
|
||||
},
|
||||
placement: {
|
||||
width: options?.placement?.width ?? 160,
|
||||
height: options?.placement?.height ?? 138,
|
||||
top: options?.placement?.top,
|
||||
left: options?.placement?.left,
|
||||
rotation: options?.placement?.rotation ?? 0,
|
||||
width: options?.placement?.width ?? { fixed: 160, mode: PositionDimensionMode.Fixed },
|
||||
height: options?.placement?.height ?? { fixed: 138, mode: PositionDimensionMode.Fixed },
|
||||
top: options?.placement?.top ?? { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
left: options?.placement?.left ?? { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
rotation: options?.placement?.rotation ?? { fixed: 0, min: 0, max: 360, mode: ScalarDimensionMode.Clamped },
|
||||
},
|
||||
links: options?.links ?? [],
|
||||
}),
|
||||
|
||||
@@ -4,7 +4,13 @@ import { CSSProperties } from 'react';
|
||||
|
||||
import { LinkModel } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { ColorDimensionConfig, ResourceDimensionConfig, ResourceDimensionMode } from '@grafana/schema';
|
||||
import {
|
||||
ColorDimensionConfig,
|
||||
ResourceDimensionConfig,
|
||||
ResourceDimensionMode,
|
||||
ScalarDimensionMode,
|
||||
PositionDimensionMode,
|
||||
} from '@grafana/schema';
|
||||
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
|
||||
import { DimensionContext } from 'app/features/dimensions/context';
|
||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||
@@ -76,11 +82,11 @@ export const iconItem: CanvasElementItem<IconConfig, IconData> = {
|
||||
},
|
||||
},
|
||||
placement: {
|
||||
width: options?.placement?.width ?? 100,
|
||||
height: options?.placement?.height ?? 100,
|
||||
top: options?.placement?.top ?? 100,
|
||||
left: options?.placement?.left ?? 100,
|
||||
rotation: options?.placement?.rotation ?? 0,
|
||||
width: options?.placement?.width ?? { fixed: 100, mode: PositionDimensionMode.Fixed },
|
||||
height: options?.placement?.height ?? { fixed: 100, mode: PositionDimensionMode.Fixed },
|
||||
top: options?.placement?.top ?? { fixed: 100, mode: PositionDimensionMode.Fixed },
|
||||
left: options?.placement?.left ?? { fixed: 100, mode: PositionDimensionMode.Fixed },
|
||||
rotation: options?.placement?.rotation ?? { fixed: 0, min: 0, max: 360, mode: ScalarDimensionMode.Clamped },
|
||||
},
|
||||
links: options?.links ?? [],
|
||||
}),
|
||||
|
||||
@@ -5,7 +5,7 @@ import { of } from 'rxjs';
|
||||
|
||||
import { DataFrame, FieldNamePickerConfigSettings, GrafanaTheme2, StandardEditorsRegistryItem } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { TextDimensionMode } from '@grafana/schema';
|
||||
import { TextDimensionMode, ScalarDimensionMode, PositionDimensionMode } from '@grafana/schema';
|
||||
import { usePanelContext, useStyles2 } from '@grafana/ui';
|
||||
import { FieldNamePicker, frameHasName, getFrameFieldsDisplayNames } from '@grafana/ui/internal';
|
||||
import { DimensionContext } from 'app/features/dimensions/context';
|
||||
@@ -171,9 +171,9 @@ export const metricValueItem: CanvasElementItem<TextConfig, TextData> = {
|
||||
placement: {
|
||||
width: options?.placement?.width,
|
||||
height: options?.placement?.height,
|
||||
top: options?.placement?.top ?? 100,
|
||||
left: options?.placement?.left ?? 100,
|
||||
rotation: options?.placement?.rotation ?? 0,
|
||||
top: options?.placement?.top ?? { fixed: 100, mode: PositionDimensionMode.Fixed },
|
||||
left: options?.placement?.left ?? { fixed: 100, mode: PositionDimensionMode.Fixed },
|
||||
rotation: options?.placement?.rotation ?? { fixed: 0, min: 0, max: 360, mode: ScalarDimensionMode.Clamped },
|
||||
},
|
||||
links: options?.links ?? [],
|
||||
}),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { ScalarDimensionMode, PositionDimensionMode } from '@grafana/schema';
|
||||
import { config } from 'app/core/config';
|
||||
import { DimensionContext } from 'app/features/dimensions/context';
|
||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||
@@ -94,11 +95,11 @@ export const parallelogramItem: CanvasElementItem = {
|
||||
},
|
||||
},
|
||||
placement: {
|
||||
width: options?.placement?.width ?? 250,
|
||||
height: options?.placement?.height ?? 150,
|
||||
top: options?.placement?.top,
|
||||
left: options?.placement?.left,
|
||||
rotation: options?.placement?.rotation ?? 0,
|
||||
width: options?.placement?.width ?? { fixed: 250, mode: PositionDimensionMode.Fixed },
|
||||
height: options?.placement?.height ?? { fixed: 150, mode: PositionDimensionMode.Fixed },
|
||||
top: options?.placement?.top ?? { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
left: options?.placement?.left ?? { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
rotation: options?.placement?.rotation ?? { fixed: 0, min: 0, max: 360, mode: ScalarDimensionMode.Clamped },
|
||||
},
|
||||
links: options?.links ?? [],
|
||||
}),
|
||||
|
||||
@@ -2,7 +2,12 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { ColorDimensionConfig, ScalarDimensionConfig } from '@grafana/schema';
|
||||
import {
|
||||
ColorDimensionConfig,
|
||||
ScalarDimensionConfig,
|
||||
ScalarDimensionMode,
|
||||
PositionDimensionMode,
|
||||
} from '@grafana/schema';
|
||||
import config from 'app/core/config';
|
||||
import { DimensionContext } from 'app/features/dimensions/context';
|
||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||
@@ -76,11 +81,11 @@ export const serverItem: CanvasElementItem<ServerConfig, ServerData> = {
|
||||
},
|
||||
},
|
||||
placement: {
|
||||
width: options?.placement?.width ?? 100,
|
||||
height: options?.placement?.height ?? 100,
|
||||
top: options?.placement?.top,
|
||||
left: options?.placement?.left,
|
||||
rotation: options?.placement?.rotation ?? 0,
|
||||
width: options?.placement?.width ?? { fixed: 100, mode: PositionDimensionMode.Fixed },
|
||||
height: options?.placement?.height ?? { fixed: 100, mode: PositionDimensionMode.Fixed },
|
||||
top: options?.placement?.top ?? { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
left: options?.placement?.left ?? { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
rotation: options?.placement?.rotation ?? { fixed: 0, min: 0, max: 360, mode: ScalarDimensionMode.Clamped },
|
||||
},
|
||||
config: {
|
||||
type: ServerType.Single,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { of } from 'rxjs';
|
||||
|
||||
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { ScalarDimensionMode, PositionDimensionMode } from '@grafana/schema';
|
||||
import { Input, usePanelContext, useStyles2 } from '@grafana/ui';
|
||||
import { DimensionContext } from 'app/features/dimensions/context';
|
||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||
@@ -145,11 +146,11 @@ export const textItem: CanvasElementItem<TextConfig, TextData> = {
|
||||
size: 16,
|
||||
},
|
||||
placement: {
|
||||
width: options?.placement?.width ?? 100,
|
||||
height: options?.placement?.height ?? 100,
|
||||
top: options?.placement?.top,
|
||||
left: options?.placement?.left,
|
||||
rotation: options?.placement?.rotation ?? 0,
|
||||
width: options?.placement?.width ?? { fixed: 100, mode: PositionDimensionMode.Fixed },
|
||||
height: options?.placement?.height ?? { fixed: 100, mode: PositionDimensionMode.Fixed },
|
||||
top: options?.placement?.top ?? { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
left: options?.placement?.left ?? { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
rotation: options?.placement?.rotation ?? { fixed: 0, min: 0, max: 360, mode: ScalarDimensionMode.Clamped },
|
||||
},
|
||||
links: options?.links ?? [],
|
||||
}),
|
||||
|
||||
@@ -3,6 +3,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { ScalarDimensionMode, PositionDimensionMode } from '@grafana/schema';
|
||||
import { config } from 'app/core/config';
|
||||
import { DimensionContext } from 'app/features/dimensions/context';
|
||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||
@@ -95,11 +96,11 @@ export const triangleItem: CanvasElementItem = {
|
||||
},
|
||||
},
|
||||
placement: {
|
||||
width: options?.placement?.width ?? 160,
|
||||
height: options?.placement?.height ?? 138,
|
||||
top: options?.placement?.top,
|
||||
left: options?.placement?.left,
|
||||
rotation: options?.placement?.rotation ?? 0,
|
||||
width: options?.placement?.width ?? { fixed: 160, mode: PositionDimensionMode.Fixed },
|
||||
height: options?.placement?.height ?? { fixed: 138, mode: PositionDimensionMode.Fixed },
|
||||
top: options?.placement?.top ?? { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
left: options?.placement?.left ?? { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
rotation: options?.placement?.rotation ?? { fixed: 0, min: 0, max: 360, mode: ScalarDimensionMode.Clamped },
|
||||
},
|
||||
links: options?.links ?? [],
|
||||
}),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2, LinkModel } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { ScalarDimensionConfig } from '@grafana/schema';
|
||||
import { ScalarDimensionConfig, ScalarDimensionMode, PositionDimensionMode } from '@grafana/schema';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { DimensionContext } from 'app/features/dimensions/context';
|
||||
import { ScalarDimensionEditor } from 'app/features/dimensions/editors/ScalarDimensionEditor';
|
||||
@@ -85,11 +85,11 @@ export const windTurbineItem: CanvasElementItem = {
|
||||
},
|
||||
},
|
||||
placement: {
|
||||
width: options?.placement?.width ?? 100,
|
||||
height: options?.placement?.height ?? 155,
|
||||
top: options?.placement?.top,
|
||||
left: options?.placement?.left,
|
||||
rotation: options?.placement?.rotation ?? 0,
|
||||
width: options?.placement?.width ?? { fixed: 100, mode: PositionDimensionMode.Fixed },
|
||||
height: options?.placement?.height ?? { fixed: 155, mode: PositionDimensionMode.Fixed },
|
||||
top: options?.placement?.top ?? { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
left: options?.placement?.left ?? { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
rotation: options?.placement?.rotation ?? { fixed: 0, min: 0, max: 360, mode: ScalarDimensionMode.Clamped },
|
||||
},
|
||||
links: options?.links ?? [],
|
||||
}),
|
||||
|
||||
@@ -14,7 +14,12 @@ import {
|
||||
ActionType,
|
||||
} from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { TooltipDisplayMode } from '@grafana/schema';
|
||||
import {
|
||||
PositionDimensionConfig,
|
||||
PositionDimensionMode,
|
||||
ScalarDimensionMode,
|
||||
TooltipDisplayMode,
|
||||
} from '@grafana/schema';
|
||||
import { ConfirmModal, VariablesInputModal } from '@grafana/ui';
|
||||
import { LayerElement } from 'app/core/components/Layers/types';
|
||||
import { config } from 'app/core/config';
|
||||
@@ -74,6 +79,40 @@ export class ElementState implements LayerElement {
|
||||
showActionVarsModal = false;
|
||||
actionVars: ActionVariableInput = {};
|
||||
|
||||
// Cached values resolved from dimension context
|
||||
private cachedRotation = 0;
|
||||
private cachedTop = 0;
|
||||
private cachedLeft = 0;
|
||||
private cachedWidth = 100;
|
||||
private cachedHeight = 100;
|
||||
private cachedRight?: number;
|
||||
private cachedBottom?: number;
|
||||
|
||||
/** Check if a position property is field-driven (not fixed) */
|
||||
isPositionFieldDriven(prop: 'top' | 'left' | 'width' | 'height' | 'right' | 'bottom'): boolean {
|
||||
const pos = this.options.placement?.[prop];
|
||||
return pos?.mode === PositionDimensionMode.Field && !!pos?.field;
|
||||
}
|
||||
|
||||
/** Check if rotation is field-driven (has a field binding) */
|
||||
isRotationFieldDriven(): boolean {
|
||||
const rot = this.options.placement?.rotation;
|
||||
return !!rot?.field;
|
||||
}
|
||||
|
||||
/** Check if ANY position/size property is field-driven - if so, element can't be moved in editor */
|
||||
hasFieldDrivenPosition(): boolean {
|
||||
return (
|
||||
this.isPositionFieldDriven('top') ||
|
||||
this.isPositionFieldDriven('left') ||
|
||||
this.isPositionFieldDriven('width') ||
|
||||
this.isPositionFieldDriven('height') ||
|
||||
this.isPositionFieldDriven('right') ||
|
||||
this.isPositionFieldDriven('bottom') ||
|
||||
this.isRotationFieldDriven()
|
||||
);
|
||||
}
|
||||
|
||||
setActionVars = (vars: ActionVariableInput) => {
|
||||
this.actionVars = vars;
|
||||
this.forceUpdate();
|
||||
@@ -93,7 +132,13 @@ export class ElementState implements LayerElement {
|
||||
vertical: VerticalConstraint.Top,
|
||||
horizontal: HorizontalConstraint.Left,
|
||||
};
|
||||
options.placement = options.placement ?? { width: 100, height: 100, top: 0, left: 0, rotation: 0 };
|
||||
options.placement = options.placement ?? {
|
||||
width: { fixed: 100, mode: PositionDimensionMode.Fixed },
|
||||
height: { fixed: 100, mode: PositionDimensionMode.Fixed },
|
||||
top: { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
left: { fixed: 0, mode: PositionDimensionMode.Fixed },
|
||||
rotation: { fixed: 0, min: 0, max: 360, mode: ScalarDimensionMode.Clamped },
|
||||
};
|
||||
options.background = options.background ?? { color: { fixed: 'transparent' } };
|
||||
options.border = options.border ?? { color: { fixed: 'dark-green' } };
|
||||
|
||||
@@ -121,6 +166,18 @@ export class ElementState implements LayerElement {
|
||||
return this.options.name;
|
||||
}
|
||||
|
||||
/** Get the current rotation value (resolved from dimension context) */
|
||||
getRotation(): number {
|
||||
return this.cachedRotation;
|
||||
}
|
||||
|
||||
/** Set the fixed value of a PositionDimensionConfig */
|
||||
private setPositionFixed(pos: PositionDimensionConfig | undefined, value: number): void {
|
||||
if (pos) {
|
||||
pos.fixed = value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Use the configured options to update CSS style properties directly on the wrapper div **/
|
||||
applyLayoutStylesToDiv(disablePointerEvents?: boolean) {
|
||||
if (config.featureToggles.canvasPanelPanZoom) {
|
||||
@@ -134,7 +191,6 @@ export class ElementState implements LayerElement {
|
||||
|
||||
const { constraint } = this.options;
|
||||
const { vertical, horizontal } = constraint ?? {};
|
||||
const placement: Placement = this.options.placement ?? {};
|
||||
|
||||
const editingEnabled = this.getScene()?.isEditingEnabled;
|
||||
|
||||
@@ -145,95 +201,64 @@ export class ElementState implements LayerElement {
|
||||
// Minimum element size is 10x10
|
||||
minWidth: '10px',
|
||||
minHeight: '10px',
|
||||
rotate: `${placement.rotation ?? 0}deg`,
|
||||
rotate: `${this.cachedRotation}deg`,
|
||||
};
|
||||
|
||||
const translate = ['0px', '0px'];
|
||||
|
||||
switch (vertical) {
|
||||
case VerticalConstraint.Top:
|
||||
placement.top = placement.top ?? 0;
|
||||
placement.height = placement.height ?? 100;
|
||||
style.top = `${placement.top}px`;
|
||||
style.height = `${placement.height}px`;
|
||||
delete placement.bottom;
|
||||
style.top = `${this.cachedTop}px`;
|
||||
style.height = `${this.cachedHeight}px`;
|
||||
break;
|
||||
case VerticalConstraint.Bottom:
|
||||
placement.bottom = placement.bottom ?? 0;
|
||||
placement.height = placement.height ?? 100;
|
||||
style.bottom = `${placement.bottom}px`;
|
||||
style.height = `${placement.height}px`;
|
||||
delete placement.top;
|
||||
style.bottom = `${this.cachedBottom ?? 0}px`;
|
||||
style.height = `${this.cachedHeight}px`;
|
||||
break;
|
||||
case VerticalConstraint.TopBottom:
|
||||
placement.top = placement.top ?? 0;
|
||||
placement.bottom = placement.bottom ?? 0;
|
||||
style.top = `${placement.top}px`;
|
||||
style.bottom = `${placement.bottom}px`;
|
||||
delete placement.height;
|
||||
style.top = `${this.cachedTop}px`;
|
||||
style.bottom = `${this.cachedBottom ?? 0}px`;
|
||||
style.height = '';
|
||||
break;
|
||||
case VerticalConstraint.Center:
|
||||
placement.top = placement.top ?? 0;
|
||||
placement.height = placement.height ?? 100;
|
||||
translate[1] = '-50%';
|
||||
style.top = `calc(50% - ${placement.top}px)`;
|
||||
style.height = `${placement.height}px`;
|
||||
delete placement.bottom;
|
||||
style.top = `calc(50% - ${this.cachedTop}px)`;
|
||||
style.height = `${this.cachedHeight}px`;
|
||||
break;
|
||||
case VerticalConstraint.Scale:
|
||||
placement.top = placement.top ?? 0;
|
||||
placement.bottom = placement.bottom ?? 0;
|
||||
style.top = `${placement.top}%`;
|
||||
style.bottom = `${placement.bottom}%`;
|
||||
delete placement.height;
|
||||
style.top = `${this.cachedTop}%`;
|
||||
style.bottom = `${this.cachedBottom ?? 0}%`;
|
||||
style.height = '';
|
||||
break;
|
||||
}
|
||||
|
||||
switch (horizontal) {
|
||||
case HorizontalConstraint.Left:
|
||||
placement.left = placement.left ?? 0;
|
||||
placement.width = placement.width ?? 100;
|
||||
style.left = `${placement.left}px`;
|
||||
style.width = `${placement.width}px`;
|
||||
delete placement.right;
|
||||
style.left = `${this.cachedLeft}px`;
|
||||
style.width = `${this.cachedWidth}px`;
|
||||
break;
|
||||
case HorizontalConstraint.Right:
|
||||
placement.right = placement.right ?? 0;
|
||||
placement.width = placement.width ?? 100;
|
||||
style.right = `${placement.right}px`;
|
||||
style.width = `${placement.width}px`;
|
||||
delete placement.left;
|
||||
style.right = `${this.cachedRight ?? 0}px`;
|
||||
style.width = `${this.cachedWidth}px`;
|
||||
break;
|
||||
case HorizontalConstraint.LeftRight:
|
||||
placement.left = placement.left ?? 0;
|
||||
placement.right = placement.right ?? 0;
|
||||
style.left = `${placement.left}px`;
|
||||
style.right = `${placement.right}px`;
|
||||
delete placement.width;
|
||||
style.left = `${this.cachedLeft}px`;
|
||||
style.right = `${this.cachedRight ?? 0}px`;
|
||||
style.width = '';
|
||||
break;
|
||||
case HorizontalConstraint.Center:
|
||||
placement.left = placement.left ?? 0;
|
||||
placement.width = placement.width ?? 100;
|
||||
translate[0] = '-50%';
|
||||
style.left = `calc(50% - ${placement.left}px)`;
|
||||
style.width = `${placement.width}px`;
|
||||
delete placement.right;
|
||||
style.left = `calc(50% - ${this.cachedLeft}px)`;
|
||||
style.width = `${this.cachedWidth}px`;
|
||||
break;
|
||||
case HorizontalConstraint.Scale:
|
||||
placement.left = placement.left ?? 0;
|
||||
placement.right = placement.right ?? 0;
|
||||
style.left = `${placement.left}%`;
|
||||
style.right = `${placement.right}%`;
|
||||
delete placement.width;
|
||||
style.left = `${this.cachedLeft}%`;
|
||||
style.right = `${this.cachedRight ?? 0}%`;
|
||||
style.width = '';
|
||||
break;
|
||||
}
|
||||
|
||||
style.transform = `translate(${translate[0]}, ${translate[1]})`;
|
||||
this.options.placement = placement;
|
||||
this.sizeStyle = style;
|
||||
|
||||
if (this.div) {
|
||||
@@ -267,7 +292,6 @@ export class ElementState implements LayerElement {
|
||||
|
||||
const { constraint } = this.options;
|
||||
const { vertical, horizontal } = constraint ?? {};
|
||||
const placement: Placement = this.options.placement ?? {};
|
||||
|
||||
const editingEnabled = scene?.isEditingEnabled;
|
||||
|
||||
@@ -275,7 +299,6 @@ export class ElementState implements LayerElement {
|
||||
cursor: editingEnabled ? 'grab' : 'auto',
|
||||
pointerEvents: disablePointerEvents ? 'none' : 'auto',
|
||||
position: 'absolute',
|
||||
// Minimum element size is 10x10
|
||||
minWidth: '10px',
|
||||
minHeight: '10px',
|
||||
};
|
||||
@@ -285,81 +308,50 @@ export class ElementState implements LayerElement {
|
||||
|
||||
switch (vertical) {
|
||||
case VerticalConstraint.Top:
|
||||
placement.top = placement.top ?? 0;
|
||||
placement.height = placement.height ?? 100;
|
||||
transformY = `${placement.top ?? 0}px`;
|
||||
style.height = `${placement.height}px`;
|
||||
delete placement.bottom;
|
||||
transformY = `${this.cachedTop}px`;
|
||||
style.height = `${this.cachedHeight}px`;
|
||||
break;
|
||||
case VerticalConstraint.Bottom:
|
||||
placement.bottom = placement.bottom ?? 0;
|
||||
placement.height = placement.height ?? 100;
|
||||
transformY = `${sceneHeight! - (placement.bottom ?? 0) - (placement.height ?? 100)}px`;
|
||||
style.height = `${placement.height}px`;
|
||||
delete placement.top;
|
||||
transformY = `${sceneHeight! - (this.cachedBottom ?? 0) - this.cachedHeight}px`;
|
||||
style.height = `${this.cachedHeight}px`;
|
||||
break;
|
||||
case VerticalConstraint.TopBottom:
|
||||
placement.top = placement.top ?? 0;
|
||||
placement.bottom = placement.bottom ?? 0;
|
||||
transformY = `${placement.top ?? 0}px`;
|
||||
style.height = `${sceneHeight! - (placement.top ?? 0) - (placement.bottom ?? 0)}px`;
|
||||
delete placement.height;
|
||||
transformY = `${this.cachedTop}px`;
|
||||
style.height = `${sceneHeight! - this.cachedTop - (this.cachedBottom ?? 0)}px`;
|
||||
break;
|
||||
case VerticalConstraint.Center:
|
||||
placement.top = placement.top ?? 0;
|
||||
placement.height = placement.height ?? 100;
|
||||
transformY = `${sceneHeight! / 2 - (placement.top ?? 0) - (placement.height ?? 0) / 2}px`;
|
||||
style.height = `${placement.height}px`;
|
||||
delete placement.bottom;
|
||||
transformY = `${sceneHeight! / 2 - this.cachedTop - this.cachedHeight / 2}px`;
|
||||
style.height = `${this.cachedHeight}px`;
|
||||
break;
|
||||
case VerticalConstraint.Scale:
|
||||
placement.top = placement.top ?? 0;
|
||||
placement.bottom = placement.bottom ?? 0;
|
||||
transformY = `${(placement.top ?? 0) * (sceneHeight! / 100)}px`;
|
||||
style.height = `${sceneHeight! - (placement.top ?? 0) * (sceneHeight! / 100) - (placement.bottom ?? 0) * (sceneHeight! / 100)}px`;
|
||||
delete placement.height;
|
||||
transformY = `${this.cachedTop * (sceneHeight! / 100)}px`;
|
||||
style.height = `${sceneHeight! - this.cachedTop * (sceneHeight! / 100) - (this.cachedBottom ?? 0) * (sceneHeight! / 100)}px`;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (horizontal) {
|
||||
case HorizontalConstraint.Left:
|
||||
placement.left = placement.left ?? 0;
|
||||
placement.width = placement.width ?? 100;
|
||||
transformX = `${placement.left ?? 0}px`;
|
||||
style.width = `${placement.width}px`;
|
||||
delete placement.right;
|
||||
transformX = `${this.cachedLeft}px`;
|
||||
style.width = `${this.cachedWidth}px`;
|
||||
break;
|
||||
case HorizontalConstraint.Right:
|
||||
placement.right = placement.right ?? 0;
|
||||
placement.width = placement.width ?? 100;
|
||||
transformX = `${sceneWidth! - (placement.right ?? 0) - (placement.width ?? 100)}px`;
|
||||
style.width = `${placement.width}px`;
|
||||
delete placement.left;
|
||||
transformX = `${sceneWidth! - (this.cachedRight ?? 0) - this.cachedWidth}px`;
|
||||
style.width = `${this.cachedWidth}px`;
|
||||
break;
|
||||
case HorizontalConstraint.LeftRight:
|
||||
placement.left = placement.left ?? 0;
|
||||
placement.right = placement.right ?? 0;
|
||||
transformX = `${placement.left ?? 0}px`;
|
||||
style.width = `${sceneWidth! - (placement.left ?? 0) - (placement.right ?? 0)}px`;
|
||||
delete placement.width;
|
||||
transformX = `${this.cachedLeft}px`;
|
||||
style.width = `${sceneWidth! - this.cachedLeft - (this.cachedRight ?? 0)}px`;
|
||||
break;
|
||||
case HorizontalConstraint.Center:
|
||||
placement.left = placement.left ?? 0;
|
||||
placement.width = placement.width ?? 100;
|
||||
transformX = `${sceneWidth! / 2 - (placement.left ?? 0) - (placement.width ?? 0) / 2}px`;
|
||||
style.width = `${placement.width}px`;
|
||||
delete placement.right;
|
||||
transformX = `${sceneWidth! / 2 - this.cachedLeft - this.cachedWidth / 2}px`;
|
||||
style.width = `${this.cachedWidth}px`;
|
||||
break;
|
||||
case HorizontalConstraint.Scale:
|
||||
placement.left = placement.left ?? 0;
|
||||
placement.right = placement.right ?? 0;
|
||||
transformX = `${(placement.left ?? 0) * (sceneWidth! / 100)}px`;
|
||||
style.width = `${sceneWidth! - (placement.left ?? 0) * (sceneWidth! / 100) - (placement.right ?? 0) * (sceneWidth! / 100)}px`;
|
||||
delete placement.width;
|
||||
transformX = `${this.cachedLeft * (sceneWidth! / 100)}px`;
|
||||
style.width = `${sceneWidth! - this.cachedLeft * (sceneWidth! / 100) - (this.cachedRight ?? 0) * (sceneWidth! / 100)}px`;
|
||||
break;
|
||||
}
|
||||
this.options.placement = placement;
|
||||
style.transform = `translate(${transformX}, ${transformY}) rotate(${placement.rotation ?? 0}deg)`;
|
||||
style.transform = `translate(${transformX}, ${transformY}) rotate(${this.cachedRotation}deg)`;
|
||||
this.sizeStyle = style;
|
||||
|
||||
if (this.div) {
|
||||
@@ -415,8 +407,8 @@ export class ElementState implements LayerElement {
|
||||
// TODO: Fix behavior for top+bottom, left+right, center, and scale constraints
|
||||
let rotationTopOffset = 0;
|
||||
let rotationLeftOffset = 0;
|
||||
if (this.options.placement?.rotation && this.options.placement?.width && this.options.placement?.height) {
|
||||
const rotationDegrees = this.options.placement.rotation;
|
||||
if (this.cachedRotation && this.options.placement?.width && this.options.placement?.height) {
|
||||
const rotationDegrees = this.cachedRotation;
|
||||
const rotationRadians = (Math.PI / 180) * rotationDegrees;
|
||||
let rotationOffset = rotationRadians;
|
||||
|
||||
@@ -438,8 +430,8 @@ export class ElementState implements LayerElement {
|
||||
const calculateDelta = (dimension1: number, dimension2: number) =>
|
||||
(dimension1 / 2) * Math.sin(rotationOffset) + (dimension2 / 2) * (Math.cos(rotationOffset) - 1);
|
||||
|
||||
rotationTopOffset = calculateDelta(this.options.placement.width, this.options.placement.height);
|
||||
rotationLeftOffset = calculateDelta(this.options.placement.height, this.options.placement.width);
|
||||
rotationTopOffset = calculateDelta(this.cachedWidth, this.cachedHeight);
|
||||
rotationLeftOffset = calculateDelta(this.cachedHeight, this.cachedWidth);
|
||||
}
|
||||
|
||||
const relativeTop =
|
||||
@@ -463,67 +455,103 @@ export class ElementState implements LayerElement {
|
||||
transformScale
|
||||
: 0;
|
||||
|
||||
const placement: Placement = {};
|
||||
// Don't update placement if any position is field-driven
|
||||
if (this.hasFieldDrivenPosition()) {
|
||||
this.applyLayoutStylesToDiv();
|
||||
this.revId++;
|
||||
return;
|
||||
}
|
||||
|
||||
const width = (elementContainer?.width ?? 100) / transformScale;
|
||||
const height = (elementContainer?.height ?? 100) / transformScale;
|
||||
|
||||
// Helper to create a position dimension config
|
||||
const fixedPosition = (value: number): PositionDimensionConfig => ({
|
||||
fixed: value,
|
||||
mode: PositionDimensionMode.Fixed,
|
||||
});
|
||||
|
||||
const placement: Placement = {};
|
||||
|
||||
switch (vertical) {
|
||||
case VerticalConstraint.Top:
|
||||
placement.top = relativeTop;
|
||||
placement.height = height;
|
||||
placement.top = fixedPosition(relativeTop);
|
||||
placement.height = fixedPosition(height);
|
||||
this.cachedTop = relativeTop;
|
||||
this.cachedHeight = height;
|
||||
break;
|
||||
case VerticalConstraint.Bottom:
|
||||
placement.bottom = relativeBottom;
|
||||
placement.height = height;
|
||||
placement.bottom = fixedPosition(relativeBottom);
|
||||
placement.height = fixedPosition(height);
|
||||
this.cachedBottom = relativeBottom;
|
||||
this.cachedHeight = height;
|
||||
break;
|
||||
case VerticalConstraint.TopBottom:
|
||||
placement.top = relativeTop;
|
||||
placement.bottom = relativeBottom;
|
||||
placement.top = fixedPosition(relativeTop);
|
||||
placement.bottom = fixedPosition(relativeBottom);
|
||||
this.cachedTop = relativeTop;
|
||||
this.cachedBottom = relativeBottom;
|
||||
break;
|
||||
case VerticalConstraint.Center:
|
||||
const elementCenter = elementContainer ? relativeTop + height / 2 : 0;
|
||||
const parentCenter = parentContainer ? parentContainer.height / 2 : 0;
|
||||
const distanceFromCenter = parentCenter - elementCenter;
|
||||
placement.top = distanceFromCenter;
|
||||
placement.height = height;
|
||||
const elementCenterV = elementContainer ? relativeTop + height / 2 : 0;
|
||||
const parentCenterV = parentContainer ? parentContainer.height / 2 : 0;
|
||||
const distanceFromCenterV = parentCenterV - elementCenterV;
|
||||
placement.top = fixedPosition(distanceFromCenterV);
|
||||
placement.height = fixedPosition(height);
|
||||
this.cachedTop = distanceFromCenterV;
|
||||
this.cachedHeight = height;
|
||||
break;
|
||||
case VerticalConstraint.Scale:
|
||||
placement.top = (relativeTop / (parentContainer?.height ?? height)) * 100 * transformScale;
|
||||
placement.bottom = (relativeBottom / (parentContainer?.height ?? height)) * 100 * transformScale;
|
||||
const scaleTop = (relativeTop / (parentContainer?.height ?? height)) * 100 * transformScale;
|
||||
const scaleBottom = (relativeBottom / (parentContainer?.height ?? height)) * 100 * transformScale;
|
||||
placement.top = fixedPosition(scaleTop);
|
||||
placement.bottom = fixedPosition(scaleBottom);
|
||||
this.cachedTop = scaleTop;
|
||||
this.cachedBottom = scaleBottom;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (horizontal) {
|
||||
case HorizontalConstraint.Left:
|
||||
placement.left = relativeLeft;
|
||||
placement.width = width;
|
||||
placement.left = fixedPosition(relativeLeft);
|
||||
placement.width = fixedPosition(width);
|
||||
this.cachedLeft = relativeLeft;
|
||||
this.cachedWidth = width;
|
||||
break;
|
||||
case HorizontalConstraint.Right:
|
||||
placement.right = relativeRight;
|
||||
placement.width = width;
|
||||
placement.right = fixedPosition(relativeRight);
|
||||
placement.width = fixedPosition(width);
|
||||
this.cachedRight = relativeRight;
|
||||
this.cachedWidth = width;
|
||||
break;
|
||||
case HorizontalConstraint.LeftRight:
|
||||
placement.left = relativeLeft;
|
||||
placement.right = relativeRight;
|
||||
placement.left = fixedPosition(relativeLeft);
|
||||
placement.right = fixedPosition(relativeRight);
|
||||
this.cachedLeft = relativeLeft;
|
||||
this.cachedRight = relativeRight;
|
||||
break;
|
||||
case HorizontalConstraint.Center:
|
||||
const elementCenter = elementContainer ? relativeLeft + width / 2 : 0;
|
||||
const parentCenter = parentContainer ? parentContainer.width / 2 : 0;
|
||||
const distanceFromCenter = parentCenter - elementCenter;
|
||||
placement.left = distanceFromCenter;
|
||||
placement.width = width;
|
||||
const elementCenterH = elementContainer ? relativeLeft + width / 2 : 0;
|
||||
const parentCenterH = parentContainer ? parentContainer.width / 2 : 0;
|
||||
const distanceFromCenterH = parentCenterH - elementCenterH;
|
||||
placement.left = fixedPosition(distanceFromCenterH);
|
||||
placement.width = fixedPosition(width);
|
||||
this.cachedLeft = distanceFromCenterH;
|
||||
this.cachedWidth = width;
|
||||
break;
|
||||
case HorizontalConstraint.Scale:
|
||||
placement.left = (relativeLeft / (parentContainer?.width ?? width)) * 100 * transformScale;
|
||||
placement.right = (relativeRight / (parentContainer?.width ?? width)) * 100 * transformScale;
|
||||
const scaleLeft = (relativeLeft / (parentContainer?.width ?? width)) * 100 * transformScale;
|
||||
const scaleRight = (relativeRight / (parentContainer?.width ?? width)) * 100 * transformScale;
|
||||
placement.left = fixedPosition(scaleLeft);
|
||||
placement.right = fixedPosition(scaleRight);
|
||||
this.cachedLeft = scaleLeft;
|
||||
this.cachedRight = scaleRight;
|
||||
break;
|
||||
}
|
||||
|
||||
// Preserve rotation
|
||||
if (this.options.placement?.rotation) {
|
||||
placement.rotation = this.options.placement.rotation;
|
||||
placement.width = this.options.placement.width;
|
||||
placement.height = this.options.placement.height;
|
||||
}
|
||||
|
||||
this.options.placement = placement;
|
||||
@@ -554,71 +582,109 @@ export class ElementState implements LayerElement {
|
||||
const relativeLeft = Math.round(elementRect.left);
|
||||
const relativeRight = Math.round(scene.width - elementRect.left - elementRect.width);
|
||||
|
||||
const placement: Placement = {};
|
||||
// Don't update placement if any position is field-driven
|
||||
if (this.hasFieldDrivenPosition()) {
|
||||
this.applyLayoutStylesToDiv();
|
||||
this.revId++;
|
||||
return;
|
||||
}
|
||||
|
||||
const width = elementRect.width;
|
||||
const height = elementRect.height;
|
||||
|
||||
// INFO: calculate it anyway to be able to use it for pan&zoom
|
||||
placement.top = relativeTop;
|
||||
placement.left = relativeLeft;
|
||||
// Helper to create a position dimension config
|
||||
const fixedPosition = (value: number): PositionDimensionConfig => ({
|
||||
fixed: value,
|
||||
mode: PositionDimensionMode.Fixed,
|
||||
});
|
||||
|
||||
const placement: Placement = {};
|
||||
|
||||
// INFO: calculate for pan&zoom
|
||||
placement.top = fixedPosition(relativeTop);
|
||||
placement.left = fixedPosition(relativeLeft);
|
||||
this.cachedTop = relativeTop;
|
||||
this.cachedLeft = relativeLeft;
|
||||
|
||||
switch (vertical) {
|
||||
case VerticalConstraint.Top:
|
||||
placement.top = relativeTop;
|
||||
placement.height = height;
|
||||
placement.top = fixedPosition(relativeTop);
|
||||
placement.height = fixedPosition(height);
|
||||
this.cachedTop = relativeTop;
|
||||
this.cachedHeight = height;
|
||||
break;
|
||||
case VerticalConstraint.Bottom:
|
||||
placement.bottom = relativeBottom;
|
||||
placement.height = height;
|
||||
placement.bottom = fixedPosition(relativeBottom);
|
||||
placement.height = fixedPosition(height);
|
||||
this.cachedBottom = relativeBottom;
|
||||
this.cachedHeight = height;
|
||||
break;
|
||||
case VerticalConstraint.TopBottom:
|
||||
placement.top = relativeTop;
|
||||
placement.bottom = relativeBottom;
|
||||
placement.top = fixedPosition(relativeTop);
|
||||
placement.bottom = fixedPosition(relativeBottom);
|
||||
this.cachedTop = relativeTop;
|
||||
this.cachedBottom = relativeBottom;
|
||||
break;
|
||||
case VerticalConstraint.Center:
|
||||
const elementCenter = elementContainer ? relativeTop + height / 2 : 0;
|
||||
const parentCenter = scene.height / 2; // Use scene height instead of scaled viewport height
|
||||
const distanceFromCenter = parentCenter - elementCenter;
|
||||
placement.top = distanceFromCenter;
|
||||
placement.height = height;
|
||||
const elementCenterV = elementContainer ? relativeTop + height / 2 : 0;
|
||||
const parentCenterV = scene.height / 2;
|
||||
const distanceFromCenterV = parentCenterV - elementCenterV;
|
||||
placement.top = fixedPosition(distanceFromCenterV);
|
||||
placement.height = fixedPosition(height);
|
||||
this.cachedTop = distanceFromCenterV;
|
||||
this.cachedHeight = height;
|
||||
break;
|
||||
case VerticalConstraint.Scale:
|
||||
placement.top = (relativeTop / (parentContainer?.height ?? height)) * 100 * transformScale;
|
||||
placement.bottom = (relativeBottom / (parentContainer?.height ?? height)) * 100 * transformScale;
|
||||
const scaleTop = (relativeTop / (parentContainer?.height ?? height)) * 100 * transformScale;
|
||||
const scaleBottom = (relativeBottom / (parentContainer?.height ?? height)) * 100 * transformScale;
|
||||
placement.top = fixedPosition(scaleTop);
|
||||
placement.bottom = fixedPosition(scaleBottom);
|
||||
this.cachedTop = scaleTop;
|
||||
this.cachedBottom = scaleBottom;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (horizontal) {
|
||||
case HorizontalConstraint.Left:
|
||||
placement.left = relativeLeft;
|
||||
placement.width = width;
|
||||
placement.left = fixedPosition(relativeLeft);
|
||||
placement.width = fixedPosition(width);
|
||||
this.cachedLeft = relativeLeft;
|
||||
this.cachedWidth = width;
|
||||
break;
|
||||
case HorizontalConstraint.Right:
|
||||
placement.right = relativeRight;
|
||||
placement.width = width;
|
||||
placement.right = fixedPosition(relativeRight);
|
||||
placement.width = fixedPosition(width);
|
||||
this.cachedRight = relativeRight;
|
||||
this.cachedWidth = width;
|
||||
break;
|
||||
case HorizontalConstraint.LeftRight:
|
||||
placement.left = relativeLeft;
|
||||
placement.right = relativeRight;
|
||||
placement.left = fixedPosition(relativeLeft);
|
||||
placement.right = fixedPosition(relativeRight);
|
||||
this.cachedLeft = relativeLeft;
|
||||
this.cachedRight = relativeRight;
|
||||
break;
|
||||
case HorizontalConstraint.Center:
|
||||
const elementCenter = elementContainer ? relativeLeft + width / 2 : 0;
|
||||
const parentCenter = scene.width / 2; // Use scene width instead of scaled viewport width
|
||||
const distanceFromCenter = parentCenter - elementCenter;
|
||||
placement.left = distanceFromCenter;
|
||||
placement.width = width;
|
||||
const elementCenterH = elementContainer ? relativeLeft + width / 2 : 0;
|
||||
const parentCenterH = scene.width / 2;
|
||||
const distanceFromCenterH = parentCenterH - elementCenterH;
|
||||
placement.left = fixedPosition(distanceFromCenterH);
|
||||
placement.width = fixedPosition(width);
|
||||
this.cachedLeft = distanceFromCenterH;
|
||||
this.cachedWidth = width;
|
||||
break;
|
||||
case HorizontalConstraint.Scale:
|
||||
placement.left = (relativeLeft / (parentContainer?.width ?? width)) * 100 * transformScale;
|
||||
placement.right = (relativeRight / (parentContainer?.width ?? width)) * 100 * transformScale;
|
||||
const scaleLeft = (relativeLeft / (parentContainer?.width ?? width)) * 100 * transformScale;
|
||||
const scaleRight = (relativeRight / (parentContainer?.width ?? width)) * 100 * transformScale;
|
||||
placement.left = fixedPosition(scaleLeft);
|
||||
placement.right = fixedPosition(scaleRight);
|
||||
this.cachedLeft = scaleLeft;
|
||||
this.cachedRight = scaleRight;
|
||||
break;
|
||||
}
|
||||
|
||||
// Preserve rotation
|
||||
if (this.options.placement?.rotation) {
|
||||
placement.rotation = this.options.placement.rotation;
|
||||
placement.width = this.options.placement.width;
|
||||
placement.height = this.options.placement.height;
|
||||
}
|
||||
|
||||
this.options.placement = placement;
|
||||
@@ -635,6 +701,32 @@ export class ElementState implements LayerElement {
|
||||
this.revId++; // rerender
|
||||
}
|
||||
|
||||
// Update placement values from dimension context
|
||||
const placement = this.options.placement;
|
||||
if (placement) {
|
||||
if (placement.rotation) {
|
||||
this.cachedRotation = ctx.getScalar(placement.rotation).value();
|
||||
}
|
||||
if (placement.top) {
|
||||
this.cachedTop = ctx.getPosition(placement.top).value();
|
||||
}
|
||||
if (placement.left) {
|
||||
this.cachedLeft = ctx.getPosition(placement.left).value();
|
||||
}
|
||||
if (placement.width) {
|
||||
this.cachedWidth = ctx.getPosition(placement.width).value();
|
||||
}
|
||||
if (placement.height) {
|
||||
this.cachedHeight = ctx.getPosition(placement.height).value();
|
||||
}
|
||||
if (placement.right) {
|
||||
this.cachedRight = ctx.getPosition(placement.right).value();
|
||||
}
|
||||
if (placement.bottom) {
|
||||
this.cachedBottom = ctx.getPosition(placement.bottom).value();
|
||||
}
|
||||
}
|
||||
|
||||
const scene = this.getScene();
|
||||
const frames = scene?.data?.series;
|
||||
|
||||
@@ -793,6 +885,11 @@ export class ElementState implements LayerElement {
|
||||
};
|
||||
|
||||
applyDrag = (event: OnDrag) => {
|
||||
// Don't allow dragging if any position is field-driven
|
||||
if (this.hasFieldDrivenPosition()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasHorizontalCenterConstraint = this.options.constraint?.horizontal === HorizontalConstraint.Center;
|
||||
const hasVerticalCenterConstraint = this.options.constraint?.vertical === VerticalConstraint.Center;
|
||||
if (hasHorizontalCenterConstraint || hasVerticalCenterConstraint) {
|
||||
@@ -813,18 +910,31 @@ export class ElementState implements LayerElement {
|
||||
applyRotate = (event: OnRotate) => {
|
||||
const rotationDelta = event.delta;
|
||||
const placement = this.options.placement!;
|
||||
const placementRotation = placement.rotation ?? 0;
|
||||
const placementRotation = this.cachedRotation;
|
||||
|
||||
const calculatedRotation = placementRotation + rotationDelta;
|
||||
|
||||
// Ensure rotation is between 0 and 360
|
||||
placement.rotation = calculatedRotation - Math.floor(calculatedRotation / 360) * 360;
|
||||
const newRotation = calculatedRotation - Math.floor(calculatedRotation / 360) * 360;
|
||||
|
||||
// Update the config value as fixed
|
||||
if (!placement.rotation) {
|
||||
placement.rotation = { fixed: newRotation, min: 0, max: 360, mode: ScalarDimensionMode.Clamped };
|
||||
} else {
|
||||
placement.rotation.fixed = newRotation;
|
||||
}
|
||||
this.cachedRotation = newRotation;
|
||||
event.target.style.transform = event.transform;
|
||||
};
|
||||
|
||||
// kinda like:
|
||||
// https://github.com/grafana/grafana-edge-app/blob/main/src/panels/draw/WrapItem.tsx#L44
|
||||
applyResize = (event: OnResize) => {
|
||||
// Don't allow resizing if any position is field-driven
|
||||
if (this.hasFieldDrivenPosition()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const placement = this.options.placement!;
|
||||
|
||||
const style = event.target.style;
|
||||
@@ -834,8 +944,8 @@ export class ElementState implements LayerElement {
|
||||
let dirTB = event.direction[1];
|
||||
|
||||
// Handle case when element is rotated
|
||||
if (placement.rotation) {
|
||||
const rotation = placement.rotation ?? 0;
|
||||
if (this.cachedRotation) {
|
||||
const rotation = this.cachedRotation;
|
||||
const rotationInRadians = (rotation * Math.PI) / 180;
|
||||
const originalDirLR = dirLR;
|
||||
const originalDirTB = dirTB;
|
||||
@@ -845,31 +955,37 @@ export class ElementState implements LayerElement {
|
||||
}
|
||||
|
||||
if (dirLR === 1) {
|
||||
placement.width = event.width;
|
||||
style.width = `${placement.width}px`;
|
||||
this.setPositionFixed(placement.width, event.width);
|
||||
this.cachedWidth = event.width;
|
||||
style.width = `${this.cachedWidth}px`;
|
||||
} else if (dirLR === -1) {
|
||||
placement.left! -= deltaX;
|
||||
placement.width = event.width;
|
||||
this.cachedLeft -= deltaX;
|
||||
this.setPositionFixed(placement.left, this.cachedLeft);
|
||||
this.cachedWidth = event.width;
|
||||
this.setPositionFixed(placement.width, this.cachedWidth);
|
||||
if (config.featureToggles.canvasPanelPanZoom) {
|
||||
style.transform = `translate(${placement.left}px, ${placement.top}px) rotate(${placement.rotation ?? 0}deg)`;
|
||||
style.transform = `translate(${this.cachedLeft}px, ${this.cachedTop}px) rotate(${this.cachedRotation}deg)`;
|
||||
} else {
|
||||
style.left = `${placement.left}px`;
|
||||
style.left = `${this.cachedLeft}px`;
|
||||
}
|
||||
style.width = `${placement.width}px`;
|
||||
style.width = `${this.cachedWidth}px`;
|
||||
}
|
||||
|
||||
if (dirTB === -1) {
|
||||
placement.top! -= deltaY;
|
||||
placement.height = event.height;
|
||||
this.cachedTop -= deltaY;
|
||||
this.setPositionFixed(placement.top, this.cachedTop);
|
||||
this.cachedHeight = event.height;
|
||||
this.setPositionFixed(placement.height, this.cachedHeight);
|
||||
if (config.featureToggles.canvasPanelPanZoom) {
|
||||
style.transform = `translate(${placement.left}px, ${placement.top}px) rotate(${placement.rotation ?? 0}deg)`;
|
||||
style.transform = `translate(${this.cachedLeft}px, ${this.cachedTop}px) rotate(${this.cachedRotation}deg)`;
|
||||
} else {
|
||||
style.top = `${placement.top}px`;
|
||||
style.top = `${this.cachedTop}px`;
|
||||
}
|
||||
style.height = `${placement.height}px`;
|
||||
style.height = `${this.cachedHeight}px`;
|
||||
} else if (dirTB === 1) {
|
||||
placement.height = event.height;
|
||||
style.height = `${placement.height}px`;
|
||||
this.cachedHeight = event.height;
|
||||
this.setPositionFixed(placement.height, this.cachedHeight);
|
||||
style.height = `${this.cachedHeight}px`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -880,7 +996,8 @@ export class ElementState implements LayerElement {
|
||||
!scene?.isEditingEnabled && (!scene?.tooltipPayload?.isOpen || scene?.tooltipPayload?.element === this);
|
||||
if (shouldHandleTooltip) {
|
||||
this.handleTooltip(event);
|
||||
} else if (!isSelected) {
|
||||
} else if (!isSelected && !this.hasFieldDrivenPosition()) {
|
||||
// Don't show connection anchors for field-driven elements
|
||||
scene?.connections.handleMouseEnter(event);
|
||||
}
|
||||
|
||||
@@ -1090,6 +1207,25 @@ export class ElementState implements LayerElement {
|
||||
);
|
||||
};
|
||||
|
||||
// Track if this field-driven element is selected (for showing outline)
|
||||
isFieldDrivenSelected = false;
|
||||
|
||||
setFieldDrivenSelected(selected: boolean) {
|
||||
if (this.hasFieldDrivenPosition()) {
|
||||
this.isFieldDrivenSelected = selected;
|
||||
// Update the outline style
|
||||
if (this.div) {
|
||||
if (selected) {
|
||||
this.div.style.outline = '2px solid #3274d9';
|
||||
this.div.style.outlineOffset = '2px';
|
||||
} else {
|
||||
this.div.style.outline = '';
|
||||
this.div.style.outlineOffset = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderElement() {
|
||||
const { item, div } = this;
|
||||
const scene = this.getScene();
|
||||
@@ -1112,7 +1248,7 @@ export class ElementState implements LayerElement {
|
||||
key={`${this.UID}/${this.revId}`}
|
||||
config={this.options.config}
|
||||
data={this.data}
|
||||
isSelected={isSelected}
|
||||
isSelected={isSelected || this.isFieldDrivenSelected}
|
||||
/>
|
||||
</div>
|
||||
{this.showActionConfirmation && this.renderActionsConfirmModal(this.getPrimaryAction())}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AppEvents, PanelData, OneClickMode, ActionType } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import {
|
||||
ColorDimensionConfig,
|
||||
PositionDimensionConfig,
|
||||
ResourceDimensionConfig,
|
||||
ScalarDimensionConfig,
|
||||
ScaleDimensionConfig,
|
||||
@@ -21,6 +22,7 @@ import { config } from 'app/core/config';
|
||||
import { DimensionContext } from 'app/features/dimensions/context';
|
||||
import {
|
||||
getColorDimensionFromData,
|
||||
getPositionDimensionFromData,
|
||||
getResourceDimensionFromData,
|
||||
getScalarDimensionFromData,
|
||||
getScaleDimensionFromData,
|
||||
@@ -109,6 +111,22 @@ export class Scene {
|
||||
|
||||
targetsToSelect = new Set<HTMLDivElement>();
|
||||
|
||||
// Track currently selected field-driven element (these aren't in Selecto)
|
||||
private fieldDrivenSelectedElement?: ElementState;
|
||||
|
||||
clearFieldDrivenSelection = () => {
|
||||
if (this.fieldDrivenSelectedElement) {
|
||||
this.fieldDrivenSelectedElement.setFieldDrivenSelected(false);
|
||||
this.fieldDrivenSelectedElement = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
setFieldDrivenSelection = (element: ElementState) => {
|
||||
this.clearFieldDrivenSelection();
|
||||
this.fieldDrivenSelectedElement = element;
|
||||
element.setFieldDrivenSelected(true);
|
||||
};
|
||||
|
||||
constructor(
|
||||
options: Options,
|
||||
public onSave: (cfg: CanvasFrameOptions) => void,
|
||||
@@ -211,6 +229,7 @@ export class Scene {
|
||||
getColor: (color: ColorDimensionConfig) => getColorDimensionFromData(this.data, color),
|
||||
getScale: (scale: ScaleDimensionConfig) => getScaleDimensionFromData(this.data, scale),
|
||||
getScalar: (scalar: ScalarDimensionConfig) => getScalarDimensionFromData(this.data, scalar),
|
||||
getPosition: (pos: PositionDimensionConfig) => getPositionDimensionFromData(this.data, pos),
|
||||
getText: (text: TextDimensionConfig) => getTextDimensionFromData(this.data, text),
|
||||
getResource: (res: ResourceDimensionConfig) => getResourceDimensionFromData(this.data, res),
|
||||
getDirection: (direction: DirectionDimensionConfig) => getDirectionDimensionFromData(this.data, direction),
|
||||
@@ -267,6 +286,8 @@ export class Scene {
|
||||
|
||||
clearCurrentSelection(skipNextSelectionBroadcast = false) {
|
||||
this.skipNextSelectionBroadcast = skipNextSelectionBroadcast;
|
||||
// Clear field-driven selection
|
||||
this.clearFieldDrivenSelection();
|
||||
let event: MouseEvent = new MouseEvent('click');
|
||||
if (config.featureToggles.canvasPanelPanZoom) {
|
||||
this.selecto?.clickTarget(event, this.viewportDiv);
|
||||
@@ -324,6 +345,9 @@ export class Scene {
|
||||
|
||||
select = (selection: SelectionParams) => {
|
||||
if (this.selecto) {
|
||||
// Clear any field-driven selection when selecting via Selecto
|
||||
this.clearFieldDrivenSelection();
|
||||
|
||||
this.selecto.setSelectedTargets(selection.targets);
|
||||
this.updateSelection(selection);
|
||||
this.editModeEnabled.next(false);
|
||||
|
||||
@@ -69,6 +69,7 @@ const isTargetAlreadySelected = (selectedTarget: HTMLElement, scene: Scene) => {
|
||||
};
|
||||
|
||||
// Generate HTML element divs for every canvas element to configure selecto / moveable
|
||||
// Excludes elements with field-driven positions (they can't be moved in editor)
|
||||
const generateTargetElements = (rootElements: ElementState[]): HTMLDivElement[] => {
|
||||
let targetElements: HTMLDivElement[] = [];
|
||||
|
||||
@@ -77,7 +78,10 @@ const generateTargetElements = (rootElements: ElementState[]): HTMLDivElement[]
|
||||
const currentElement = stack.shift();
|
||||
|
||||
if (currentElement && currentElement.div) {
|
||||
targetElements.push(currentElement.div);
|
||||
// Skip elements with field-driven positions - they can't be moved
|
||||
if (!currentElement.hasFieldDrivenPosition()) {
|
||||
targetElements.push(currentElement.div);
|
||||
}
|
||||
}
|
||||
|
||||
const nestedElements = currentElement instanceof FrameState ? currentElement.elements : [];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PanelData } from '@grafana/data';
|
||||
import {
|
||||
ColorDimensionConfig,
|
||||
PositionDimensionConfig,
|
||||
ResourceDimensionConfig,
|
||||
ScalarDimensionConfig,
|
||||
ScaleDimensionConfig,
|
||||
@@ -18,6 +19,8 @@ export interface DimensionContext {
|
||||
|
||||
getScalar(scalar: ScalarDimensionConfig): DimensionSupplier<number>;
|
||||
|
||||
getPosition(position: PositionDimensionConfig): DimensionSupplier<number>;
|
||||
|
||||
getText(text: TextDimensionConfig): DimensionSupplier<string>;
|
||||
|
||||
getResource(resource: ResourceDimensionConfig): DimensionSupplier<string>;
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { useCallback, useId, useMemo } from 'react';
|
||||
|
||||
import { FieldType, SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { PositionDimensionConfig, PositionDimensionMode } from '@grafana/schema';
|
||||
import { InlineField, InlineFieldRow, RadioButtonGroup, Select } from '@grafana/ui';
|
||||
import { useFieldDisplayNames, useSelectOptions } from '@grafana/ui/internal';
|
||||
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
|
||||
|
||||
import { PositionDimensionOptions } from '../types';
|
||||
|
||||
type Props = StandardEditorProps<PositionDimensionConfig, PositionDimensionOptions>;
|
||||
|
||||
export const PositionDimensionEditor = ({ value, context, onChange }: Props) => {
|
||||
const positionOptions = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t('dimensions.position-dimension-editor.label-fixed', 'Fixed'),
|
||||
value: PositionDimensionMode.Fixed,
|
||||
description: t('dimensions.position-dimension-editor.description-fixed', 'Fixed value'),
|
||||
},
|
||||
{
|
||||
label: t('dimensions.position-dimension-editor.label-field', 'Field'),
|
||||
value: PositionDimensionMode.Field,
|
||||
description: t('dimensions.position-dimension-editor.description-field', 'Use field value'),
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
const fixedValueOption: SelectableValue<string> = useMemo(
|
||||
() => ({
|
||||
label: t('dimensions.position-dimension-editor.fixed-value-option.label', 'Fixed value'),
|
||||
value: '_____fixed_____',
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const labelWidth = 9;
|
||||
const fieldName = value?.field;
|
||||
const names = useFieldDisplayNames(context.data);
|
||||
// Filter to only show number fields for position values
|
||||
const selectOptions = useSelectOptions(names, fieldName, fixedValueOption, FieldType.number);
|
||||
|
||||
const onModeChange = useCallback(
|
||||
(mode: PositionDimensionMode) => {
|
||||
onChange({
|
||||
...value,
|
||||
mode,
|
||||
});
|
||||
},
|
||||
[onChange, value]
|
||||
);
|
||||
|
||||
const onFieldChange = useCallback(
|
||||
(selection: SelectableValue<string>) => {
|
||||
const field = selection.value;
|
||||
if (field && field !== fixedValueOption.value) {
|
||||
onChange({
|
||||
...value,
|
||||
field,
|
||||
});
|
||||
} else {
|
||||
onChange({
|
||||
...value,
|
||||
field: undefined,
|
||||
});
|
||||
}
|
||||
},
|
||||
[onChange, value, fixedValueOption.value]
|
||||
);
|
||||
|
||||
const onFixedChange = useCallback(
|
||||
(fixed?: number) => {
|
||||
onChange({
|
||||
...value,
|
||||
fixed: fixed ?? 0,
|
||||
});
|
||||
},
|
||||
[onChange, value]
|
||||
);
|
||||
|
||||
const fieldInputId = useId();
|
||||
const valueInputId = useId();
|
||||
|
||||
const mode = value?.mode ?? PositionDimensionMode.Fixed;
|
||||
const selectedOption =
|
||||
mode === PositionDimensionMode.Field ? selectOptions.find((v) => v.value === fieldName) : fixedValueOption;
|
||||
|
||||
return (
|
||||
<>
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label={t('dimensions.position-dimension-editor.label-source', 'Source')}
|
||||
labelWidth={labelWidth}
|
||||
grow={true}
|
||||
>
|
||||
<RadioButtonGroup value={mode} options={positionOptions} onChange={onModeChange} fullWidth />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
{mode === PositionDimensionMode.Field && (
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label={t('dimensions.position-dimension-editor.label-field', 'Field')}
|
||||
labelWidth={labelWidth}
|
||||
grow={true}
|
||||
>
|
||||
<Select
|
||||
inputId={fieldInputId}
|
||||
value={selectedOption}
|
||||
options={selectOptions}
|
||||
onChange={onFieldChange}
|
||||
noOptionsMessage={t('dimensions.position-dimension-editor.no-fields', 'No number fields found')}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
{mode === PositionDimensionMode.Fixed && (
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
label={t('dimensions.position-dimension-editor.label-value', 'Value')}
|
||||
labelWidth={labelWidth}
|
||||
grow={true}
|
||||
>
|
||||
<NumberInput id={valueInputId} value={value?.fixed ?? 0} onChange={onFixedChange} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { DataFrame, Field } from '@grafana/data';
|
||||
import { PositionDimensionConfig, PositionDimensionMode } from '@grafana/schema';
|
||||
|
||||
import { DimensionSupplier } from './types';
|
||||
import { findField, getLastNotNullFieldValue } from './utils';
|
||||
|
||||
//---------------------------------------------------------
|
||||
// Position dimension - simple fixed or field value
|
||||
//---------------------------------------------------------
|
||||
|
||||
export function getPositionDimension(
|
||||
frame: DataFrame | undefined,
|
||||
config: PositionDimensionConfig
|
||||
): DimensionSupplier<number> {
|
||||
return getPositionDimensionForField(findField(frame, config?.field), config);
|
||||
}
|
||||
|
||||
export function getPositionDimensionForField(
|
||||
field: Field | undefined,
|
||||
config: PositionDimensionConfig
|
||||
): DimensionSupplier<number> {
|
||||
const v = config.fixed ?? 0;
|
||||
const mode = config.mode ?? PositionDimensionMode.Fixed;
|
||||
|
||||
if (mode === PositionDimensionMode.Fixed) {
|
||||
return {
|
||||
isAssumed: !config.fixed,
|
||||
fixed: v,
|
||||
value: () => v,
|
||||
get: () => v,
|
||||
};
|
||||
}
|
||||
|
||||
// Field mode
|
||||
if (!field) {
|
||||
return {
|
||||
isAssumed: true,
|
||||
fixed: v,
|
||||
value: () => v,
|
||||
get: () => v,
|
||||
};
|
||||
}
|
||||
|
||||
const get = (i: number) => {
|
||||
const val = field.values[i];
|
||||
if (val === null || typeof val !== 'number') {
|
||||
return 0;
|
||||
}
|
||||
return val;
|
||||
};
|
||||
|
||||
return {
|
||||
field,
|
||||
get,
|
||||
value: () => {
|
||||
const val = getLastNotNullFieldValue(field);
|
||||
return typeof val === 'number' ? val : 0;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -46,6 +46,10 @@ export interface TextDimensionOptions {
|
||||
// anything?
|
||||
}
|
||||
|
||||
export interface PositionDimensionOptions {
|
||||
// anything?
|
||||
}
|
||||
|
||||
export const defaultTextConfig: TextDimensionConfig = Object.freeze({
|
||||
fixed: '',
|
||||
mode: TextDimensionMode.Field,
|
||||
|
||||
@@ -6,12 +6,14 @@ import {
|
||||
TextDimensionConfig,
|
||||
ColorDimensionConfig,
|
||||
ScalarDimensionConfig,
|
||||
PositionDimensionConfig,
|
||||
DirectionDimensionConfig,
|
||||
ConnectionDirection,
|
||||
} from '@grafana/schema';
|
||||
|
||||
import { getColorDimension } from './color';
|
||||
import { getDirectionDimension } from './direction';
|
||||
import { getPositionDimension } from './position';
|
||||
import { getResourceDimension } from './resource';
|
||||
import { getScalarDimension } from './scalar';
|
||||
import { getScaledDimension } from './scale';
|
||||
@@ -78,6 +80,21 @@ export function getScalarDimensionFromData(
|
||||
return getScalarDimension(undefined, cfg);
|
||||
}
|
||||
|
||||
export function getPositionDimensionFromData(
|
||||
data: PanelData | undefined,
|
||||
cfg: PositionDimensionConfig
|
||||
): DimensionSupplier<number> {
|
||||
if (data?.series && cfg.field) {
|
||||
for (const frame of data.series) {
|
||||
const d = getPositionDimension(frame, cfg);
|
||||
if (!d.isAssumed || data.series.length === 1) {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
}
|
||||
return getPositionDimension(undefined, cfg);
|
||||
}
|
||||
|
||||
export function getResourceDimensionFromData(
|
||||
data: PanelData | undefined,
|
||||
cfg: ResourceDimensionConfig
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useObservable } from 'react-use';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||
import { SelectableValue, StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { PositionDimensionConfig, ScalarDimensionConfig, ScalarDimensionMode } from '@grafana/schema';
|
||||
import { Field, Icon, InlineField, InlineFieldRow, Select, Stack } from '@grafana/ui';
|
||||
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
|
||||
import { PositionDimensionEditor } from 'app/features/dimensions/editors/PositionDimensionEditor';
|
||||
import { ScalarDimensionEditor } from 'app/features/dimensions/editors/ScalarDimensionEditor';
|
||||
|
||||
import { HorizontalConstraint, Options, Placement, VerticalConstraint } from '../../panelcfg.gen';
|
||||
|
||||
@@ -12,7 +14,7 @@ import { ConstraintSelectionBox } from './ConstraintSelectionBox';
|
||||
import { QuickPositioning } from './QuickPositioning';
|
||||
import { CanvasEditorOptions } from './elementEditor';
|
||||
|
||||
const places: Array<keyof Placement> = ['top', 'left', 'bottom', 'right', 'width', 'height', 'rotation'];
|
||||
const places: Array<keyof Placement> = ['top', 'left', 'bottom', 'right', 'width', 'height'];
|
||||
|
||||
type Props = StandardEditorProps<unknown, CanvasEditorOptions, Options>;
|
||||
|
||||
@@ -61,8 +63,9 @@ export function PlacementEditor({ item }: Props) {
|
||||
const { options } = element;
|
||||
const { placement, constraint: layout } = options;
|
||||
|
||||
if (placement) {
|
||||
placement.rotation = placement?.rotation ?? 0;
|
||||
// Initialize rotation if not set
|
||||
if (placement && !placement.rotation) {
|
||||
placement.rotation = { fixed: 0, min: 0, max: 360, mode: ScalarDimensionMode.Clamped };
|
||||
}
|
||||
|
||||
const reselectElementAfterChange = () => {
|
||||
@@ -95,20 +98,34 @@ export function PlacementEditor({ item }: Props) {
|
||||
reselectElementAfterChange();
|
||||
};
|
||||
|
||||
const onPositionChange = (value: number | undefined, placement: keyof Placement) => {
|
||||
element.options.placement![placement] = value ?? element.options.placement![placement];
|
||||
element.applyLayoutStylesToDiv();
|
||||
settings.scene.clearCurrentSelection(true);
|
||||
reselectElementAfterChange();
|
||||
const onPositionChange = (value: PositionDimensionConfig | undefined, key: keyof Placement) => {
|
||||
if (value && key !== 'rotation') {
|
||||
element.options.placement![key] = value as any;
|
||||
element.updateData(settings.scene.context);
|
||||
element.applyLayoutStylesToDiv();
|
||||
settings.scene.clearCurrentSelection(true);
|
||||
reselectElementAfterChange();
|
||||
}
|
||||
};
|
||||
|
||||
const onRotationChange = (value?: ScalarDimensionConfig) => {
|
||||
if (value) {
|
||||
element.options.placement!.rotation = value;
|
||||
element.updateData(settings.scene.context);
|
||||
element.applyLayoutStylesToDiv();
|
||||
settings.scene.clearCurrentSelection(true);
|
||||
reselectElementAfterChange();
|
||||
}
|
||||
};
|
||||
|
||||
const constraint = element.tempConstraint ?? layout ?? {};
|
||||
const editorContext = { ...settings.scene.context, data: settings.scene.context.getPanelData()?.series ?? [] };
|
||||
|
||||
return (
|
||||
<div>
|
||||
<QuickPositioning onPositionChange={onPositionChange} settings={settings} element={element} />
|
||||
<br />
|
||||
<Field label={t('canvas.placement-editor.label-constraints', 'Constraints')}>
|
||||
<Field label={t('canvas.placement-editor.label-constraints', 'Constraints')} noMargin>
|
||||
<Stack direction="row">
|
||||
<ConstraintSelectionBox
|
||||
onVerticalConstraintChange={onVerticalConstraintChange}
|
||||
@@ -134,7 +151,7 @@ export function PlacementEditor({ item }: Props) {
|
||||
|
||||
<br />
|
||||
|
||||
<Field label={t('canvas.placement-editor.label-position', 'Position')}>
|
||||
<Field label={t('canvas.placement-editor.label-position', 'Position')} noMargin>
|
||||
<>
|
||||
{places.map((p) => {
|
||||
const v = placement![p];
|
||||
@@ -142,18 +159,40 @@ export function PlacementEditor({ item }: Props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Need to set explicit min/max for rotation as logic only can handle 0-360
|
||||
const min = p === 'rotation' ? 0 : undefined;
|
||||
const max = p === 'rotation' ? 360 : undefined;
|
||||
|
||||
return (
|
||||
<InlineFieldRow key={p}>
|
||||
<InlineField label={p} labelWidth={8} grow={true}>
|
||||
<NumberInput min={min} max={max} value={v} onChange={(v) => onPositionChange(v, p)} />
|
||||
<PositionDimensionEditor
|
||||
value={v as PositionDimensionConfig}
|
||||
context={editorContext}
|
||||
onChange={(val) => onPositionChange(val, p)}
|
||||
item={{} as any}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
);
|
||||
})}
|
||||
{placement?.rotation && (
|
||||
<InlineFieldRow>
|
||||
<InlineField label={t('canvas.placement-editor.label-rotation', 'rotation')} labelWidth={8} grow={true}>
|
||||
<ScalarDimensionEditor
|
||||
value={placement.rotation}
|
||||
context={editorContext}
|
||||
onChange={onRotationChange}
|
||||
item={
|
||||
{
|
||||
id: 'rotation',
|
||||
name: 'Rotation',
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 360,
|
||||
},
|
||||
} as StandardEditorsRegistryItem<ScalarDimensionConfig>
|
||||
}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
)}
|
||||
</>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { PositionDimensionConfig, PositionDimensionMode } from '@grafana/schema';
|
||||
import { IconButton, useStyles2 } from '@grafana/ui';
|
||||
import { ElementState } from 'app/features/canvas/runtime/element';
|
||||
import { QuickPlacement } from 'app/features/canvas/types';
|
||||
@@ -11,7 +12,7 @@ import { HorizontalConstraint, VerticalConstraint, Placement } from '../../panel
|
||||
import { CanvasEditorOptions } from './elementEditor';
|
||||
|
||||
type Props = {
|
||||
onPositionChange: (value: number | undefined, placement: keyof Placement) => void;
|
||||
onPositionChange: (value: PositionDimensionConfig | undefined, placement: keyof Placement) => void;
|
||||
element: ElementState;
|
||||
settings: CanvasEditorOptions;
|
||||
};
|
||||
@@ -19,6 +20,17 @@ type Props = {
|
||||
export const QuickPositioning = ({ onPositionChange, element, settings }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
// Helper to get numeric value from PositionDimensionConfig
|
||||
const getPositionValue = (config: PositionDimensionConfig | undefined): number => {
|
||||
return config?.fixed ?? 0;
|
||||
};
|
||||
|
||||
// Helper to create a fixed PositionDimensionConfig
|
||||
const fixedPosition = (value: number): PositionDimensionConfig => ({
|
||||
fixed: value,
|
||||
mode: PositionDimensionMode.Fixed,
|
||||
});
|
||||
|
||||
const onQuickPositioningChange = (position: QuickPlacement) => {
|
||||
const defaultConstraint = { vertical: VerticalConstraint.Top, horizontal: HorizontalConstraint.Left };
|
||||
const originalConstraint = { ...element.options.constraint };
|
||||
@@ -26,24 +38,27 @@ export const QuickPositioning = ({ onPositionChange, element, settings }: Props)
|
||||
element.options.constraint = defaultConstraint;
|
||||
element.setPlacementFromConstraint();
|
||||
|
||||
const height = getPositionValue(element.options.placement?.height);
|
||||
const width = getPositionValue(element.options.placement?.width);
|
||||
|
||||
switch (position) {
|
||||
case QuickPlacement.Top:
|
||||
onPositionChange(0, 'top');
|
||||
onPositionChange(fixedPosition(0), 'top');
|
||||
break;
|
||||
case QuickPlacement.Bottom:
|
||||
onPositionChange(getRightBottomPosition(element.options.placement?.height ?? 0, 'bottom'), 'top');
|
||||
onPositionChange(fixedPosition(getRightBottomPosition(height, 'bottom')), 'top');
|
||||
break;
|
||||
case QuickPlacement.VerticalCenter:
|
||||
onPositionChange(getCenterPosition(element.options.placement?.height ?? 0, 'v'), 'top');
|
||||
onPositionChange(fixedPosition(getCenterPosition(height, 'v')), 'top');
|
||||
break;
|
||||
case QuickPlacement.Left:
|
||||
onPositionChange(0, 'left');
|
||||
onPositionChange(fixedPosition(0), 'left');
|
||||
break;
|
||||
case QuickPlacement.Right:
|
||||
onPositionChange(getRightBottomPosition(element.options.placement?.width ?? 0, 'right'), 'left');
|
||||
onPositionChange(fixedPosition(getRightBottomPosition(width, 'right')), 'left');
|
||||
break;
|
||||
case QuickPlacement.HorizontalCenter:
|
||||
onPositionChange(getCenterPosition(element.options.placement?.width ?? 0, 'h'), 'left');
|
||||
onPositionChange(fixedPosition(getCenterPosition(width, 'h')), 'left');
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import { PanelModel, OneClickMode } from '@grafana/data';
|
||||
import { PositionDimensionMode, ScalarDimensionMode } from '@grafana/schema';
|
||||
|
||||
import { Options } from './panelcfg.gen';
|
||||
|
||||
// Helper to migrate a position value from number to PositionDimensionConfig
|
||||
const migratePositionValue = (value: number | undefined) => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
fixed: value,
|
||||
mode: PositionDimensionMode.Fixed,
|
||||
};
|
||||
};
|
||||
|
||||
export const canvasMigrationHandler = (panel: PanelModel): Partial<Options> => {
|
||||
const pluginVersion = panel?.pluginVersion ?? '';
|
||||
|
||||
@@ -99,5 +111,52 @@ export const canvasMigrationHandler = (panel: PanelModel): Partial<Options> => {
|
||||
}
|
||||
}
|
||||
|
||||
// migrate placement values from numbers to dimension configs
|
||||
if (parseFloat(pluginVersion) <= 12.4) {
|
||||
const root = panel.options?.root;
|
||||
if (root?.elements) {
|
||||
for (const element of root.elements) {
|
||||
if (element.placement) {
|
||||
// Migrate rotation from number to ScalarDimensionConfig
|
||||
if (typeof element.placement.rotation === 'number') {
|
||||
element.placement.rotation = {
|
||||
fixed: element.placement.rotation,
|
||||
min: 0,
|
||||
max: 360,
|
||||
mode: ScalarDimensionMode.Clamped,
|
||||
};
|
||||
} else if (!element.placement.rotation) {
|
||||
element.placement.rotation = {
|
||||
fixed: 0,
|
||||
min: 0,
|
||||
max: 360,
|
||||
mode: ScalarDimensionMode.Clamped,
|
||||
};
|
||||
}
|
||||
|
||||
// Migrate position values from numbers to PositionDimensionConfig
|
||||
if (typeof element.placement.top === 'number') {
|
||||
element.placement.top = migratePositionValue(element.placement.top);
|
||||
}
|
||||
if (typeof element.placement.left === 'number') {
|
||||
element.placement.left = migratePositionValue(element.placement.left);
|
||||
}
|
||||
if (typeof element.placement.width === 'number') {
|
||||
element.placement.width = migratePositionValue(element.placement.width);
|
||||
}
|
||||
if (typeof element.placement.height === 'number') {
|
||||
element.placement.height = migratePositionValue(element.placement.height);
|
||||
}
|
||||
if (typeof element.placement.right === 'number') {
|
||||
element.placement.right = migratePositionValue(element.placement.right);
|
||||
}
|
||||
if (typeof element.placement.bottom === 'number') {
|
||||
element.placement.bottom = migratePositionValue(element.placement.bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return panel.options;
|
||||
};
|
||||
|
||||
@@ -34,17 +34,17 @@ composableKinds: PanelCfg: {
|
||||
vertical?: VerticalConstraint
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
Placement: {
|
||||
top?: float64
|
||||
left?: float64
|
||||
right?: float64
|
||||
bottom?: float64
|
||||
Placement: {
|
||||
top?: ui.PositionDimensionConfig
|
||||
left?: ui.PositionDimensionConfig
|
||||
right?: ui.PositionDimensionConfig
|
||||
bottom?: ui.PositionDimensionConfig
|
||||
|
||||
width?: float64
|
||||
height?: float64
|
||||
width?: ui.PositionDimensionConfig
|
||||
height?: ui.PositionDimensionConfig
|
||||
|
||||
rotation?: float64
|
||||
} @cuetsy(kind="interface")
|
||||
rotation?: ui.ScalarDimensionConfig
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
BackgroundImageSize: "original" | "contain" | "cover" | "fill" | "tile" @cuetsy(kind="enum", memberNames="Original|Contain|Cover|Fill|Tile")
|
||||
BackgroundConfig: {
|
||||
|
||||
+7
-7
@@ -32,13 +32,13 @@ export interface Constraint {
|
||||
}
|
||||
|
||||
export interface Placement {
|
||||
bottom?: number;
|
||||
height?: number;
|
||||
left?: number;
|
||||
right?: number;
|
||||
rotation?: number;
|
||||
top?: number;
|
||||
width?: number;
|
||||
bottom?: ui.PositionDimensionConfig;
|
||||
height?: ui.PositionDimensionConfig;
|
||||
left?: ui.PositionDimensionConfig;
|
||||
right?: ui.PositionDimensionConfig;
|
||||
rotation?: ui.ScalarDimensionConfig;
|
||||
top?: ui.PositionDimensionConfig;
|
||||
width?: ui.PositionDimensionConfig;
|
||||
}
|
||||
|
||||
export enum BackgroundImageSize {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isNumber, isString } from 'lodash';
|
||||
|
||||
import { DataFrame, Field, AppEvents, getFieldDisplayName, PluginState, SelectableValue } from '@grafana/data';
|
||||
import { ConnectionDirection } from '@grafana/schema';
|
||||
import { ConnectionDirection, PositionDimensionConfig, PositionDimensionMode } from '@grafana/schema';
|
||||
import { appEvents } from 'app/core/app_events';
|
||||
import { hasAlphaPanels, config } from 'app/core/config';
|
||||
import { CanvasConnection, CanvasElementItem, CanvasElementOptions } from 'app/features/canvas/element';
|
||||
@@ -15,6 +15,9 @@ import { AnchorPoint, ConnectionState, LineStyle, StrokeDasharray } from './type
|
||||
|
||||
export function doSelect(scene: Scene, element: ElementState | FrameState) {
|
||||
try {
|
||||
// Clear any previous field-driven selection
|
||||
scene.clearFieldDrivenSelection?.();
|
||||
|
||||
let selection: SelectionParams = { targets: [] };
|
||||
if (element instanceof FrameState) {
|
||||
const targetElements: HTMLDivElement[] = [];
|
||||
@@ -22,6 +25,14 @@ export function doSelect(scene: Scene, element: ElementState | FrameState) {
|
||||
selection.targets = targetElements;
|
||||
selection.frame = element;
|
||||
scene.select(selection);
|
||||
} else if (element.hasFieldDrivenPosition()) {
|
||||
// Field-driven elements can't be selected via Selecto, show custom selection
|
||||
scene.currentLayer = element.parent;
|
||||
scene.setFieldDrivenSelection(element);
|
||||
// Clear Selecto selection and broadcast this element as selected
|
||||
scene.selecto?.setSelectedTargets([]);
|
||||
scene.moveable!.target = [];
|
||||
scene.selection.next([element]);
|
||||
} else {
|
||||
scene.currentLayer = element.parent;
|
||||
selection.targets = [element?.div!];
|
||||
@@ -81,12 +92,30 @@ export function onAddItem(sel: SelectableValue<string>, rootLayer: FrameState |
|
||||
name: '',
|
||||
};
|
||||
|
||||
// Helper to create a fixed PositionDimensionConfig
|
||||
const fixedPosition = (value: number): PositionDimensionConfig => ({
|
||||
fixed: value,
|
||||
mode: PositionDimensionMode.Fixed,
|
||||
});
|
||||
|
||||
if (anchorPoint) {
|
||||
newElementOptions.placement = { ...newElementOptions.placement, top: anchorPoint.y, left: anchorPoint.x };
|
||||
newElementOptions.placement = {
|
||||
...newElementOptions.placement,
|
||||
top: fixedPosition(anchorPoint.y),
|
||||
left: fixedPosition(anchorPoint.x),
|
||||
};
|
||||
}
|
||||
|
||||
if (newItem.defaultSize) {
|
||||
newElementOptions.placement = { ...newElementOptions.placement, ...newItem.defaultSize };
|
||||
// defaultSize uses simple numbers, convert to PositionDimensionConfig
|
||||
const sizeConfig: Partial<typeof newElementOptions.placement> = {};
|
||||
if (newItem.defaultSize.width !== undefined) {
|
||||
sizeConfig.width = fixedPosition(newItem.defaultSize.width);
|
||||
}
|
||||
if (newItem.defaultSize.height !== undefined) {
|
||||
sizeConfig.height = fixedPosition(newItem.defaultSize.height);
|
||||
}
|
||||
newElementOptions.placement = { ...newElementOptions.placement, ...sizeConfig };
|
||||
}
|
||||
|
||||
if (rootLayer) {
|
||||
|
||||
Reference in New Issue
Block a user