mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Canvas: Add tooltip for data links (#61648)
This commit is contained in:
parent
c1251311d0
commit
cef15873d6
@ -11,6 +11,7 @@ import { DimensionContext } from 'app/features/dimensions/context';
|
||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||
import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor';
|
||||
|
||||
import { getDataLinks } from '../../../plugins/panel/canvas/utils';
|
||||
import { CanvasElementItem, CanvasElementProps, defaultBgColor, defaultTextColor } from '../element';
|
||||
import { ElementState } from '../runtime/element';
|
||||
import { Align, TextConfig, TextData, VAlign } from '../types';
|
||||
@ -159,6 +160,8 @@ export const metricValueItem: CanvasElementItem<TextConfig, TextData> = {
|
||||
data.color = ctx.getColor(cfg.color).value();
|
||||
}
|
||||
|
||||
data.links = getDataLinks(ctx, cfg, data.text);
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
|
@ -9,6 +9,7 @@ import { DimensionContext } from 'app/features/dimensions/context';
|
||||
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
|
||||
import { TextDimensionEditor } from 'app/features/dimensions/editors/TextDimensionEditor';
|
||||
|
||||
import { getDataLinks } from '../../../plugins/panel/canvas/utils';
|
||||
import { CanvasElementItem, CanvasElementProps, defaultThemeTextColor } from '../element';
|
||||
import { ElementState } from '../runtime/element';
|
||||
import { Align, TextConfig, TextData, VAlign } from '../types';
|
||||
@ -158,6 +159,8 @@ export const textItem: CanvasElementItem<TextConfig, TextData> = {
|
||||
data.color = ctx.getColor(cfg.color).value();
|
||||
}
|
||||
|
||||
data.links = getDataLinks(ctx, cfg, data.text);
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
|
@ -446,6 +446,45 @@ export class ElementState implements LayerElement {
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseEnter = (event: React.MouseEvent, isSelected: boolean | undefined) => {
|
||||
const scene = this.getScene();
|
||||
if (!scene?.isEditingEnabled) {
|
||||
this.handleTooltip(event);
|
||||
} else if (!isSelected) {
|
||||
scene?.connections.handleMouseEnter(event);
|
||||
}
|
||||
};
|
||||
|
||||
handleTooltip = (event: React.MouseEvent) => {
|
||||
const scene = this.getScene();
|
||||
if (scene?.tooltipCallback) {
|
||||
const rect = this.div?.getBoundingClientRect();
|
||||
scene.tooltipCallback({
|
||||
anchorPoint: { x: rect?.right ?? event.pageX, y: rect?.top ?? event.pageY },
|
||||
element: this,
|
||||
isOpen: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseLeave = (event: React.MouseEvent) => {
|
||||
const scene = this.getScene();
|
||||
if (scene?.tooltipCallback && !scene?.tooltip?.isOpen) {
|
||||
scene.tooltipCallback(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
onElementClick = (event: React.MouseEvent) => {
|
||||
const scene = this.getScene();
|
||||
if (scene?.tooltipCallback && scene.tooltip?.anchorPoint) {
|
||||
scene.tooltipCallback({
|
||||
anchorPoint: { x: scene.tooltip.anchorPoint.x, y: scene.tooltip.anchorPoint.y },
|
||||
element: this,
|
||||
isOpen: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { item, div } = this;
|
||||
const scene = this.getScene();
|
||||
@ -456,7 +495,9 @@ export class ElementState implements LayerElement {
|
||||
<div
|
||||
key={this.UID}
|
||||
ref={this.initElement}
|
||||
onMouseEnter={!isSelected ? scene?.connections.handleMouseEnter : undefined}
|
||||
onMouseEnter={(e: React.MouseEvent) => this.handleMouseEnter(e, isSelected)}
|
||||
onMouseLeave={!scene?.isEditingEnabled ? this.handleMouseLeave : undefined}
|
||||
onClick={!scene?.isEditingEnabled ? this.onElementClick : undefined}
|
||||
>
|
||||
<item.display
|
||||
key={`${this.UID}/${this.revId}`}
|
||||
|
@ -26,9 +26,10 @@ import {
|
||||
getTextDimensionFromData,
|
||||
} from 'app/features/dimensions/utils';
|
||||
import { CanvasContextMenu } from 'app/plugins/panel/canvas/CanvasContextMenu';
|
||||
import { CanvasTooltip } from 'app/plugins/panel/canvas/CanvasTooltip';
|
||||
import { CONNECTION_ANCHOR_DIV_ID } from 'app/plugins/panel/canvas/ConnectionAnchors';
|
||||
import { Connections } from 'app/plugins/panel/canvas/Connections';
|
||||
import { AnchorPoint, LayerActionID } from 'app/plugins/panel/canvas/types';
|
||||
import { AnchorPoint, CanvasTooltipPayload, LayerActionID } from 'app/plugins/panel/canvas/types';
|
||||
|
||||
import appEvents from '../../../core/app_events';
|
||||
import { CanvasPanel } from '../../../plugins/panel/canvas/CanvasPanel';
|
||||
@ -74,6 +75,9 @@ export class Scene {
|
||||
inlineEditingCallback?: () => void;
|
||||
setBackgroundCallback?: (anchorPoint: AnchorPoint) => void;
|
||||
|
||||
tooltipCallback?: (tooltip: CanvasTooltipPayload | undefined) => void;
|
||||
tooltip?: CanvasTooltipPayload;
|
||||
|
||||
readonly editModeEnabled = new BehaviorSubject<boolean>(false);
|
||||
subscription: Subscription;
|
||||
|
||||
@ -149,6 +153,7 @@ export class Scene {
|
||||
getScalar: (scalar: ScalarDimensionConfig) => getScalarDimensionFromData(this.data, scalar),
|
||||
getText: (text: TextDimensionConfig) => getTextDimensionFromData(this.data, text),
|
||||
getResource: (res: ResourceDimensionConfig) => getResourceDimensionFromData(this.data, res),
|
||||
getPanelData: () => this.data,
|
||||
};
|
||||
|
||||
updateData(data: PanelData) {
|
||||
@ -600,6 +605,8 @@ export class Scene {
|
||||
|
||||
render() {
|
||||
const canShowContextMenu = this.isPanelEditing || (!this.isPanelEditing && this.isEditingEnabled);
|
||||
const canShowElementTooltip =
|
||||
!this.isEditingEnabled && this.tooltip?.element && this.tooltip.element.data.links?.length > 0;
|
||||
|
||||
return (
|
||||
<div key={this.revId} className={this.styles.wrap} style={this.style} ref={this.setRef}>
|
||||
@ -610,6 +617,11 @@ export class Scene {
|
||||
<CanvasContextMenu scene={this} panel={this.panel} />
|
||||
</Portal>
|
||||
)}
|
||||
{canShowElementTooltip && (
|
||||
<Portal>
|
||||
<CanvasTooltip scene={this} />
|
||||
</Portal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { LinkModel } from '@grafana/data/src';
|
||||
import { ColorDimensionConfig, ResourceDimensionConfig, TextDimensionConfig } from 'app/features/dimensions/types';
|
||||
|
||||
export interface Placement {
|
||||
@ -77,6 +78,7 @@ export interface TextData {
|
||||
size?: number; // 0 or missing will "auto size"
|
||||
align: Align;
|
||||
valign: VAlign;
|
||||
links?: LinkModel[];
|
||||
}
|
||||
|
||||
export interface TextConfig {
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { PanelData } from '@grafana/data/src';
|
||||
|
||||
import {
|
||||
ColorDimensionConfig,
|
||||
DimensionSupplier,
|
||||
@ -13,4 +15,5 @@ export interface DimensionContext {
|
||||
getScalar(scalar: ScalarDimensionConfig): DimensionSupplier<number>;
|
||||
getText(text: TextDimensionConfig): DimensionSupplier<string>;
|
||||
getResource(resource: ResourceDimensionConfig): DimensionSupplier<string>;
|
||||
getPanelData(): PanelData | undefined;
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events';
|
||||
import { InlineEdit } from './InlineEdit';
|
||||
import { SetBackground } from './SetBackground';
|
||||
import { PanelOptions } from './models.gen';
|
||||
import { AnchorPoint } from './types';
|
||||
import { AnchorPoint, CanvasTooltipPayload } from './types';
|
||||
|
||||
interface Props extends PanelProps<PanelOptions> {}
|
||||
|
||||
@ -71,6 +71,7 @@ export class CanvasPanel extends Component<Props, State> {
|
||||
this.scene.updateData(props.data);
|
||||
this.scene.inlineEditingCallback = this.openInlineEdit;
|
||||
this.scene.setBackgroundCallback = this.openSetBackground;
|
||||
this.scene.tooltipCallback = this.tooltipCallback;
|
||||
|
||||
this.subs.add(
|
||||
this.props.eventBus.subscribe(PanelEditEnteredEvent, (evt: PanelEditEnteredEvent) => {
|
||||
@ -230,6 +231,11 @@ export class CanvasPanel extends Component<Props, State> {
|
||||
isSetBackgroundOpen = true;
|
||||
};
|
||||
|
||||
tooltipCallback = (tooltip: CanvasTooltipPayload | undefined) => {
|
||||
this.scene.tooltip = tooltip;
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
closeInlineEdit = () => {
|
||||
this.setState({ openInlineEdit: false });
|
||||
isInlineEditOpen = false;
|
||||
|
80
public/app/plugins/panel/canvas/CanvasTooltip.tsx
Normal file
80
public/app/plugins/panel/canvas/CanvasTooltip.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useDialog } from '@react-aria/dialog';
|
||||
import { useOverlay } from '@react-aria/overlays';
|
||||
import React, { createRef } from 'react';
|
||||
|
||||
import { GrafanaTheme2, LinkModel } from '@grafana/data/src';
|
||||
import { LinkButton, Portal, useStyles2, VerticalGroup, VizTooltipContainer } from '@grafana/ui';
|
||||
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
|
||||
import { Scene } from 'app/features/canvas/runtime/scene';
|
||||
|
||||
interface Props {
|
||||
scene: Scene;
|
||||
}
|
||||
|
||||
export const CanvasTooltip = ({ scene }: Props) => {
|
||||
const style = useStyles2(getStyles);
|
||||
|
||||
const onClose = () => {
|
||||
if (scene?.tooltipCallback && scene.tooltip) {
|
||||
scene.tooltipCallback(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const ref = createRef<HTMLElement>();
|
||||
const { overlayProps } = useOverlay({ onClose: onClose, isDismissable: true }, ref);
|
||||
const { dialogProps } = useDialog({}, ref);
|
||||
|
||||
const element = scene.tooltip?.element;
|
||||
if (!element) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const renderDataLinks = () =>
|
||||
element.data?.links &&
|
||||
element.data?.links.length > 0 && (
|
||||
<div>
|
||||
<VerticalGroup>
|
||||
{element.data?.links?.map((link: LinkModel, i: number) => (
|
||||
<LinkButton
|
||||
key={i}
|
||||
icon={'external-link-alt'}
|
||||
target={link.target}
|
||||
href={link.href}
|
||||
onClick={link.onClick}
|
||||
fill="text"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{link.title}
|
||||
</LinkButton>
|
||||
))}
|
||||
</VerticalGroup>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{scene.tooltip?.element && scene.tooltip.anchorPoint && (
|
||||
<Portal>
|
||||
<VizTooltipContainer
|
||||
position={{ x: scene.tooltip.anchorPoint.x, y: scene.tooltip.anchorPoint.y }}
|
||||
offset={{ x: 5, y: 0 }}
|
||||
allowPointerEvents={scene.tooltip.isOpen}
|
||||
>
|
||||
<section ref={ref} {...overlayProps} {...dialogProps}>
|
||||
{scene.tooltip.isOpen && <CloseButton style={{ zIndex: 1 }} onClick={onClose} />}
|
||||
<div className={style.wrapper}>{renderDataLinks()}</div>
|
||||
</section>
|
||||
</VizTooltipContainer>
|
||||
</Portal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
wrapper: css`
|
||||
margin-top: 20px;
|
||||
background: ${theme.colors.background.primary};
|
||||
`,
|
||||
});
|
@ -25,3 +25,9 @@ export type AnchorPoint = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
export interface CanvasTooltipPayload {
|
||||
anchorPoint: AnchorPoint | undefined;
|
||||
element: ElementState | undefined;
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AppEvents, PluginState, SelectableValue } from '@grafana/data';
|
||||
import { AppEvents, Field, LinkModel, PluginState, SelectableValue } from '@grafana/data';
|
||||
import { hasAlphaPanels } from 'app/core/config';
|
||||
|
||||
import appEvents from '../../../core/app_events';
|
||||
@ -8,11 +8,13 @@ import {
|
||||
CanvasElementOptions,
|
||||
canvasElementRegistry,
|
||||
defaultElementItems,
|
||||
TextConfig,
|
||||
} from '../../../features/canvas';
|
||||
import { notFoundItem } from '../../../features/canvas/elements/notFound';
|
||||
import { ElementState } from '../../../features/canvas/runtime/element';
|
||||
import { FrameState } from '../../../features/canvas/runtime/frame';
|
||||
import { Scene, SelectionParams } from '../../../features/canvas/runtime/scene';
|
||||
import { DimensionContext } from '../../../features/dimensions';
|
||||
|
||||
import { AnchorPoint } from './types';
|
||||
|
||||
@ -99,3 +101,31 @@ export function onAddItem(sel: SelectableValue<string>, rootLayer: FrameState |
|
||||
setTimeout(() => doSelect(rootLayer.scene, newElement));
|
||||
}
|
||||
}
|
||||
|
||||
export function getDataLinks(ctx: DimensionContext, cfg: TextConfig, textData: string | undefined): LinkModel[] {
|
||||
const panelData = ctx.getPanelData();
|
||||
const frames = panelData?.series;
|
||||
|
||||
const links: Array<LinkModel<Field>> = [];
|
||||
const linkLookup = new Set<string>();
|
||||
|
||||
frames?.forEach((frame) => {
|
||||
const visibleFields = frame.fields.filter((field) => !Boolean(field.config.custom?.hideFrom?.tooltip));
|
||||
|
||||
if (cfg.text?.field && visibleFields.some((f) => f.name === cfg.text?.field)) {
|
||||
const field = visibleFields.filter((field) => field.name === cfg.text?.field)[0];
|
||||
if (field?.getLinks) {
|
||||
const disp = field.display ? field.display(textData) : { text: `${textData}`, numeric: +textData! };
|
||||
field.getLinks({ calculatedValue: disp }).forEach((link) => {
|
||||
const key = `${link.title}/${link.href}`;
|
||||
if (!linkLookup.has(key)) {
|
||||
links.push(link);
|
||||
linkLookup.add(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return links;
|
||||
}
|
||||
|
@ -57,6 +57,7 @@ export class IconPanel extends Component<Props> {
|
||||
getScalar: (scalar: ScalarDimensionConfig) => getScalarDimensionFromData(this.props.data, scalar),
|
||||
getText: (text: TextDimensionConfig) => getTextDimensionFromData(this.props.data, text),
|
||||
getResource: (res: ResourceDimensionConfig) => getResourceDimensionFromData(this.props.data, res),
|
||||
getPanelData: () => this.props.data,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps: Props) {
|
||||
|
Loading…
Reference in New Issue
Block a user