mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
[grafana/UI] Hoovering over a legend label highlights the corresponding pie slice (#32941)
* Hoovering over a legend label hightlighs that pie slice * Change to event bus * Adds EventBusWithSource to help identify the origin of the event * Add tests and fix bug with incorrect source * Clean up PieChart and EventBus a bit * Fix bug when payload.source is undefined * Add some documentation and adjust naming * useState instead of useSetState * Clean up some more documentation * Move eventbus to state * add event bus actions to the debug panel * add event bus actions to the debug panel * Try to make the naming a bit clearer * Try passing eventbus as context * Fix lint issues * Move event bus context to panel chrome * Fix event handler functions * switch to using useCallback for legend item callbacks * Remove unused parameters * Add id to panel fixture of PanelChrome test * Simplify event source * Place eventBus inside more generic context * Push handling of context up the tree to VizLegend only export usePanelContext and PanelContextProvider implement isOwnEvent on EventBus some cleanup Co-authored-by: Ryan McKinley <ryantxu@gmail.com> Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
parent
e21d1ccba4
commit
d0239ac958
@ -1,6 +1,7 @@
|
|||||||
import { EventBusSrv } from './EventBus';
|
import { EventBusSrv, EventBusWithSource } from './EventBus';
|
||||||
import { BusEventWithPayload } from './types';
|
import { BusEvent, BusEventWithPayload } from './types';
|
||||||
import { eventFactory } from './eventFactory';
|
import { eventFactory } from './eventFactory';
|
||||||
|
import { DataHoverEvent } from './common';
|
||||||
|
|
||||||
interface LoginEventPayload {
|
interface LoginEventPayload {
|
||||||
logins: number;
|
logins: number;
|
||||||
@ -46,6 +47,27 @@ describe('EventBus', () => {
|
|||||||
expect(events.length).toBe(1);
|
expect(events.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('EventBusWithSource', () => {
|
||||||
|
it('can add sources to the source path', () => {
|
||||||
|
const bus = new EventBusSrv();
|
||||||
|
const busWithSource = new EventBusWithSource(bus, 'foo');
|
||||||
|
expect(busWithSource.source).toEqual('foo');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds the source to the event payload', () => {
|
||||||
|
const bus = new EventBusSrv();
|
||||||
|
let events: BusEvent[] = [];
|
||||||
|
|
||||||
|
bus.subscribe(DataHoverEvent, (event) => events.push(event));
|
||||||
|
|
||||||
|
const busWithSource = new EventBusWithSource(bus, 'foo');
|
||||||
|
busWithSource.publish({ type: DataHoverEvent.type });
|
||||||
|
|
||||||
|
expect(events.length).toEqual(1);
|
||||||
|
expect(events[0].payload.source).toEqual('foo');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('Legacy emitter behavior', () => {
|
describe('Legacy emitter behavior', () => {
|
||||||
it('Supports legacy events', () => {
|
it('Supports legacy events', () => {
|
||||||
const bus = new EventBusSrv();
|
const bus = new EventBusSrv();
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import EventEmitter from 'eventemitter3';
|
import EventEmitter from 'eventemitter3';
|
||||||
import { Unsubscribable, Observable } from 'rxjs';
|
import { Unsubscribable, Observable } from 'rxjs';
|
||||||
|
import { PayloadWithSource } from './common';
|
||||||
import {
|
import {
|
||||||
EventBus,
|
EventBus,
|
||||||
LegacyEmitter,
|
LegacyEmitter,
|
||||||
@ -8,6 +9,7 @@ import {
|
|||||||
LegacyEventHandler,
|
LegacyEventHandler,
|
||||||
BusEvent,
|
BusEvent,
|
||||||
AppEvent,
|
AppEvent,
|
||||||
|
BusEventWithPayload,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,3 +92,42 @@ export class EventBusSrv implements EventBus, LegacyEmitter {
|
|||||||
this.emitter.removeAllListeners();
|
this.emitter.removeAllListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alpha
|
||||||
|
*
|
||||||
|
* Wraps EventBus and adds a source to help with identifying if a subscriber should react to the event or not.
|
||||||
|
*/
|
||||||
|
export class EventBusWithSource implements EventBus {
|
||||||
|
source: string;
|
||||||
|
eventBus: EventBus;
|
||||||
|
|
||||||
|
constructor(eventBus: EventBus, source: string) {
|
||||||
|
this.eventBus = eventBus;
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
publish<T extends BusEvent>(event: T): void {
|
||||||
|
const decoratedEvent = {
|
||||||
|
...event,
|
||||||
|
...{ payload: { ...event.payload, ...{ source: this.source } } },
|
||||||
|
};
|
||||||
|
this.eventBus.publish(decoratedEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe<T extends BusEvent>(eventType: BusEventType<T>, handler: BusEventHandler<T>): Unsubscribable {
|
||||||
|
return this.eventBus.subscribe(eventType, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
getStream<T extends BusEvent>(eventType: BusEventType<T>): Observable<T> {
|
||||||
|
return this.eventBus.getStream(eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAllListeners(): void {
|
||||||
|
this.eventBus.removeAllListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
isOwnEvent(event: BusEventWithPayload<PayloadWithSource>): boolean {
|
||||||
|
return event.payload.source === this.source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
38
packages/grafana-data/src/events/common.ts
Normal file
38
packages/grafana-data/src/events/common.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { DataFrame } from '../types';
|
||||||
|
import { BusEventWithPayload } from './types';
|
||||||
|
|
||||||
|
/** @alpha */
|
||||||
|
export interface PayloadWithSource {
|
||||||
|
source?: string; // source from where the event originates
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @alpha */
|
||||||
|
export interface DataHoverPayload extends PayloadWithSource {
|
||||||
|
raw: any; // Original mouse event (includes pageX etc)
|
||||||
|
|
||||||
|
x: Record<string, any>; // { time: 5678 },
|
||||||
|
y: Record<string, any>; // { __fixed: 123, lengthft: 456 } // each axis|scale gets a value
|
||||||
|
|
||||||
|
data?: DataFrame; // source data
|
||||||
|
rowIndex?: number; // the hover row
|
||||||
|
columnIndex?: number; // the hover column
|
||||||
|
dataId?: string; // identifying string to correlate data between publishers and subscribers
|
||||||
|
|
||||||
|
// When dragging, this will capture the original state
|
||||||
|
down?: Omit<DataHoverPayload, 'down'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @alpha */
|
||||||
|
export class DataHoverEvent extends BusEventWithPayload<DataHoverPayload> {
|
||||||
|
static type = 'data-hover';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @alpha */
|
||||||
|
export class DataHoverClearEvent extends BusEventWithPayload<DataHoverPayload> {
|
||||||
|
static type = 'data-hover-clear';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @alpha */
|
||||||
|
export class DataSelectEvent extends BusEventWithPayload<DataHoverPayload> {
|
||||||
|
static type = 'data-select';
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
export * from './eventFactory';
|
export * from './eventFactory';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './EventBus';
|
export * from './EventBus';
|
||||||
|
export * from './common';
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
import { EventBusWithSource } from '@grafana/data';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface PanelContext {
|
||||||
|
eventBus?: EventBusWithSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PanelContextRoot = React.createContext<PanelContext>({});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alpha
|
||||||
|
*/
|
||||||
|
export const PanelContextProvider = PanelContextRoot.Provider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alpha
|
||||||
|
*/
|
||||||
|
export const usePanelContext = () => React.useContext(PanelContextRoot);
|
@ -36,3 +36,5 @@ export {
|
|||||||
ErrorIndicator as PanelChromeErrorIndicator,
|
ErrorIndicator as PanelChromeErrorIndicator,
|
||||||
ErrorIndicatorProps as PanelChromeErrorIndicatorProps,
|
ErrorIndicatorProps as PanelChromeErrorIndicatorProps,
|
||||||
} from './ErrorIndicator';
|
} from './ErrorIndicator';
|
||||||
|
|
||||||
|
export { usePanelContext, PanelContextProvider } from './PanelContext';
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import React, { FC, ReactNode } from 'react';
|
import React, { FC, ReactNode, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
|
DataHoverClearEvent,
|
||||||
|
DataHoverEvent,
|
||||||
FALLBACK_COLOR,
|
FALLBACK_COLOR,
|
||||||
FieldDisplay,
|
FieldDisplay,
|
||||||
formattedValueToString,
|
formattedValueToString,
|
||||||
@ -30,6 +32,7 @@ import {
|
|||||||
} from './types';
|
} from './types';
|
||||||
import { getTooltipContainerStyles } from '../../themes/mixins';
|
import { getTooltipContainerStyles } from '../../themes/mixins';
|
||||||
import { SeriesTable, SeriesTableRowProps, VizTooltipOptions } from '../VizTooltip';
|
import { SeriesTable, SeriesTableRowProps, VizTooltipOptions } from '../VizTooltip';
|
||||||
|
import { usePanelContext } from '../PanelChrome';
|
||||||
|
|
||||||
const defaultLegendOptions: PieChartLegendOptions = {
|
const defaultLegendOptions: PieChartLegendOptions = {
|
||||||
displayMode: LegendDisplayMode.List,
|
displayMode: LegendDisplayMode.List,
|
||||||
@ -38,6 +41,9 @@ const defaultLegendOptions: PieChartLegendOptions = {
|
|||||||
values: [PieChartLegendValues.Percent],
|
values: [PieChartLegendValues.Percent],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @beta
|
||||||
|
*/
|
||||||
export const PieChart: FC<PieChartProps> = ({
|
export const PieChart: FC<PieChartProps> = ({
|
||||||
data,
|
data,
|
||||||
timeZone,
|
timeZone,
|
||||||
@ -52,6 +58,25 @@ export const PieChart: FC<PieChartProps> = ({
|
|||||||
...restProps
|
...restProps
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const [highlightedTitle, setHighlightedTitle] = useState<string>();
|
||||||
|
const { eventBus } = usePanelContext();
|
||||||
|
|
||||||
|
if (eventBus) {
|
||||||
|
const setHighlightedSlice = (event: DataHoverEvent) => {
|
||||||
|
if (eventBus.isOwnEvent(event)) {
|
||||||
|
setHighlightedTitle(event.payload.dataId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetHighlightedSlice = (event: DataHoverClearEvent) => {
|
||||||
|
if (eventBus.isOwnEvent(event)) {
|
||||||
|
setHighlightedTitle(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
eventBus.subscribe(DataHoverEvent, setHighlightedSlice);
|
||||||
|
eventBus.subscribe(DataHoverClearEvent, resetHighlightedSlice);
|
||||||
|
}
|
||||||
|
|
||||||
const getLegend = (fields: FieldDisplay[], legendOptions: PieChartLegendOptions) => {
|
const getLegend = (fields: FieldDisplay[], legendOptions: PieChartLegendOptions) => {
|
||||||
if (legendOptions.displayMode === LegendDisplayMode.Hidden) {
|
if (legendOptions.displayMode === LegendDisplayMode.Hidden) {
|
||||||
@ -117,6 +142,7 @@ export const PieChart: FC<PieChartProps> = ({
|
|||||||
<PieChartSvg
|
<PieChartSvg
|
||||||
width={vizWidth}
|
width={vizWidth}
|
||||||
height={vizHeight}
|
height={vizHeight}
|
||||||
|
highlightedTitle={highlightedTitle}
|
||||||
fieldDisplayValues={fieldDisplayValues}
|
fieldDisplayValues={fieldDisplayValues}
|
||||||
tooltipOptions={tooltipOptions}
|
tooltipOptions={tooltipOptions}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
@ -132,6 +158,7 @@ export const PieChartSvg: FC<PieChartSvgProps> = ({
|
|||||||
pieType,
|
pieType,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
highlightedTitle,
|
||||||
useGradients = true,
|
useGradients = true,
|
||||||
displayLabels = [],
|
displayLabels = [],
|
||||||
tooltipOptions,
|
tooltipOptions,
|
||||||
@ -194,6 +221,7 @@ export const PieChartSvg: FC<PieChartSvgProps> = ({
|
|||||||
{(pie) => {
|
{(pie) => {
|
||||||
return pie.arcs.map((arc) => {
|
return pie.arcs.map((arc) => {
|
||||||
const color = arc.data.display.color ?? FALLBACK_COLOR;
|
const color = arc.data.display.color ?? FALLBACK_COLOR;
|
||||||
|
const highlighted = highlightedTitle === arc.data.display.title;
|
||||||
const label = showLabel ? (
|
const label = showLabel ? (
|
||||||
<PieLabel
|
<PieLabel
|
||||||
arc={arc}
|
arc={arc}
|
||||||
@ -210,6 +238,7 @@ export const PieChartSvg: FC<PieChartSvgProps> = ({
|
|||||||
{(api) => (
|
{(api) => (
|
||||||
<PieSlice
|
<PieSlice
|
||||||
tooltip={tooltip}
|
tooltip={tooltip}
|
||||||
|
highlighted={highlighted}
|
||||||
arc={arc}
|
arc={arc}
|
||||||
pie={pie}
|
pie={pie}
|
||||||
fill={getGradientColor(color)}
|
fill={getGradientColor(color)}
|
||||||
@ -225,6 +254,7 @@ export const PieChartSvg: FC<PieChartSvgProps> = ({
|
|||||||
return (
|
return (
|
||||||
<PieSlice
|
<PieSlice
|
||||||
key={arc.index}
|
key={arc.index}
|
||||||
|
highlighted={highlighted}
|
||||||
tooltip={tooltip}
|
tooltip={tooltip}
|
||||||
arc={arc}
|
arc={arc}
|
||||||
pie={pie}
|
pie={pie}
|
||||||
@ -260,11 +290,12 @@ const PieSlice: FC<{
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
arc: PieArcDatum<FieldDisplay>;
|
arc: PieArcDatum<FieldDisplay>;
|
||||||
pie: ProvidedProps<FieldDisplay>;
|
pie: ProvidedProps<FieldDisplay>;
|
||||||
|
highlighted?: boolean;
|
||||||
fill: string;
|
fill: string;
|
||||||
tooltip: UseTooltipParams<SeriesTableRowProps[]>;
|
tooltip: UseTooltipParams<SeriesTableRowProps[]>;
|
||||||
tooltipOptions: VizTooltipOptions;
|
tooltipOptions: VizTooltipOptions;
|
||||||
openMenu?: (event: React.MouseEvent<SVGElement>) => void;
|
openMenu?: (event: React.MouseEvent<SVGElement>) => void;
|
||||||
}> = ({ arc, children, pie, openMenu, fill, tooltip, tooltipOptions }) => {
|
}> = ({ arc, children, pie, highlighted, openMenu, fill, tooltip, tooltipOptions }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const styles = useStyles(getStyles);
|
const styles = useStyles(getStyles);
|
||||||
|
|
||||||
@ -280,7 +311,7 @@ const PieSlice: FC<{
|
|||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
key={arc.data.display.title}
|
key={arc.data.display.title}
|
||||||
className={styles.svgArg}
|
className={highlighted ? styles.svgArg.highlighted : styles.svgArg.normal}
|
||||||
onMouseMove={tooltipOptions.mode !== 'none' ? onMouseMoveOverArc : undefined}
|
onMouseMove={tooltipOptions.mode !== 'none' ? onMouseMoveOverArc : undefined}
|
||||||
onMouseOut={tooltip.hideTooltip}
|
onMouseOut={tooltip.hideTooltip}
|
||||||
onClick={openMenu}
|
onClick={openMenu}
|
||||||
@ -420,12 +451,18 @@ const getStyles = (theme: GrafanaTheme) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
`,
|
`,
|
||||||
svgArg: css`
|
svgArg: {
|
||||||
transition: all 200ms ease-in-out;
|
normal: css`
|
||||||
&:hover {
|
transition: all 200ms ease-in-out;
|
||||||
|
&:hover {
|
||||||
|
transform: scale3d(1.03, 1.03, 1);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
highlighted: css`
|
||||||
|
transition: all 200ms ease-in-out;
|
||||||
transform: scale3d(1.03, 1.03, 1);
|
transform: scale3d(1.03, 1.03, 1);
|
||||||
}
|
`,
|
||||||
`,
|
},
|
||||||
tooltipPortal: css`
|
tooltipPortal: css`
|
||||||
${getTooltipContainerStyles(theme)}
|
${getTooltipContainerStyles(theme)}
|
||||||
`,
|
`,
|
||||||
|
@ -2,18 +2,31 @@ import { DataFrame, FieldConfigSource, FieldDisplay, InterpolateFunction, Reduce
|
|||||||
import { VizTooltipOptions } from '../VizTooltip';
|
import { VizTooltipOptions } from '../VizTooltip';
|
||||||
import { VizLegendOptions } from '..';
|
import { VizLegendOptions } from '..';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @beta
|
||||||
|
*/
|
||||||
export interface PieChartSvgProps {
|
export interface PieChartSvgProps {
|
||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
fieldDisplayValues: FieldDisplay[];
|
fieldDisplayValues: FieldDisplay[];
|
||||||
pieType: PieChartType;
|
pieType: PieChartType;
|
||||||
|
highlightedTitle?: string;
|
||||||
displayLabels?: PieChartLabels[];
|
displayLabels?: PieChartLabels[];
|
||||||
useGradients?: boolean;
|
useGradients?: boolean;
|
||||||
onSeriesColorChange?: (label: string, color: string) => void;
|
onSeriesColorChange?: (label: string, color: string) => void;
|
||||||
tooltipOptions: VizTooltipOptions;
|
tooltipOptions: VizTooltipOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PieChartProps extends Omit<PieChartSvgProps, 'fieldDisplayValues'> {
|
/**
|
||||||
|
* @beta
|
||||||
|
*/
|
||||||
|
export interface PieChartProps {
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
pieType: PieChartType;
|
||||||
|
displayLabels?: PieChartLabels[];
|
||||||
|
useGradients?: boolean;
|
||||||
|
onSeriesColorChange?: (label: string, color: string) => void;
|
||||||
legendOptions?: PieChartLegendOptions;
|
legendOptions?: PieChartLegendOptions;
|
||||||
tooltipOptions: VizTooltipOptions;
|
tooltipOptions: VizTooltipOptions;
|
||||||
reduceOptions: ReduceDataOptions;
|
reduceOptions: ReduceDataOptions;
|
||||||
@ -23,22 +36,34 @@ export interface PieChartProps extends Omit<PieChartSvgProps, 'fieldDisplayValue
|
|||||||
timeZone?: string;
|
timeZone?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @beta
|
||||||
|
*/
|
||||||
export enum PieChartType {
|
export enum PieChartType {
|
||||||
Pie = 'pie',
|
Pie = 'pie',
|
||||||
Donut = 'donut',
|
Donut = 'donut',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @beta
|
||||||
|
*/
|
||||||
export enum PieChartLegendValues {
|
export enum PieChartLegendValues {
|
||||||
Value = 'value',
|
Value = 'value',
|
||||||
Percent = 'percent',
|
Percent = 'percent',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @beta
|
||||||
|
*/
|
||||||
export enum PieChartLabels {
|
export enum PieChartLabels {
|
||||||
Name = 'name',
|
Name = 'name',
|
||||||
Value = 'value',
|
Value = 'value',
|
||||||
Percent = 'percent',
|
Percent = 'percent',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @beta
|
||||||
|
*/
|
||||||
export interface PieChartLegendOptions extends VizLegendOptions {
|
export interface PieChartLegendOptions extends VizLegendOptions {
|
||||||
values: PieChartLegendValues[];
|
values: PieChartLegendValues[];
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { LegendProps } from './types';
|
import { LegendProps, VizLegendItem } from './types';
|
||||||
import { LegendDisplayMode } from './models.gen';
|
import { LegendDisplayMode } from './models.gen';
|
||||||
import { VizLegendTable } from './VizLegendTable';
|
import { VizLegendTable } from './VizLegendTable';
|
||||||
import { VizLegendList } from './VizLegendList';
|
import { VizLegendList } from './VizLegendList';
|
||||||
|
import { DataHoverClearEvent, DataHoverEvent } from '@grafana/data';
|
||||||
|
import { usePanelContext } from '../PanelChrome';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -18,6 +20,38 @@ export const VizLegend: React.FunctionComponent<LegendProps> = ({
|
|||||||
placement,
|
placement,
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { eventBus } = usePanelContext();
|
||||||
|
|
||||||
|
const onMouseEnter = useCallback(
|
||||||
|
(item: VizLegendItem, event: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||||
|
eventBus?.publish({
|
||||||
|
type: DataHoverEvent.type,
|
||||||
|
payload: {
|
||||||
|
raw: event,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
dataId: item.label,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[eventBus]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onMouseOut = useCallback(
|
||||||
|
(item: VizLegendItem, event: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||||
|
eventBus?.publish({
|
||||||
|
type: DataHoverClearEvent.type,
|
||||||
|
payload: {
|
||||||
|
raw: event,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
dataId: item.label,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[eventBus]
|
||||||
|
);
|
||||||
|
|
||||||
switch (displayMode) {
|
switch (displayMode) {
|
||||||
case LegendDisplayMode.Table:
|
case LegendDisplayMode.Table:
|
||||||
return (
|
return (
|
||||||
@ -29,6 +63,8 @@ export const VizLegend: React.FunctionComponent<LegendProps> = ({
|
|||||||
sortDesc={sortDesc}
|
sortDesc={sortDesc}
|
||||||
onLabelClick={onLabelClick}
|
onLabelClick={onLabelClick}
|
||||||
onToggleSort={onToggleSort}
|
onToggleSort={onToggleSort}
|
||||||
|
onLabelMouseEnter={onMouseEnter}
|
||||||
|
onLabelMouseOut={onMouseOut}
|
||||||
onSeriesColorChange={onSeriesColorChange}
|
onSeriesColorChange={onSeriesColorChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -38,6 +74,8 @@ export const VizLegend: React.FunctionComponent<LegendProps> = ({
|
|||||||
className={className}
|
className={className}
|
||||||
items={items}
|
items={items}
|
||||||
placement={placement}
|
placement={placement}
|
||||||
|
onLabelMouseEnter={onMouseEnter}
|
||||||
|
onLabelMouseOut={onMouseOut}
|
||||||
onLabelClick={onLabelClick}
|
onLabelClick={onLabelClick}
|
||||||
onSeriesColorChange={onSeriesColorChange}
|
onSeriesColorChange={onSeriesColorChange}
|
||||||
/>
|
/>
|
||||||
|
@ -17,6 +17,8 @@ export const VizLegendList: React.FunctionComponent<Props> = ({
|
|||||||
itemRenderer,
|
itemRenderer,
|
||||||
onSeriesColorChange,
|
onSeriesColorChange,
|
||||||
onLabelClick,
|
onLabelClick,
|
||||||
|
onLabelMouseEnter,
|
||||||
|
onLabelMouseOut,
|
||||||
placement,
|
placement,
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
@ -25,7 +27,13 @@ export const VizLegendList: React.FunctionComponent<Props> = ({
|
|||||||
if (!itemRenderer) {
|
if (!itemRenderer) {
|
||||||
/* eslint-disable-next-line react/display-name */
|
/* eslint-disable-next-line react/display-name */
|
||||||
itemRenderer = (item) => (
|
itemRenderer = (item) => (
|
||||||
<VizLegendListItem item={item} onLabelClick={onLabelClick} onSeriesColorChange={onSeriesColorChange} />
|
<VizLegendListItem
|
||||||
|
item={item}
|
||||||
|
onLabelClick={onLabelClick}
|
||||||
|
onSeriesColorChange={onSeriesColorChange}
|
||||||
|
onLabelMouseEnter={onLabelMouseEnter}
|
||||||
|
onLabelMouseOut={onLabelMouseOut}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { VizLegendSeriesIcon } from './VizLegendSeriesIcon';
|
import { VizLegendSeriesIcon } from './VizLegendSeriesIcon';
|
||||||
import { VizLegendItem, SeriesColorChangeHandler } from './types';
|
import { VizLegendItem, SeriesColorChangeHandler } from './types';
|
||||||
@ -11,31 +11,65 @@ export interface Props {
|
|||||||
className?: string;
|
className?: string;
|
||||||
onLabelClick?: (item: VizLegendItem, event: React.MouseEvent<HTMLDivElement>) => void;
|
onLabelClick?: (item: VizLegendItem, event: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
onSeriesColorChange?: SeriesColorChangeHandler;
|
onSeriesColorChange?: SeriesColorChangeHandler;
|
||||||
|
onLabelMouseEnter?: (item: VizLegendItem, event: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
|
onLabelMouseOut?: (item: VizLegendItem, event: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
export const VizLegendListItem: React.FunctionComponent<Props> = ({ item, onSeriesColorChange, onLabelClick }) => {
|
export const VizLegendListItem: React.FunctionComponent<Props> = ({
|
||||||
|
item,
|
||||||
|
onSeriesColorChange,
|
||||||
|
onLabelClick,
|
||||||
|
onLabelMouseEnter,
|
||||||
|
onLabelMouseOut,
|
||||||
|
}) => {
|
||||||
const styles = useStyles(getStyles);
|
const styles = useStyles(getStyles);
|
||||||
|
|
||||||
|
const onMouseEnter = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
|
if (onLabelMouseEnter) {
|
||||||
|
onLabelMouseEnter(item, event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[item, onLabelMouseEnter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onMouseOut = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
|
if (onLabelMouseOut) {
|
||||||
|
onLabelMouseOut(item, event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[item, onLabelMouseOut]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClick = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
|
if (onLabelClick) {
|
||||||
|
onLabelClick(item, event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[item, onLabelClick]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onColorChange = useCallback(
|
||||||
|
(color: string) => {
|
||||||
|
if (onSeriesColorChange) {
|
||||||
|
onSeriesColorChange(item.label, color);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[item, onSeriesColorChange]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.itemWrapper}>
|
<div className={styles.itemWrapper}>
|
||||||
<VizLegendSeriesIcon
|
<VizLegendSeriesIcon disabled={!onSeriesColorChange} color={item.color} onColorChange={onColorChange} />
|
||||||
disabled={!onSeriesColorChange}
|
|
||||||
color={item.color}
|
|
||||||
onColorChange={(color) => {
|
|
||||||
if (onSeriesColorChange) {
|
|
||||||
onSeriesColorChange(item.label, color);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
onClick={(event) => {
|
onMouseEnter={onMouseEnter}
|
||||||
if (onLabelClick) {
|
onMouseOut={onMouseOut}
|
||||||
onLabelClick(item, event);
|
onClick={onClick}
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={cx(styles.label, item.disabled && styles.labelDisabled)}
|
className={cx(styles.label, item.disabled && styles.labelDisabled)}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
|
@ -18,6 +18,8 @@ export const VizLegendTable: FC<VizLegendTableProps> = ({
|
|||||||
className,
|
className,
|
||||||
onToggleSort,
|
onToggleSort,
|
||||||
onLabelClick,
|
onLabelClick,
|
||||||
|
onLabelMouseEnter,
|
||||||
|
onLabelMouseOut,
|
||||||
onSeriesColorChange,
|
onSeriesColorChange,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles(getStyles);
|
const styles = useStyles(getStyles);
|
||||||
@ -57,6 +59,8 @@ export const VizLegendTable: FC<VizLegendTableProps> = ({
|
|||||||
item={item}
|
item={item}
|
||||||
onSeriesColorChange={onSeriesColorChange}
|
onSeriesColorChange={onSeriesColorChange}
|
||||||
onLabelClick={onLabelClick}
|
onLabelClick={onLabelClick}
|
||||||
|
onLabelMouseEnter={onLabelMouseEnter}
|
||||||
|
onLabelMouseOut={onLabelMouseOut}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { VizLegendSeriesIcon } from './VizLegendSeriesIcon';
|
import { VizLegendSeriesIcon } from './VizLegendSeriesIcon';
|
||||||
import { VizLegendItem, SeriesColorChangeHandler } from './types';
|
import { VizLegendItem, SeriesColorChangeHandler } from './types';
|
||||||
@ -12,6 +12,8 @@ export interface Props {
|
|||||||
className?: string;
|
className?: string;
|
||||||
onLabelClick?: (item: VizLegendItem, event: React.MouseEvent<HTMLDivElement>) => void;
|
onLabelClick?: (item: VizLegendItem, event: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
onSeriesColorChange?: SeriesColorChangeHandler;
|
onSeriesColorChange?: SeriesColorChangeHandler;
|
||||||
|
onLabelMouseEnter?: (item: VizLegendItem, event: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
|
onLabelMouseOut?: (item: VizLegendItem, event: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,29 +23,57 @@ export const LegendTableItem: React.FunctionComponent<Props> = ({
|
|||||||
item,
|
item,
|
||||||
onSeriesColorChange,
|
onSeriesColorChange,
|
||||||
onLabelClick,
|
onLabelClick,
|
||||||
|
onLabelMouseEnter,
|
||||||
|
onLabelMouseOut,
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles(getStyles);
|
const styles = useStyles(getStyles);
|
||||||
|
|
||||||
|
const onMouseEnter = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
|
if (onLabelMouseEnter) {
|
||||||
|
onLabelMouseEnter(item, event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[item, onLabelMouseEnter]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onMouseOut = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
|
if (onLabelMouseOut) {
|
||||||
|
onLabelMouseOut(item, event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[item, onLabelMouseOut]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClick = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
|
if (onLabelClick) {
|
||||||
|
onLabelClick(item, event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[item, onLabelClick]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onColorChange = useCallback(
|
||||||
|
(color: string) => {
|
||||||
|
if (onSeriesColorChange) {
|
||||||
|
onSeriesColorChange(item.label, color);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[item, onSeriesColorChange]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className={cx(styles.row, className)}>
|
<tr className={cx(styles.row, className)}>
|
||||||
<td>
|
<td>
|
||||||
<span className={styles.itemWrapper}>
|
<span className={styles.itemWrapper}>
|
||||||
<VizLegendSeriesIcon
|
<VizLegendSeriesIcon disabled={!onSeriesColorChange} color={item.color} onColorChange={onColorChange} />
|
||||||
disabled={!onSeriesColorChange}
|
|
||||||
color={item.color}
|
|
||||||
onColorChange={(color) => {
|
|
||||||
if (onSeriesColorChange) {
|
|
||||||
onSeriesColorChange(item.label, color);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
onClick={(event) => {
|
onMouseEnter={onMouseEnter}
|
||||||
if (onLabelClick) {
|
onMouseOut={onMouseOut}
|
||||||
onLabelClick(item, event);
|
onClick={onClick}
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={cx(styles.label, item.disabled && styles.labelDisabled)}
|
className={cx(styles.label, item.disabled && styles.labelDisabled)}
|
||||||
>
|
>
|
||||||
{item.label} {item.yAxis === 2 && <span className={styles.yAxisLabel}>(right y-axis)</span>}
|
{item.label} {item.yAxis === 2 && <span className={styles.yAxisLabel}>(right y-axis)</span>}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { DataFrameFieldIndex, DisplayValue } from '@grafana/data';
|
import { DataFrameFieldIndex, DisplayValue } from '@grafana/data';
|
||||||
|
import React from 'react';
|
||||||
import { LegendDisplayMode, LegendPlacement } from './models.gen';
|
import { LegendDisplayMode, LegendPlacement } from './models.gen';
|
||||||
|
|
||||||
export interface VizLegendBaseProps {
|
export interface VizLegendBaseProps {
|
||||||
@ -8,6 +9,8 @@ export interface VizLegendBaseProps {
|
|||||||
itemRenderer?: (item: VizLegendItem, index: number) => JSX.Element;
|
itemRenderer?: (item: VizLegendItem, index: number) => JSX.Element;
|
||||||
onSeriesColorChange?: SeriesColorChangeHandler;
|
onSeriesColorChange?: SeriesColorChangeHandler;
|
||||||
onLabelClick?: (item: VizLegendItem, event: React.MouseEvent<HTMLElement>) => void;
|
onLabelClick?: (item: VizLegendItem, event: React.MouseEvent<HTMLElement>) => void;
|
||||||
|
onLabelMouseEnter?: (item: VizLegendItem, event: React.MouseEvent<HTMLElement>) => void;
|
||||||
|
onLabelMouseOut?: (item: VizLegendItem, event: React.MouseEvent<HTMLElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VizLegendTableProps extends VizLegendBaseProps {
|
export interface VizLegendTableProps extends VizLegendBaseProps {
|
||||||
|
@ -94,6 +94,8 @@ export {
|
|||||||
PanelChromeLoadingIndicatorProps,
|
PanelChromeLoadingIndicatorProps,
|
||||||
PanelChromeErrorIndicator,
|
PanelChromeErrorIndicator,
|
||||||
PanelChromeErrorIndicatorProps,
|
PanelChromeErrorIndicatorProps,
|
||||||
|
PanelContextProvider,
|
||||||
|
usePanelContext,
|
||||||
} from './PanelChrome';
|
} from './PanelChrome';
|
||||||
export { VizLayout, VizLayoutComponentType, VizLayoutLegendProps, VizLayoutProps } from './VizLayout/VizLayout';
|
export { VizLayout, VizLayoutComponentType, VizLayoutLegendProps, VizLayoutProps } from './VizLayout/VizLayout';
|
||||||
export { VizLegendItem } from './VizLegend/types';
|
export { VizLegendItem } from './VizLegend/types';
|
||||||
|
@ -32,6 +32,7 @@ function setupTestContext(options: Partial<Props>) {
|
|||||||
setTimeSrv(timeSrv);
|
setTimeSrv(timeSrv);
|
||||||
const defaults: Props = {
|
const defaults: Props = {
|
||||||
panel: ({
|
panel: ({
|
||||||
|
id: 123,
|
||||||
hasTitle: jest.fn(),
|
hasTitle: jest.fn(),
|
||||||
replaceVariables: jest.fn(),
|
replaceVariables: jest.fn(),
|
||||||
events: { subscribe: jest.fn() },
|
events: { subscribe: jest.fn() },
|
||||||
|
@ -4,7 +4,7 @@ import classNames from 'classnames';
|
|||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
// Components
|
// Components
|
||||||
import { PanelHeader } from './PanelHeader/PanelHeader';
|
import { PanelHeader } from './PanelHeader/PanelHeader';
|
||||||
import { ErrorBoundary } from '@grafana/ui';
|
import { ErrorBoundary, PanelContextProvider } from '@grafana/ui';
|
||||||
// Utils & Services
|
// Utils & Services
|
||||||
import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
|
import { getTimeSrv, TimeSrv } from '../services/TimeSrv';
|
||||||
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
|
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
|
||||||
@ -15,6 +15,7 @@ import { DashboardModel, PanelModel } from '../state';
|
|||||||
import { PANEL_BORDER } from 'app/core/constants';
|
import { PANEL_BORDER } from 'app/core/constants';
|
||||||
import {
|
import {
|
||||||
AbsoluteTimeRange,
|
AbsoluteTimeRange,
|
||||||
|
EventBusWithSource,
|
||||||
FieldConfigSource,
|
FieldConfigSource,
|
||||||
getDefaultTimeRange,
|
getDefaultTimeRange,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
@ -46,6 +47,7 @@ export interface State {
|
|||||||
renderCounter: number;
|
renderCounter: number;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
refreshWhenInView: boolean;
|
refreshWhenInView: boolean;
|
||||||
|
eventBus: EventBusWithSource;
|
||||||
data: PanelData;
|
data: PanelData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,6 +62,7 @@ export class PanelChrome extends Component<Props, State> {
|
|||||||
isFirstLoad: true,
|
isFirstLoad: true,
|
||||||
renderCounter: 0,
|
renderCounter: 0,
|
||||||
refreshWhenInView: false,
|
refreshWhenInView: false,
|
||||||
|
eventBus: new EventBusWithSource(props.dashboard.events, `panel-${props.panel.id}`),
|
||||||
data: {
|
data: {
|
||||||
state: LoadingState.NotStarted,
|
state: LoadingState.NotStarted,
|
||||||
series: [],
|
series: [],
|
||||||
@ -286,24 +289,26 @@ export class PanelChrome extends Component<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={panelContentClassNames}>
|
<div className={panelContentClassNames}>
|
||||||
<PanelComponent
|
<PanelContextProvider value={{ eventBus: this.state.eventBus }}>
|
||||||
id={panel.id}
|
<PanelComponent
|
||||||
data={data}
|
id={panel.id}
|
||||||
title={panel.title}
|
data={data}
|
||||||
timeRange={timeRange}
|
title={panel.title}
|
||||||
timeZone={this.props.dashboard.getTimezone()}
|
timeRange={timeRange}
|
||||||
options={panelOptions}
|
timeZone={this.props.dashboard.getTimezone()}
|
||||||
fieldConfig={panel.fieldConfig}
|
options={panelOptions}
|
||||||
transparent={panel.transparent}
|
fieldConfig={panel.fieldConfig}
|
||||||
width={panelWidth}
|
transparent={panel.transparent}
|
||||||
height={innerPanelHeight}
|
width={panelWidth}
|
||||||
renderCounter={renderCounter}
|
height={innerPanelHeight}
|
||||||
replaceVariables={panel.replaceVariables}
|
renderCounter={renderCounter}
|
||||||
onOptionsChange={this.onOptionsChange}
|
replaceVariables={panel.replaceVariables}
|
||||||
onFieldConfigChange={this.onFieldConfigChange}
|
onOptionsChange={this.onOptionsChange}
|
||||||
onChangeTimeRange={this.onChangeTimeRange}
|
onFieldConfigChange={this.onFieldConfigChange}
|
||||||
eventBus={dashboard.events}
|
onChangeTimeRange={this.onChangeTimeRange}
|
||||||
/>
|
eventBus={dashboard.events}
|
||||||
|
/>
|
||||||
|
</PanelContextProvider>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { fieldReducers, getFieldDisplayName, getFrameDisplayName, PanelProps, ReducerID } from '@grafana/data';
|
import { PanelProps } from '@grafana/data';
|
||||||
|
|
||||||
import { DebugPanelOptions, UpdateCounters, UpdateConfig } from './types';
|
import { DebugPanelOptions, DebugMode, UpdateCounters } from './types';
|
||||||
import { IconButton } from '@grafana/ui';
|
import { EventBusLoggerPanel } from './EventBusLogger';
|
||||||
|
import { RenderInfoViewer } from './RenderInfoViewer';
|
||||||
|
|
||||||
type Props = PanelProps<DebugPanelOptions>;
|
type Props = PanelProps<DebugPanelOptions>;
|
||||||
|
|
||||||
@ -40,57 +41,11 @@ export class DebugPanel extends Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { data, options } = this.props;
|
const { options } = this.props;
|
||||||
const showCounters = options.counters ?? ({} as UpdateConfig);
|
if (options.mode === DebugMode.Events) {
|
||||||
this.counters.render++;
|
return <EventBusLoggerPanel eventBus={this.props.eventBus} />;
|
||||||
const now = Date.now();
|
}
|
||||||
const elapsed = now - this.lastRender;
|
|
||||||
this.lastRender = now;
|
|
||||||
|
|
||||||
const reducer = fieldReducers.get(ReducerID.lastNotNull);
|
return <RenderInfoViewer {...this.props} />;
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<IconButton name="step-backward" title="reset counters" onClick={this.resetCounters} />
|
|
||||||
<span>
|
|
||||||
{showCounters.render && <span>Render: {this.counters.render} </span>}
|
|
||||||
{showCounters.dataChanged && <span>Data: {this.counters.dataChanged} </span>}
|
|
||||||
{showCounters.schemaChanged && <span>Schema: {this.counters.schemaChanged} </span>}
|
|
||||||
<span>TIME: {elapsed}ms</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{data.series &&
|
|
||||||
data.series.map((frame, idx) => (
|
|
||||||
<div key={`${idx}/${frame.refId}`}>
|
|
||||||
<h4>
|
|
||||||
{getFrameDisplayName(frame, idx)} ({frame.length})
|
|
||||||
</h4>
|
|
||||||
<table className="filter-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<td>Field</td>
|
|
||||||
<td>Type</td>
|
|
||||||
<td>Last</td>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{frame.fields.map((field, idx) => {
|
|
||||||
const v = reducer.reduce!(field, false, false)[reducer.id];
|
|
||||||
return (
|
|
||||||
<tr key={`${idx}/${field.name}`}>
|
|
||||||
<td>{getFieldDisplayName(field, frame, data.series)}</td>
|
|
||||||
<td>{field.type}</td>
|
|
||||||
<td>{`${v}`}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
75
public/app/plugins/panel/debug/EventBusLogger.tsx
Normal file
75
public/app/plugins/panel/debug/EventBusLogger.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { CustomScrollbar } from '@grafana/ui';
|
||||||
|
import {
|
||||||
|
BusEvent,
|
||||||
|
CircularVector,
|
||||||
|
DataHoverPayload,
|
||||||
|
DataHoverEvent,
|
||||||
|
DataHoverClearEvent,
|
||||||
|
DataSelectEvent,
|
||||||
|
EventBus,
|
||||||
|
BusEventHandler,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { PartialObserver, Unsubscribable } from 'rxjs';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
eventBus: EventBus;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
isError?: boolean;
|
||||||
|
counter: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BusEventEx {
|
||||||
|
key: number;
|
||||||
|
type: string;
|
||||||
|
payload: DataHoverPayload;
|
||||||
|
}
|
||||||
|
let counter = 100;
|
||||||
|
|
||||||
|
export class EventBusLoggerPanel extends PureComponent<Props, State> {
|
||||||
|
history = new CircularVector<BusEventEx>({ capacity: 40, append: 'head' });
|
||||||
|
subs: Unsubscribable[] = [];
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = { counter };
|
||||||
|
|
||||||
|
this.subs.push(props.eventBus.subscribe(DataHoverEvent, this.hoverHandler));
|
||||||
|
props.eventBus.getStream(DataHoverClearEvent).subscribe(this.eventObserver);
|
||||||
|
props.eventBus.getStream(DataSelectEvent).subscribe(this.eventObserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
for (const sub of this.subs) {
|
||||||
|
sub.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hoverHandler: BusEventHandler<DataHoverEvent> = (event: DataHoverEvent) => {
|
||||||
|
this.history.add({
|
||||||
|
key: counter++,
|
||||||
|
type: event.type,
|
||||||
|
payload: event.payload,
|
||||||
|
});
|
||||||
|
this.setState({ counter });
|
||||||
|
};
|
||||||
|
|
||||||
|
eventObserver: PartialObserver<BusEvent> = {
|
||||||
|
next: (v: BusEvent) => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
|
||||||
|
{this.history.map((v, idx) => (
|
||||||
|
<div key={v.key}>
|
||||||
|
{v.key} {v.type} / X:{JSON.stringify(v.payload.x)} / Y:{JSON.stringify(v.payload.y)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</CustomScrollbar>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
109
public/app/plugins/panel/debug/RenderInfoViewer.tsx
Normal file
109
public/app/plugins/panel/debug/RenderInfoViewer.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import {
|
||||||
|
compareArrayValues,
|
||||||
|
compareDataFrameStructures,
|
||||||
|
fieldReducers,
|
||||||
|
getFieldDisplayName,
|
||||||
|
getFrameDisplayName,
|
||||||
|
PanelProps,
|
||||||
|
ReducerID,
|
||||||
|
} from '@grafana/data';
|
||||||
|
|
||||||
|
import { DebugPanelOptions, UpdateCounters, UpdateConfig } from './types';
|
||||||
|
import { IconButton } from '@grafana/ui';
|
||||||
|
|
||||||
|
type Props = PanelProps<DebugPanelOptions>;
|
||||||
|
|
||||||
|
export class RenderInfoViewer extends Component<Props> {
|
||||||
|
// Intentionally not state to avoid overhead -- yes, things will be 1 tick behind
|
||||||
|
lastRender = Date.now();
|
||||||
|
counters: UpdateCounters = {
|
||||||
|
render: 0,
|
||||||
|
dataChanged: 0,
|
||||||
|
schemaChanged: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
shouldComponentUpdate(prevProps: Props) {
|
||||||
|
const { data, options } = this.props;
|
||||||
|
|
||||||
|
if (prevProps.data !== data) {
|
||||||
|
this.counters.dataChanged++;
|
||||||
|
|
||||||
|
if (options.counters?.schemaChanged) {
|
||||||
|
const oldSeries = prevProps.data?.series;
|
||||||
|
const series = data.series;
|
||||||
|
if (series && oldSeries) {
|
||||||
|
const sameStructure = compareArrayValues(series, oldSeries, compareDataFrameStructures);
|
||||||
|
if (!sameStructure) {
|
||||||
|
this.counters.schemaChanged++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true; // always render?
|
||||||
|
}
|
||||||
|
|
||||||
|
resetCounters = () => {
|
||||||
|
this.counters = {
|
||||||
|
render: 0,
|
||||||
|
dataChanged: 0,
|
||||||
|
schemaChanged: 0,
|
||||||
|
};
|
||||||
|
this.forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { data, options } = this.props;
|
||||||
|
const showCounters = options.counters ?? ({} as UpdateConfig);
|
||||||
|
this.counters.render++;
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsed = now - this.lastRender;
|
||||||
|
this.lastRender = now;
|
||||||
|
|
||||||
|
const reducer = fieldReducers.get(ReducerID.lastNotNull);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<IconButton name="step-backward" title="reset counters" onClick={this.resetCounters} />
|
||||||
|
<span>
|
||||||
|
{showCounters.render && <span>Render: {this.counters.render} </span>}
|
||||||
|
{showCounters.dataChanged && <span>Data: {this.counters.dataChanged} </span>}
|
||||||
|
{showCounters.schemaChanged && <span>Schema: {this.counters.schemaChanged} </span>}
|
||||||
|
<span>TIME: {elapsed}ms</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data.series &&
|
||||||
|
data.series.map((frame, idx) => (
|
||||||
|
<div key={`${idx}/${frame.refId}`}>
|
||||||
|
<h4>
|
||||||
|
{getFrameDisplayName(frame, idx)} ({frame.length})
|
||||||
|
</h4>
|
||||||
|
<table className="filter-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>Field</td>
|
||||||
|
<td>Type</td>
|
||||||
|
<td>Last</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{frame.fields.map((field, idx) => {
|
||||||
|
const v = reducer.reduce!(field, false, false)[reducer.id];
|
||||||
|
return (
|
||||||
|
<tr key={`${idx}/${field.name}`}>
|
||||||
|
<td>{getFieldDisplayName(field, frame, data.series)}</td>
|
||||||
|
<td>{field.type}</td>
|
||||||
|
<td>{`${v}`}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,22 +1,36 @@
|
|||||||
import { PanelPlugin } from '@grafana/data';
|
import { PanelPlugin } from '@grafana/data';
|
||||||
import { DebugPanel } from './DebugPanel';
|
import { DebugPanel } from './DebugPanel';
|
||||||
import { DebugPanelOptions } from './types';
|
import { DebugMode, DebugPanelOptions } from './types';
|
||||||
|
|
||||||
export const plugin = new PanelPlugin<DebugPanelOptions>(DebugPanel).useFieldConfig().setPanelOptions((builder) => {
|
export const plugin = new PanelPlugin<DebugPanelOptions>(DebugPanel).useFieldConfig().setPanelOptions((builder) => {
|
||||||
builder
|
builder
|
||||||
|
.addRadio({
|
||||||
|
path: 'mode',
|
||||||
|
name: 'Mode',
|
||||||
|
defaultValue: DebugMode.Render,
|
||||||
|
settings: {
|
||||||
|
options: [
|
||||||
|
{ label: 'Render', value: DebugMode.Render },
|
||||||
|
{ label: 'Events', value: DebugMode.Events },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
.addBooleanSwitch({
|
.addBooleanSwitch({
|
||||||
path: 'counters.render',
|
path: 'counters.render',
|
||||||
name: 'Render Count',
|
name: 'Render Count',
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
|
showIf: ({ mode }) => mode === DebugMode.Render,
|
||||||
})
|
})
|
||||||
.addBooleanSwitch({
|
.addBooleanSwitch({
|
||||||
path: 'counters.dataChanged',
|
path: 'counters.dataChanged',
|
||||||
name: 'Data Changed Count',
|
name: 'Data Changed Count',
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
|
showIf: ({ mode }) => mode === DebugMode.Render,
|
||||||
})
|
})
|
||||||
.addBooleanSwitch({
|
.addBooleanSwitch({
|
||||||
path: 'counters.schemaChanged',
|
path: 'counters.schemaChanged',
|
||||||
name: 'Schema Changed Count',
|
name: 'Schema Changed Count',
|
||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
|
showIf: ({ mode }) => mode === DebugMode.Render,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -8,6 +8,12 @@ export type UpdateCounters = {
|
|||||||
schemaChanged: number;
|
schemaChanged: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum DebugMode {
|
||||||
|
Render = 'render',
|
||||||
|
Events = 'events',
|
||||||
|
}
|
||||||
|
|
||||||
export interface DebugPanelOptions {
|
export interface DebugPanelOptions {
|
||||||
|
mode: DebugMode;
|
||||||
counters?: UpdateConfig;
|
counters?: UpdateConfig;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user