diff --git a/packages/grafana-ui/src/components/Icon/iconBundle.ts b/packages/grafana-ui/src/components/Icon/iconBundle.ts index 8bea9dba083..67737b810a5 100644 --- a/packages/grafana-ui/src/components/Icon/iconBundle.ts +++ b/packages/grafana-ui/src/components/Icon/iconBundle.ts @@ -94,6 +94,9 @@ import u1067 from '!!raw-loader!../../../../../public/img/icons/unicons/forward. import u1068 from '!!raw-loader!../../../../../public/img/icons/unicons/graph-bar.svg'; import u1069 from '!!raw-loader!../../../../../public/img/icons/unicons/history.svg'; import u1070 from '!!raw-loader!../../../../../public/img/icons/unicons/home-alt.svg'; +import u1158 from '!!raw-loader!../../../../../public/img/icons/unicons/horizontal-align-center.svg'; +import u1156 from '!!raw-loader!../../../../../public/img/icons/unicons/horizontal-align-left.svg'; +import u1157 from '!!raw-loader!../../../../../public/img/icons/unicons/horizontal-align-right.svg'; import u1126 from '!!raw-loader!../../../../../public/img/icons/unicons/hourglass.svg'; import u1071 from '!!raw-loader!../../../../../public/img/icons/unicons/import.svg'; import u1073 from '!!raw-loader!../../../../../public/img/icons/unicons/info-circle.svg'; @@ -154,6 +157,9 @@ import u1117 from '!!raw-loader!../../../../../public/img/icons/unicons/unlock.s import u1118 from '!!raw-loader!../../../../../public/img/icons/unicons/upload.svg'; import u1119 from '!!raw-loader!../../../../../public/img/icons/unicons/user.svg'; import u1120 from '!!raw-loader!../../../../../public/img/icons/unicons/users-alt.svg'; +import u1160 from '!!raw-loader!../../../../../public/img/icons/unicons/vertical-align-bottom.svg'; +import u1161 from '!!raw-loader!../../../../../public/img/icons/unicons/vertical-align-center.svg'; +import u1159 from '!!raw-loader!../../../../../public/img/icons/unicons/vertical-align-top.svg'; import u1121 from '!!raw-loader!../../../../../public/img/icons/unicons/wrap-text.svg'; import u1135 from '!!raw-loader!../../../../../public/img/icons/unicons/x.svg'; import { cacheStore } from 'react-inlinesvg'; @@ -329,4 +335,10 @@ export function initIconCache() { cacheItem(u1153, 'mono/panel-add.svg'); cacheItem(u1154, 'mono/library-panel.svg'); cacheItem(u1155, 'unicons/record-audio.svg'); + cacheItem(u1156, 'unicons/horizontal-align-left.svg'); + cacheItem(u1157, 'unicons/horizontal-align-right.svg'); + cacheItem(u1158, 'unicons/horizontal-align-center.svg'); + cacheItem(u1159, 'unicons/vertical-align-top.svg'); + cacheItem(u1160, 'unicons/vertical-align-bottom.svg'); + cacheItem(u1161, 'unicons/vertical-align-center.svg'); } diff --git a/packages/grafana-ui/src/types/icon.ts b/packages/grafana-ui/src/types/icon.ts index 0aee8ac145e..5e179b851f5 100644 --- a/packages/grafana-ui/src/types/icon.ts +++ b/packages/grafana-ui/src/types/icon.ts @@ -1,6 +1,7 @@ import { Field, FieldType } from '@grafana/data'; import { ComponentSize } from './size'; + export type IconType = 'mono' | 'default' | 'solid'; export type IconSize = ComponentSize | 'xl' | 'xxl' | 'xxxl'; @@ -106,6 +107,9 @@ export const getAvailableIcons = () => 'heart-break', 'history', 'home-alt', + 'horizontal-align-center', + 'horizontal-align-left', + 'horizontal-align-right', 'hourglass', 'import', 'info', @@ -171,6 +175,9 @@ export const getAvailableIcons = () => 'upload', 'user', 'users-alt', + 'vertical-align-bottom', + 'vertical-align-center', + 'vertical-align-top', 'wrap-text', 'x', ] as const; diff --git a/public/app/features/canvas/types.ts b/public/app/features/canvas/types.ts index 54055e1be1f..24185c5448d 100644 --- a/public/app/features/canvas/types.ts +++ b/public/app/features/canvas/types.ts @@ -49,3 +49,12 @@ export interface LineConfig { color?: ColorDimensionConfig; width?: number; } + +export enum QuickPlacement { + Top = 'top', + Bottom = 'bottom', + Left = 'left', + Right = 'right', + HorizontalCenter = 'hcenter', + VerticalCenter = 'vcenter', +} diff --git a/public/app/plugins/panel/canvas/editor/PlacementEditor.tsx b/public/app/plugins/panel/canvas/editor/PlacementEditor.tsx index 7916ef11743..8dd44107164 100644 --- a/public/app/plugins/panel/canvas/editor/PlacementEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/PlacementEditor.tsx @@ -10,6 +10,7 @@ import { NumberInput } from 'app/features/dimensions/editors/NumberInput'; import { PanelOptions } from '../models.gen'; import { ConstraintSelectionBox } from './ConstraintSelectionBox'; +import { QuickPositioning } from './QuickPositioning'; import { CanvasEditorOptions } from './elementEditor'; const places: Array = ['top', 'left', 'bottom', 'right', 'width', 'height']; @@ -72,26 +73,35 @@ export const PlacementEditor: FC { element.options.placement![placement] = value ?? element.options.placement![placement]; element.applyLayoutStylesToDiv(); - settings.scene.clearCurrentSelection(); + settings.scene.clearCurrentSelection(true); + // TODO: This needs to have a better sync method with where div is + setTimeout(() => { + settings.scene.select({ targets: [element.div!] }); + }, 100); }; return (
- - - - +
+ + + -
-
+ + + + + +
diff --git a/public/app/plugins/panel/canvas/editor/QuickPositioning.tsx b/public/app/plugins/panel/canvas/editor/QuickPositioning.tsx new file mode 100644 index 00000000000..67c38976a40 --- /dev/null +++ b/public/app/plugins/panel/canvas/editor/QuickPositioning.tsx @@ -0,0 +1,122 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data/src'; +import { IconButton, useStyles2 } from '@grafana/ui/src'; +import { HorizontalConstraint, Placement, QuickPlacement, VerticalConstraint } from 'app/features/canvas'; +import { ElementState } from 'app/features/canvas/runtime/element'; + +import { CanvasEditorOptions } from './elementEditor'; + +type Props = { + onPositionChange: (value: number | undefined, placement: keyof Placement) => void; + element: ElementState; + settings: CanvasEditorOptions; +}; + +export const QuickPositioning = ({ onPositionChange, element, settings }: Props) => { + const styles = useStyles2(getStyles); + + const onQuickPositioningChange = (position: QuickPlacement) => { + const defaultConstraint = { vertical: VerticalConstraint.Top, horizontal: HorizontalConstraint.Left }; + const originalConstraint = { ...element.options.constraint }; + + element.options.constraint = defaultConstraint; + element.setPlacementFromConstraint(); + + switch (position) { + case QuickPlacement.Top: + onPositionChange(0, 'top'); + break; + case QuickPlacement.Bottom: + onPositionChange(getRightBottomPosition(element.options.placement?.height ?? 0, 'bottom'), 'top'); + break; + case QuickPlacement.VerticalCenter: + onPositionChange(getCenterPosition(element.options.placement?.height ?? 0, 'v'), 'top'); + break; + case QuickPlacement.Left: + onPositionChange(0, 'left'); + break; + case QuickPlacement.Right: + onPositionChange(getRightBottomPosition(element.options.placement?.width ?? 0, 'right'), 'left'); + break; + case QuickPlacement.HorizontalCenter: + onPositionChange(getCenterPosition(element.options.placement?.width ?? 0, 'h'), 'left'); + break; + } + + element.options.constraint = originalConstraint; + element.setPlacementFromConstraint(); + }; + + // Basing this on scene will mean that center is based on root for the time being + const getCenterPosition = (elementSize: number, align: 'h' | 'v') => { + const sceneSize = align === 'h' ? settings.scene.width : settings.scene.height; + + return (sceneSize - elementSize) / 2; + }; + + const getRightBottomPosition = (elementSize: number, align: 'right' | 'bottom') => { + const sceneSize = align === 'right' ? settings.scene.width : settings.scene.height; + + return sceneSize - elementSize; + }; + + return ( +
+ onQuickPositioningChange(QuickPlacement.Left)} + className={styles.button} + size={'lg'} + tooltip={'Align left'} + /> + onQuickPositioningChange(QuickPlacement.HorizontalCenter)} + className={styles.button} + size={'lg'} + tooltip={'Align horizontal centers'} + /> + onQuickPositioningChange(QuickPlacement.Right)} + className={styles.button} + size={'lg'} + tooltip={'Align right'} + /> + onQuickPositioningChange(QuickPlacement.Top)} + size={'lg'} + tooltip={'Align top'} + /> + onQuickPositioningChange(QuickPlacement.VerticalCenter)} + className={styles.button} + size={'lg'} + tooltip={'Align vertical centers'} + /> + onQuickPositioningChange(QuickPlacement.Bottom)} + className={styles.button} + size={'lg'} + tooltip={'Align bottom'} + /> +
+ ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + buttonGroup: css` + display: flex; + flex-wrap: wrap; + padding: 12px 0 12px 0; + `, + button: css` + margin-left: 5px; + margin-right: 5px; + `, +}); diff --git a/public/app/plugins/panel/canvas/editor/elementEditor.tsx b/public/app/plugins/panel/canvas/editor/elementEditor.tsx index e0b75016b7b..3403bf463a8 100644 --- a/public/app/plugins/panel/canvas/editor/elementEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/elementEditor.tsx @@ -86,7 +86,7 @@ export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions< category: ['Layout'], id: 'content', path: '__', // not used - name: 'Constraints', + name: 'Quick placement', editor: PlacementEditor, settings: opts, });