Dashboards: Show logs on time series when hovering (#40110)

* Dashboard: Show logs on time series when hovering

* Fix passing hover handler to LogRow

* use DataHoverEvent

* use DataHoverEvent

* Clean up

Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>
This commit is contained in:
Ryan McKinley 2021-10-12 05:57:17 -07:00 committed by GitHub
parent c443f244a0
commit ea0c1006f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 107 additions and 35 deletions

View File

@ -1,5 +1,5 @@
import { AnnotationEvent, DataFrame } from '../types';
import { BusEventWithPayload } from './types';
import { BusEventBase, BusEventWithPayload } from './types';
/**
* When hovering over an element this will identify
@ -26,7 +26,7 @@ export class DataHoverEvent extends BusEventWithPayload<DataHoverPayload> {
}
/** @alpha */
export class DataHoverClearEvent extends BusEventWithPayload<DataHoverPayload> {
export class DataHoverClearEvent extends BusEventBase {
static type = 'data-hover-clear';
}

View File

@ -4,9 +4,10 @@ import { Themeable2 } from '../../types';
import { findMidPointYPosition, pluginLog } from '../uPlot/utils';
import {
DataFrame,
DataHoverClearEvent,
DataHoverEvent,
FieldMatcherID,
fieldMatchers,
LegacyGraphHoverClearEvent,
LegacyGraphHoverEvent,
TimeRange,
TimeZone,
@ -125,42 +126,60 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
return state;
}
handleCursorUpdate(evt: DataHoverEvent | LegacyGraphHoverEvent) {
const time = evt.payload?.point?.time;
const u = this.plotInstance.current;
if (u && time) {
// Try finding left position on time axis
const left = u.valToPos(time, 'x');
let top;
if (left) {
// find midpoint between points at current idx
top = findMidPointYPosition(u, u.posToIdx(left));
}
if (!top || !left) {
return;
}
u.setCursor({
left,
top,
});
}
}
componentDidMount() {
this.panelContext = this.context as PanelContext;
const { eventBus } = this.panelContext;
this.subscription.add(
eventBus
.getStream(LegacyGraphHoverEvent)
.getStream(DataHoverEvent)
.pipe(throttleTime(50))
.subscribe({
next: (evt) => {
const u = this.plotInstance.current;
if (u) {
// Try finding left position on time axis
const left = u.valToPos(evt.payload.point.time, 'x');
let top;
if (left) {
// find midpoint between points at current idx
top = findMidPointYPosition(u, u.posToIdx(left));
}
if (!top || !left) {
return;
}
u.setCursor({
left,
top,
});
if (eventBus === evt.origin) {
return;
}
this.handleCursorUpdate(evt);
},
})
);
// Legacy events (from flot graph)
this.subscription.add(
eventBus
.getStream(LegacyGraphHoverEvent)
.pipe(throttleTime(50))
.subscribe({
next: (evt) => this.handleCursorUpdate(evt),
})
);
this.subscription.add(
eventBus
.getStream(LegacyGraphHoverClearEvent)
.getStream(DataHoverClearEvent)
.pipe(throttleTime(50))
.subscribe({
next: () => {

View File

@ -53,6 +53,7 @@ interface Props extends Themeable2 {
showContextToggle?: (row?: LogRowModel) => boolean;
onClickShowDetectedField?: (key: string) => void;
onClickHideDetectedField?: (key: string) => void;
onLogRowHover?: (row?: LogRowModel) => void;
}
interface State {
@ -141,6 +142,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
theme,
getFieldLinks,
forceEscape,
onLogRowHover,
} = this.props;
const { showDetails, showContext } = this.state;
const style = getLogRowStyles(theme, row.logLevel);
@ -157,7 +159,16 @@ class UnThemedLogRow extends PureComponent<Props, State> {
return (
<>
<tr className={logRowBackground} onClick={this.toggleDetails}>
<tr
className={logRowBackground}
onClick={this.toggleDetails}
onMouseEnter={() => {
onLogRowHover && onLogRowHover(row);
}}
onMouseLeave={() => {
onLogRowHover && onLogRowHover(undefined);
}}
>
{showDuplicates && (
<td className={style.logsRowDuplicates}>
{processedRow.duplicates && processedRow.duplicates > 0 ? `${processedRow.duplicates + 1}x` : null}

View File

@ -33,6 +33,7 @@ export interface Props extends Themeable2 {
getFieldLinks?: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
onClickShowDetectedField?: (key: string) => void;
onClickHideDetectedField?: (key: string) => void;
onLogRowHover?: (row?: LogRowModel) => void;
}
interface State {
@ -99,6 +100,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
onClickShowDetectedField,
onClickHideDetectedField,
forceEscape,
onLogRowHover,
} = this.props;
const { renderAll } = this.state;
const { logsRowsTable } = getLogRowStyles(theme);
@ -144,6 +146,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
getFieldLinks={getFieldLinks}
logsSortOrder={logsSortOrder}
forceEscape={forceEscape}
onLogRowHover={onLogRowHover}
/>
))}
{hasData &&
@ -170,6 +173,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
getFieldLinks={getFieldLinks}
logsSortOrder={logsSortOrder}
forceEscape={forceEscape}
onLogRowHover={onLogRowHover}
/>
))}
{hasData && !renderAll && (

View File

@ -345,7 +345,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
if (x < 0 && y < 0) {
payload.point[xScaleUnit] = null;
payload.point[yScaleKey] = null;
eventBus.publish(new DataHoverClearEvent(payload));
eventBus.publish(new DataHoverClearEvent());
} else {
// convert the points
payload.point[xScaleUnit] = src.posToVal(x, xScaleKey);

View File

@ -5,8 +5,7 @@ import {
LegacyGraphHoverClearEvent,
DataHoverEvent,
DataHoverClearEvent,
DataHoverPayload,
BusEventWithPayload,
BusEventBase,
} from '@grafana/data';
import { Subscription } from 'rxjs';
import { CustomScrollbar } from '@grafana/ui';
@ -17,7 +16,7 @@ interface Props {
}
interface State {
event?: BusEventWithPayload<DataHoverPayload>;
event?: BusEventBase;
}
export class CursorView extends Component<Props, State> {
subscription = new Subscription();
@ -65,9 +64,13 @@ export class CursorView extends Component<Props, State> {
<CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
<h3>Origin: {(origin as any)?.path}</h3>
<span>Type: {type}</span>
<pre>{JSON.stringify(payload.point, null, ' ')}</pre>
{payload.data && (
<DataHoverView data={payload.data} rowIndex={payload.rowIndex} columnIndex={payload.columnIndex} />
{Boolean(payload) && (
<>
<pre>{JSON.stringify(payload.point, null, ' ')}</pre>
{payload.data && (
<DataHoverView data={payload.data} rowIndex={payload.rowIndex} columnIndex={payload.columnIndex} />
)}
</>
)}
</CustomScrollbar>
);

View File

@ -171,7 +171,7 @@ export class GeomapPanel extends Component<Props, State> {
// Tooltip listener
this.map.on('pointermove', this.pointerMoveListener);
this.map.getViewport().addEventListener('mouseout', (evt) => {
this.props.eventBus.publish(new DataHoverClearEvent({ point: {} }));
this.props.eventBus.publish(new DataHoverClearEvent());
});
};

View File

@ -1,7 +1,13 @@
import $ from 'jquery';
import { appEvents } from 'app/core/core';
import { CoreEvents } from 'app/types';
import { textUtil, systemDateFormats, LegacyGraphHoverClearEvent, LegacyGraphHoverEvent } from '@grafana/data';
import {
textUtil,
systemDateFormats,
LegacyGraphHoverClearEvent,
LegacyGraphHoverEvent,
DataHoverClearEvent,
} from '@grafana/data';
export default function GraphTooltip(this: any, elem: any, dashboard: any, scope: any, getSeriesFn: any) {
const self = this;
@ -153,6 +159,7 @@ export default function GraphTooltip(this: any, elem: any, dashboard: any, scope
}
}
dashboard.events.publish(new LegacyGraphHoverClearEvent());
dashboard.events.publish(new DataHoverClearEvent());
});
elem.bind('plothover', (event: any, pos: { panelRelY: number; pageY: number }, item: any) => {

View File

@ -1,7 +1,16 @@
import React, { useCallback, useMemo, useRef, useLayoutEffect, useState } from 'react';
import { css } from '@emotion/css';
import { LogRows, CustomScrollbar, LogLabels, useStyles2 } from '@grafana/ui';
import { PanelProps, Field, Labels, GrafanaTheme2, LogsSortOrder } from '@grafana/data';
import { LogRows, CustomScrollbar, LogLabels, useStyles2, usePanelContext } from '@grafana/ui';
import {
PanelProps,
Field,
Labels,
GrafanaTheme2,
LogsSortOrder,
LogRowModel,
DataHoverClearEvent,
DataHoverEvent,
} from '@grafana/data';
import { Options } from './types';
import { dataFrameToLogsModel, dedupLogRows } from 'app/core/logs_model';
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
@ -29,6 +38,24 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
const [scrollTop, setScrollTop] = useState(0);
const logsContainerRef = useRef<HTMLDivElement>(null);
const { eventBus } = usePanelContext();
const onLogRowHover = useCallback(
(row?: LogRowModel) => {
if (!row) {
eventBus.publish(new DataHoverClearEvent());
} else {
eventBus.publish(
new DataHoverEvent({
point: {
time: row.timeEpochMs,
},
})
);
}
},
[eventBus]
);
// Important to memoize stuff here, as panel rerenders a lot for example when resizing.
const [logRows, deduplicatedRows, commonLabels] = useMemo(() => {
const newResults = data ? dataFrameToLogsModel(data.series, data.request?.intervalMs) : null;
@ -85,6 +112,7 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
logsSortOrder={sortOrder}
enableLogDetails={enableLogDetails}
previewLimit={isAscending ? logRows.length : undefined}
onLogRowHover={onLogRowHover}
/>
{showCommonLabels && isAscending && renderCommonLabels()}
</div>