mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
c443f244a0
commit
ea0c1006f5
@ -1,5 +1,5 @@
|
|||||||
import { AnnotationEvent, DataFrame } from '../types';
|
import { AnnotationEvent, DataFrame } from '../types';
|
||||||
import { BusEventWithPayload } from './types';
|
import { BusEventBase, BusEventWithPayload } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When hovering over an element this will identify
|
* When hovering over an element this will identify
|
||||||
@ -26,7 +26,7 @@ export class DataHoverEvent extends BusEventWithPayload<DataHoverPayload> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @alpha */
|
/** @alpha */
|
||||||
export class DataHoverClearEvent extends BusEventWithPayload<DataHoverPayload> {
|
export class DataHoverClearEvent extends BusEventBase {
|
||||||
static type = 'data-hover-clear';
|
static type = 'data-hover-clear';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,9 +4,10 @@ import { Themeable2 } from '../../types';
|
|||||||
import { findMidPointYPosition, pluginLog } from '../uPlot/utils';
|
import { findMidPointYPosition, pluginLog } from '../uPlot/utils';
|
||||||
import {
|
import {
|
||||||
DataFrame,
|
DataFrame,
|
||||||
|
DataHoverClearEvent,
|
||||||
|
DataHoverEvent,
|
||||||
FieldMatcherID,
|
FieldMatcherID,
|
||||||
fieldMatchers,
|
fieldMatchers,
|
||||||
LegacyGraphHoverClearEvent,
|
|
||||||
LegacyGraphHoverEvent,
|
LegacyGraphHoverEvent,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
TimeZone,
|
TimeZone,
|
||||||
@ -125,42 +126,60 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
|||||||
return state;
|
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() {
|
componentDidMount() {
|
||||||
this.panelContext = this.context as PanelContext;
|
this.panelContext = this.context as PanelContext;
|
||||||
const { eventBus } = this.panelContext;
|
const { eventBus } = this.panelContext;
|
||||||
|
|
||||||
this.subscription.add(
|
this.subscription.add(
|
||||||
eventBus
|
eventBus
|
||||||
.getStream(LegacyGraphHoverEvent)
|
.getStream(DataHoverEvent)
|
||||||
.pipe(throttleTime(50))
|
.pipe(throttleTime(50))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (evt) => {
|
next: (evt) => {
|
||||||
const u = this.plotInstance.current;
|
if (eventBus === evt.origin) {
|
||||||
if (u) {
|
return;
|
||||||
// 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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
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(
|
this.subscription.add(
|
||||||
eventBus
|
eventBus
|
||||||
.getStream(LegacyGraphHoverClearEvent)
|
.getStream(DataHoverClearEvent)
|
||||||
.pipe(throttleTime(50))
|
.pipe(throttleTime(50))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
|
@ -53,6 +53,7 @@ interface Props extends Themeable2 {
|
|||||||
showContextToggle?: (row?: LogRowModel) => boolean;
|
showContextToggle?: (row?: LogRowModel) => boolean;
|
||||||
onClickShowDetectedField?: (key: string) => void;
|
onClickShowDetectedField?: (key: string) => void;
|
||||||
onClickHideDetectedField?: (key: string) => void;
|
onClickHideDetectedField?: (key: string) => void;
|
||||||
|
onLogRowHover?: (row?: LogRowModel) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@ -141,6 +142,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
|||||||
theme,
|
theme,
|
||||||
getFieldLinks,
|
getFieldLinks,
|
||||||
forceEscape,
|
forceEscape,
|
||||||
|
onLogRowHover,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { showDetails, showContext } = this.state;
|
const { showDetails, showContext } = this.state;
|
||||||
const style = getLogRowStyles(theme, row.logLevel);
|
const style = getLogRowStyles(theme, row.logLevel);
|
||||||
@ -157,7 +159,16 @@ class UnThemedLogRow extends PureComponent<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr className={logRowBackground} onClick={this.toggleDetails}>
|
<tr
|
||||||
|
className={logRowBackground}
|
||||||
|
onClick={this.toggleDetails}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
onLogRowHover && onLogRowHover(row);
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
onLogRowHover && onLogRowHover(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
{showDuplicates && (
|
{showDuplicates && (
|
||||||
<td className={style.logsRowDuplicates}>
|
<td className={style.logsRowDuplicates}>
|
||||||
{processedRow.duplicates && processedRow.duplicates > 0 ? `${processedRow.duplicates + 1}x` : null}
|
{processedRow.duplicates && processedRow.duplicates > 0 ? `${processedRow.duplicates + 1}x` : null}
|
||||||
|
@ -33,6 +33,7 @@ export interface Props extends Themeable2 {
|
|||||||
getFieldLinks?: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
|
getFieldLinks?: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
|
||||||
onClickShowDetectedField?: (key: string) => void;
|
onClickShowDetectedField?: (key: string) => void;
|
||||||
onClickHideDetectedField?: (key: string) => void;
|
onClickHideDetectedField?: (key: string) => void;
|
||||||
|
onLogRowHover?: (row?: LogRowModel) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
@ -99,6 +100,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
|||||||
onClickShowDetectedField,
|
onClickShowDetectedField,
|
||||||
onClickHideDetectedField,
|
onClickHideDetectedField,
|
||||||
forceEscape,
|
forceEscape,
|
||||||
|
onLogRowHover,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { renderAll } = this.state;
|
const { renderAll } = this.state;
|
||||||
const { logsRowsTable } = getLogRowStyles(theme);
|
const { logsRowsTable } = getLogRowStyles(theme);
|
||||||
@ -144,6 +146,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
|||||||
getFieldLinks={getFieldLinks}
|
getFieldLinks={getFieldLinks}
|
||||||
logsSortOrder={logsSortOrder}
|
logsSortOrder={logsSortOrder}
|
||||||
forceEscape={forceEscape}
|
forceEscape={forceEscape}
|
||||||
|
onLogRowHover={onLogRowHover}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{hasData &&
|
{hasData &&
|
||||||
@ -170,6 +173,7 @@ class UnThemedLogRows extends PureComponent<Props, State> {
|
|||||||
getFieldLinks={getFieldLinks}
|
getFieldLinks={getFieldLinks}
|
||||||
logsSortOrder={logsSortOrder}
|
logsSortOrder={logsSortOrder}
|
||||||
forceEscape={forceEscape}
|
forceEscape={forceEscape}
|
||||||
|
onLogRowHover={onLogRowHover}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{hasData && !renderAll && (
|
{hasData && !renderAll && (
|
||||||
|
@ -345,7 +345,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
|
|||||||
if (x < 0 && y < 0) {
|
if (x < 0 && y < 0) {
|
||||||
payload.point[xScaleUnit] = null;
|
payload.point[xScaleUnit] = null;
|
||||||
payload.point[yScaleKey] = null;
|
payload.point[yScaleKey] = null;
|
||||||
eventBus.publish(new DataHoverClearEvent(payload));
|
eventBus.publish(new DataHoverClearEvent());
|
||||||
} else {
|
} else {
|
||||||
// convert the points
|
// convert the points
|
||||||
payload.point[xScaleUnit] = src.posToVal(x, xScaleKey);
|
payload.point[xScaleUnit] = src.posToVal(x, xScaleKey);
|
||||||
|
@ -5,8 +5,7 @@ import {
|
|||||||
LegacyGraphHoverClearEvent,
|
LegacyGraphHoverClearEvent,
|
||||||
DataHoverEvent,
|
DataHoverEvent,
|
||||||
DataHoverClearEvent,
|
DataHoverClearEvent,
|
||||||
DataHoverPayload,
|
BusEventBase,
|
||||||
BusEventWithPayload,
|
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import { CustomScrollbar } from '@grafana/ui';
|
import { CustomScrollbar } from '@grafana/ui';
|
||||||
@ -17,7 +16,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
event?: BusEventWithPayload<DataHoverPayload>;
|
event?: BusEventBase;
|
||||||
}
|
}
|
||||||
export class CursorView extends Component<Props, State> {
|
export class CursorView extends Component<Props, State> {
|
||||||
subscription = new Subscription();
|
subscription = new Subscription();
|
||||||
@ -65,9 +64,13 @@ export class CursorView extends Component<Props, State> {
|
|||||||
<CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
|
<CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
|
||||||
<h3>Origin: {(origin as any)?.path}</h3>
|
<h3>Origin: {(origin as any)?.path}</h3>
|
||||||
<span>Type: {type}</span>
|
<span>Type: {type}</span>
|
||||||
<pre>{JSON.stringify(payload.point, null, ' ')}</pre>
|
{Boolean(payload) && (
|
||||||
{payload.data && (
|
<>
|
||||||
<DataHoverView data={payload.data} rowIndex={payload.rowIndex} columnIndex={payload.columnIndex} />
|
<pre>{JSON.stringify(payload.point, null, ' ')}</pre>
|
||||||
|
{payload.data && (
|
||||||
|
<DataHoverView data={payload.data} rowIndex={payload.rowIndex} columnIndex={payload.columnIndex} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
);
|
);
|
||||||
|
@ -171,7 +171,7 @@ export class GeomapPanel extends Component<Props, State> {
|
|||||||
// Tooltip listener
|
// Tooltip listener
|
||||||
this.map.on('pointermove', this.pointerMoveListener);
|
this.map.on('pointermove', this.pointerMoveListener);
|
||||||
this.map.getViewport().addEventListener('mouseout', (evt) => {
|
this.map.getViewport().addEventListener('mouseout', (evt) => {
|
||||||
this.props.eventBus.publish(new DataHoverClearEvent({ point: {} }));
|
this.props.eventBus.publish(new DataHoverClearEvent());
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,13 @@
|
|||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { appEvents } from 'app/core/core';
|
import { appEvents } from 'app/core/core';
|
||||||
import { CoreEvents } from 'app/types';
|
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) {
|
export default function GraphTooltip(this: any, elem: any, dashboard: any, scope: any, getSeriesFn: any) {
|
||||||
const self = this;
|
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 LegacyGraphHoverClearEvent());
|
||||||
|
dashboard.events.publish(new DataHoverClearEvent());
|
||||||
});
|
});
|
||||||
|
|
||||||
elem.bind('plothover', (event: any, pos: { panelRelY: number; pageY: number }, item: any) => {
|
elem.bind('plothover', (event: any, pos: { panelRelY: number; pageY: number }, item: any) => {
|
||||||
|
@ -1,7 +1,16 @@
|
|||||||
import React, { useCallback, useMemo, useRef, useLayoutEffect, useState } from 'react';
|
import React, { useCallback, useMemo, useRef, useLayoutEffect, useState } from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { LogRows, CustomScrollbar, LogLabels, useStyles2 } from '@grafana/ui';
|
import { LogRows, CustomScrollbar, LogLabels, useStyles2, usePanelContext } from '@grafana/ui';
|
||||||
import { PanelProps, Field, Labels, GrafanaTheme2, LogsSortOrder } from '@grafana/data';
|
import {
|
||||||
|
PanelProps,
|
||||||
|
Field,
|
||||||
|
Labels,
|
||||||
|
GrafanaTheme2,
|
||||||
|
LogsSortOrder,
|
||||||
|
LogRowModel,
|
||||||
|
DataHoverClearEvent,
|
||||||
|
DataHoverEvent,
|
||||||
|
} from '@grafana/data';
|
||||||
import { Options } from './types';
|
import { Options } from './types';
|
||||||
import { dataFrameToLogsModel, dedupLogRows } from 'app/core/logs_model';
|
import { dataFrameToLogsModel, dedupLogRows } from 'app/core/logs_model';
|
||||||
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
|
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
|
||||||
@ -29,6 +38,24 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
|
|||||||
const [scrollTop, setScrollTop] = useState(0);
|
const [scrollTop, setScrollTop] = useState(0);
|
||||||
const logsContainerRef = useRef<HTMLDivElement>(null);
|
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.
|
// Important to memoize stuff here, as panel rerenders a lot for example when resizing.
|
||||||
const [logRows, deduplicatedRows, commonLabels] = useMemo(() => {
|
const [logRows, deduplicatedRows, commonLabels] = useMemo(() => {
|
||||||
const newResults = data ? dataFrameToLogsModel(data.series, data.request?.intervalMs) : null;
|
const newResults = data ? dataFrameToLogsModel(data.series, data.request?.intervalMs) : null;
|
||||||
@ -85,6 +112,7 @@ export const LogsPanel: React.FunctionComponent<LogsPanelProps> = ({
|
|||||||
logsSortOrder={sortOrder}
|
logsSortOrder={sortOrder}
|
||||||
enableLogDetails={enableLogDetails}
|
enableLogDetails={enableLogDetails}
|
||||||
previewLimit={isAscending ? logRows.length : undefined}
|
previewLimit={isAscending ? logRows.length : undefined}
|
||||||
|
onLogRowHover={onLogRowHover}
|
||||||
/>
|
/>
|
||||||
{showCommonLabels && isAscending && renderCommonLabels()}
|
{showCommonLabels && isAscending && renderCommonLabels()}
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user