Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Canvas: Add tooltip for data links #61648

Merged
merged 11 commits into from Jan 20, 2023
3 changes: 3 additions & 0 deletions public/app/features/canvas/elements/metricValue.tsx
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
},

Expand Down
3 changes: 3 additions & 0 deletions public/app/features/canvas/elements/text.tsx
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
},

Expand Down
43 changes: 42 additions & 1 deletion public/app/features/canvas/runtime/element.tsx
Expand Up @@ -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();
Expand All @@ -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}`}
Expand Down
14 changes: 13 additions & 1 deletion public/app/features/canvas/runtime/scene.tsx
Expand Up @@ -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';
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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}>
Expand All @@ -610,6 +617,11 @@ export class Scene {
<CanvasContextMenu scene={this} panel={this.panel} />
</Portal>
)}
{canShowElementTooltip && (
<Portal>
<CanvasTooltip scene={this} />
</Portal>
)}
</div>
);
}
Expand Down
2 changes: 2 additions & 0 deletions public/app/features/canvas/types.ts
@@ -1,3 +1,4 @@
import { LinkModel } from '@grafana/data/src';
import { ColorDimensionConfig, ResourceDimensionConfig, TextDimensionConfig } from 'app/features/dimensions/types';

export interface Placement {
Expand Down Expand Up @@ -77,6 +78,7 @@ export interface TextData {
size?: number; // 0 or missing will "auto size"
align: Align;
valign: VAlign;
links?: LinkModel[];
}

export interface TextConfig {
Expand Down
3 changes: 3 additions & 0 deletions public/app/features/dimensions/context.ts
@@ -1,3 +1,5 @@
import { PanelData } from '@grafana/data/src';

import {
ColorDimensionConfig,
DimensionSupplier,
Expand All @@ -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;
}
8 changes: 7 additions & 1 deletion public/app/plugins/panel/canvas/CanvasPanel.tsx
Expand Up @@ -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> {}

Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -230,6 +231,11 @@ export class CanvasPanel extends Component<Props, State> {
isSetBackgroundOpen = true;
};

tooltipCallback = (tooltip: CanvasTooltipPayload | undefined) => {
this.scene.tooltip = tooltip;
this.forceUpdate();
adela-almasan marked this conversation as resolved.
Show resolved Hide resolved
};

closeInlineEdit = () => {
this.setState({ openInlineEdit: false });
isInlineEditOpen = false;
Expand Down
80 changes: 80 additions & 0 deletions public/app/plugins/panel/canvas/CanvasTooltip.tsx
@@ -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};
`,
});
6 changes: 6 additions & 0 deletions public/app/plugins/panel/canvas/types.ts
Expand Up @@ -25,3 +25,9 @@ export type AnchorPoint = {
x: number;
y: number;
};

export interface CanvasTooltipPayload {
anchorPoint: AnchorPoint | undefined;
element: ElementState | undefined;
isOpen?: boolean;
}
32 changes: 31 additions & 1 deletion public/app/plugins/panel/canvas/utils.ts
@@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -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;
}
1 change: 1 addition & 0 deletions public/app/plugins/panel/icon/IconPanel.tsx
Expand Up @@ -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) {
Expand Down