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 { 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';
|
||||
}
|
||||
|
||||
|
@ -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: () => {
|
||||
|
@ -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}
|
||||
|
@ -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 && (
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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());
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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) => {
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user