Explore: Fix shared crosshair for logs, logsvolume and graph panels (#57892)

* Explore: enable shared corsshair for logs, logsvolume & graph panel

* avoid recreating a scoped bus on every render
This commit is contained in:
Giordano Ricci 2022-11-03 09:55:02 +00:00 committed by GitHub
parent 3558cadb7e
commit e6b088fbf5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 73 additions and 17 deletions

View File

@ -3897,9 +3897,6 @@ exports[`better eslint`] = {
"public/app/features/explore/Explore.test.tsx:5381": [ "public/app/features/explore/Explore.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"] [0, 0, 0, "Unexpected any. Specify a different type.", "0"]
], ],
"public/app/features/explore/Explore.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/explore/ExploreGraph.tsx:5381": [ "public/app/features/explore/ExploreGraph.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"] [0, 0, 0, "Do not use any type assertions.", "0"]
], ],

View File

@ -3,7 +3,7 @@ import React from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { AutoSizerProps } from 'react-virtualized-auto-sizer'; import { AutoSizerProps } from 'react-virtualized-auto-sizer';
import { DataSourceApi, LoadingState, CoreApp, createTheme } from '@grafana/data'; import { DataSourceApi, LoadingState, CoreApp, createTheme, EventBusSrv } from '@grafana/data';
import { configureStore } from 'app/store/configureStore'; import { configureStore } from 'app/store/configureStore';
import { ExploreId } from 'app/types/explore'; import { ExploreId } from 'app/types/explore';
@ -86,6 +86,7 @@ const dummyProps: Props = {
splitted: false, splitted: false,
changeGraphStyle: () => {}, changeGraphStyle: () => {},
graphStyle: 'lines', graphStyle: 'lines',
eventBus: new EventBusSrv(),
}; };
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => { jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {

View File

@ -4,7 +4,6 @@ import memoizeOne from 'memoize-one';
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import { connect, ConnectedProps } from 'react-redux'; import { connect, ConnectedProps } from 'react-redux';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { compose } from 'redux';
import { Unsubscribable } from 'rxjs'; import { Unsubscribable } from 'rxjs';
import { import {
@ -14,6 +13,7 @@ import {
LoadingState, LoadingState,
QueryFixAction, QueryFixAction,
RawTimeRange, RawTimeRange,
EventBus,
SplitOpenOptions, SplitOpenOptions,
} from '@grafana/data'; } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
@ -87,6 +87,7 @@ const getStyles = (theme: GrafanaTheme2) => {
export interface ExploreProps extends Themeable2 { export interface ExploreProps extends Themeable2 {
exploreId: ExploreId; exploreId: ExploreId;
theme: GrafanaTheme2; theme: GrafanaTheme2;
eventBus: EventBus;
} }
enum ExploreDrawer { enum ExploreDrawer {
@ -128,12 +129,16 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
scrollElement: HTMLDivElement | undefined; scrollElement: HTMLDivElement | undefined;
absoluteTimeUnsubsciber: Unsubscribable | undefined; absoluteTimeUnsubsciber: Unsubscribable | undefined;
topOfViewRef = createRef<HTMLDivElement>(); topOfViewRef = createRef<HTMLDivElement>();
graphEventBus: EventBus;
logsEventBus: EventBus;
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
openDrawer: undefined, openDrawer: undefined,
}; };
this.graphEventBus = props.eventBus.newScopedBus('graph', { onlyLocal: false });
this.logsEventBus = props.eventBus.newScopedBus('logs', { onlyLocal: false });
} }
componentDidMount() { componentDidMount() {
@ -291,6 +296,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
splitOpenFn={this.onSplitOpen('graph')} splitOpenFn={this.onSplitOpen('graph')}
loadingState={queryResponse.state} loadingState={queryResponse.state}
anchorToZero={false} anchorToZero={false}
eventBus={this.graphEventBus}
/> />
</Collapse> </Collapse>
); );
@ -324,6 +330,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
onStartScanning={this.onStartScanning} onStartScanning={this.onStartScanning}
onStopScanning={this.onStopScanning} onStopScanning={this.onStopScanning}
scrollElement={this.scrollElement} scrollElement={this.scrollElement}
eventBus={this.logsEventBus}
splitOpenFn={this.onSplitOpen('logs')} splitOpenFn={this.onSplitOpen('logs')}
/> />
); );
@ -550,4 +557,4 @@ const mapDispatchToProps = {
const connector = connect(mapStateToProps, mapDispatchToProps); const connector = connect(mapStateToProps, mapDispatchToProps);
export default compose(connector, withTheme2)(Explore) as React.ComponentType<{ exploreId: ExploreId }>; export default withTheme2(connector(Explore));

View File

@ -18,6 +18,8 @@ import {
LoadingState, LoadingState,
SplitOpen, SplitOpen,
TimeZone, TimeZone,
DashboardCursorSync,
EventBus,
} from '@grafana/data'; } from '@grafana/data';
import { PanelRenderer } from '@grafana/runtime'; import { PanelRenderer } from '@grafana/runtime';
import { GraphDrawStyle, LegendDisplayMode, TooltipDisplayMode, SortOrder } from '@grafana/schema'; import { GraphDrawStyle, LegendDisplayMode, TooltipDisplayMode, SortOrder } from '@grafana/schema';
@ -29,7 +31,6 @@ import {
useStyles2, useStyles2,
useTheme2, useTheme2,
} from '@grafana/ui'; } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import { defaultGraphConfig, getGraphFieldConfig } from 'app/plugins/panel/timeseries/config'; import { defaultGraphConfig, getGraphFieldConfig } from 'app/plugins/panel/timeseries/config';
import { TimeSeriesOptions } from 'app/plugins/panel/timeseries/types'; import { TimeSeriesOptions } from 'app/plugins/panel/timeseries/types';
@ -54,6 +55,7 @@ interface Props {
onChangeTime: (timeRange: AbsoluteTimeRange) => void; onChangeTime: (timeRange: AbsoluteTimeRange) => void;
graphStyle: ExploreGraphStyle; graphStyle: ExploreGraphStyle;
anchorToZero: boolean; anchorToZero: boolean;
eventBus: EventBus;
} }
export function ExploreGraph({ export function ExploreGraph({
@ -70,6 +72,7 @@ export function ExploreGraph({
graphStyle, graphStyle,
tooltipDisplayMode = TooltipDisplayMode.Single, tooltipDisplayMode = TooltipDisplayMode.Single,
anchorToZero, anchorToZero,
eventBus,
}: Props) { }: Props) {
const theme = useTheme2(); const theme = useTheme2();
const [showAllTimeSeries, setShowAllTimeSeries] = useState(false); const [showAllTimeSeries, setShowAllTimeSeries] = useState(false);
@ -142,7 +145,8 @@ export function ExploreGraph({
const seriesToShow = showAllTimeSeries ? dataWithConfig : dataWithConfig.slice(0, MAX_NUMBER_OF_TIME_SERIES); const seriesToShow = showAllTimeSeries ? dataWithConfig : dataWithConfig.slice(0, MAX_NUMBER_OF_TIME_SERIES);
const panelContext: PanelContext = { const panelContext: PanelContext = {
eventBus: appEvents, eventBus,
sync: () => DashboardCursorSync.Crosshair,
onSplitOpen: splitOpenFn, onSplitOpen: splitOpenFn,
onToggleSeriesVisibility(label: string, mode: SeriesVisibilityChangeMode) { onToggleSeriesVisibility(label: string, mode: SeriesVisibilityChangeMode) {
setBaseStructureRev((r) => r + 1); setBaseStructureRev((r) => r + 1);

View File

@ -3,7 +3,7 @@ import memoizeOne from 'memoize-one';
import React from 'react'; import React from 'react';
import { connect, ConnectedProps } from 'react-redux'; import { connect, ConnectedProps } from 'react-redux';
import { ExploreUrlState, EventBusExtended, EventBusSrv, GrafanaTheme2 } from '@grafana/data'; import { ExploreUrlState, EventBusExtended, EventBusSrv, GrafanaTheme2, EventBus } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { Themeable2, withTheme2 } from '@grafana/ui'; import { Themeable2, withTheme2 } from '@grafana/ui';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
@ -50,6 +50,7 @@ interface OwnProps extends Themeable2 {
exploreId: ExploreId; exploreId: ExploreId;
urlQuery: string; urlQuery: string;
split: boolean; split: boolean;
eventBus: EventBus;
} }
interface Props extends OwnProps, ConnectedProps<typeof connector> {} interface Props extends OwnProps, ConnectedProps<typeof connector> {}
@ -143,12 +144,12 @@ class ExplorePaneContainerUnconnected extends React.PureComponent<Props> {
}; };
render() { render() {
const { theme, split, exploreId, initialized } = this.props; const { theme, split, exploreId, initialized, eventBus } = this.props;
const styles = getStyles(theme); const styles = getStyles(theme);
const exploreClass = cx(styles.explore, split && styles.exploreSplit); const exploreClass = cx(styles.explore, split && styles.exploreSplit);
return ( return (
<div className={exploreClass} ref={this.getRef} data-testid={selectors.pages.Explore.General.container}> <div className={exploreClass} ref={this.getRef} data-testid={selectors.pages.Explore.General.container}>
{initialized && <Explore exploreId={exploreId} />} {initialized && <Explore exploreId={exploreId} eventBus={eventBus} />}
</div> </div>
); );
} }

View File

@ -1,7 +1,7 @@
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { LoadingState, LogLevel, LogRowModel, MutableDataFrame, toUtc } from '@grafana/data'; import { LoadingState, LogLevel, LogRowModel, MutableDataFrame, toUtc, EventBusSrv } from '@grafana/data';
import { ExploreId } from 'app/types'; import { ExploreId } from 'app/types';
import { Logs } from './Logs'; import { Logs } from './Logs';
@ -37,6 +37,7 @@ describe('Logs', () => {
getFieldLinks={() => { getFieldLinks={() => {
return []; return [];
}} }}
eventBus={new EventBusSrv()}
/> />
); );
}; };

View File

@ -23,6 +23,9 @@ import {
SplitOpen, SplitOpen,
DataQueryResponse, DataQueryResponse,
CoreApp, CoreApp,
DataHoverEvent,
DataHoverClearEvent,
EventBus,
} from '@grafana/data'; } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime'; import { reportInteraction } from '@grafana/runtime';
import { import {
@ -79,6 +82,7 @@ interface Props extends Themeable2 {
getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>; getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
addResultsToCache: () => void; addResultsToCache: () => void;
clearCache: () => void; clearCache: () => void;
eventBus: EventBus;
} }
interface State { interface State {
@ -108,6 +112,7 @@ class UnthemedLogs extends PureComponent<Props, State> {
flipOrderTimer?: number; flipOrderTimer?: number;
cancelFlippingTimer?: number; cancelFlippingTimer?: number;
topLogsRef = createRef<HTMLDivElement>(); topLogsRef = createRef<HTMLDivElement>();
logsVolumeEventBus: EventBus;
state: State = { state: State = {
showLabels: store.getBool(SETTINGS_KEYS.showLabels, false), showLabels: store.getBool(SETTINGS_KEYS.showLabels, false),
@ -122,6 +127,11 @@ class UnthemedLogs extends PureComponent<Props, State> {
forceEscape: false, forceEscape: false,
}; };
constructor(props: Props) {
super(props);
this.logsVolumeEventBus = props.eventBus.newScopedBus('logsvolume', { onlyLocal: false });
}
componentWillUnmount() { componentWillUnmount() {
if (this.flipOrderTimer) { if (this.flipOrderTimer) {
window.clearTimeout(this.flipOrderTimer); window.clearTimeout(this.flipOrderTimer);
@ -132,6 +142,20 @@ class UnthemedLogs extends PureComponent<Props, State> {
} }
} }
onLogRowHover = (row?: LogRowModel) => {
if (!row) {
this.props.eventBus.publish(new DataHoverClearEvent());
} else {
this.props.eventBus.publish(
new DataHoverEvent({
point: {
time: row.timeEpochMs,
},
})
);
}
};
onChangeLogsSortOrder = () => { onChangeLogsSortOrder = () => {
this.setState({ isFlipping: true }); this.setState({ isFlipping: true });
// we are using setTimeout here to make sure that disabled button is rendered before the rendering of reordered logs // we are using setTimeout here to make sure that disabled button is rendered before the rendering of reordered logs
@ -367,6 +391,7 @@ class UnthemedLogs extends PureComponent<Props, State> {
splitOpen={splitOpen} splitOpen={splitOpen}
onLoadLogsVolume={() => loadLogsVolumeData(exploreId)} onLoadLogsVolume={() => loadLogsVolumeData(exploreId)}
onHiddenSeriesChanged={this.onToggleLogLevel} onHiddenSeriesChanged={this.onToggleLogLevel}
eventBus={this.logsVolumeEventBus}
/> />
)} )}
</Collapse> </Collapse>
@ -480,6 +505,7 @@ class UnthemedLogs extends PureComponent<Props, State> {
onClickHideDetectedField={this.hideDetectedField} onClickHideDetectedField={this.hideDetectedField}
app={CoreApp.Explore} app={CoreApp.Explore}
scrollElement={scrollElement} scrollElement={scrollElement}
onLogRowHover={this.onLogRowHover}
/> />
</div> </div>
<LogsNavigation <LogsNavigation

View File

@ -8,6 +8,7 @@ import {
LoadingState, LoadingState,
LogRowModel, LogRowModel,
RawTimeRange, RawTimeRange,
EventBus,
SplitOpen, SplitOpen,
} from '@grafana/data'; } from '@grafana/data';
import { Collapse } from '@grafana/ui'; import { Collapse } from '@grafana/ui';
@ -35,6 +36,7 @@ interface LogsContainerProps extends PropsFromRedux {
onClickFilterOutLabel?: (key: string, value: string) => void; onClickFilterOutLabel?: (key: string, value: string) => void;
onStartScanning: () => void; onStartScanning: () => void;
onStopScanning: () => void; onStopScanning: () => void;
eventBus: EventBus;
splitOpenFn: SplitOpen; splitOpenFn: SplitOpen;
} }
@ -156,6 +158,7 @@ class LogsContainer extends PureComponent<LogsContainerProps> {
addResultsToCache={() => addResultsToCache(exploreId)} addResultsToCache={() => addResultsToCache(exploreId)}
clearCache={() => clearCache(exploreId)} clearCache={() => clearCache(exploreId)}
scrollElement={scrollElement} scrollElement={scrollElement}
eventBus={this.props.eventBus}
/> />
</LogsCrossFadeTransition> </LogsCrossFadeTransition>
</> </>

View File

@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { DataQueryResponse, LoadingState } from '@grafana/data'; import { DataQueryResponse, LoadingState, EventBusSrv } from '@grafana/data';
import { LogsVolumePanel } from './LogsVolumePanel'; import { LogsVolumePanel } from './LogsVolumePanel';
@ -25,6 +25,7 @@ function renderPanel(logsVolumeData?: DataQueryResponse) {
logLinesBasedDataVisibleRange={undefined} logLinesBasedDataVisibleRange={undefined}
onLoadLogsVolume={() => {}} onLoadLogsVolume={() => {}}
onHiddenSeriesChanged={() => null} onHiddenSeriesChanged={() => null}
eventBus={new EventBusSrv()}
/> />
); );
} }

View File

@ -9,6 +9,7 @@ import {
LoadingState, LoadingState,
SplitOpen, SplitOpen,
TimeZone, TimeZone,
EventBus,
} from '@grafana/data'; } from '@grafana/data';
import { Alert, Button, Collapse, InlineField, TooltipDisplayMode, useStyles2, useTheme2 } from '@grafana/ui'; import { Alert, Button, Collapse, InlineField, TooltipDisplayMode, useStyles2, useTheme2 } from '@grafana/ui';
@ -25,6 +26,7 @@ type Props = {
onUpdateTimeRange: (timeRange: AbsoluteTimeRange) => void; onUpdateTimeRange: (timeRange: AbsoluteTimeRange) => void;
onLoadLogsVolume: () => void; onLoadLogsVolume: () => void;
onHiddenSeriesChanged: (hiddenSeries: string[]) => void; onHiddenSeriesChanged: (hiddenSeries: string[]) => void;
eventBus: EventBus;
}; };
const SHORT_ERROR_MESSAGE_LIMIT = 100; const SHORT_ERROR_MESSAGE_LIMIT = 100;
@ -131,6 +133,7 @@ export function LogsVolumePanel(props: Props) {
tooltipDisplayMode={TooltipDisplayMode.Multi} tooltipDisplayMode={TooltipDisplayMode.Multi}
onHiddenSeriesChanged={onHiddenSeriesChanged} onHiddenSeriesChanged={onHiddenSeriesChanged}
anchorToZero anchorToZero
eventBus={props.eventBus}
/> />
); );
} else { } else {

View File

@ -1,8 +1,8 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React, { useEffect } from 'react'; import React, { useEffect, useRef } from 'react';
import { locationService } from '@grafana/runtime'; import { locationService } from '@grafana/runtime';
import { ErrorBoundaryAlert } from '@grafana/ui'; import { ErrorBoundaryAlert, usePanelContext } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext'; import { useGrafana } from 'app/core/context/GrafanaContext';
import { useAppNotification } from 'app/core/copy/appNotification'; import { useAppNotification } from 'app/core/copy/appNotification';
import { useNavModel } from 'app/core/hooks/useNavModel'; import { useNavModel } from 'app/core/hooks/useNavModel';
@ -38,6 +38,8 @@ function Wrapper(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) {
const navModel = useNavModel('explore'); const navModel = useNavModel('explore');
const { get } = useCorrelations(); const { get } = useCorrelations();
const { warning } = useAppNotification(); const { warning } = useAppNotification();
const panelCtx = usePanelContext();
const eventBus = useRef(panelCtx.eventBus.newScopedBus('explore', { onlyLocal: false }));
useEffect(() => { useEffect(() => {
//This is needed for breadcrumbs and topnav. //This is needed for breadcrumbs and topnav.
@ -102,11 +104,21 @@ function Wrapper(props: GrafanaRouteComponentProps<{}, ExploreQueryParams>) {
<ExploreActions exploreIdLeft={ExploreId.left} exploreIdRight={ExploreId.right} /> <ExploreActions exploreIdLeft={ExploreId.left} exploreIdRight={ExploreId.right} />
<div className={styles.exploreWrapper}> <div className={styles.exploreWrapper}>
<ErrorBoundaryAlert style="page"> <ErrorBoundaryAlert style="page">
<ExplorePaneContainer split={hasSplit} exploreId={ExploreId.left} urlQuery={queryParams.left} /> <ExplorePaneContainer
split={hasSplit}
exploreId={ExploreId.left}
urlQuery={queryParams.left}
eventBus={eventBus.current}
/>
</ErrorBoundaryAlert> </ErrorBoundaryAlert>
{hasSplit && ( {hasSplit && (
<ErrorBoundaryAlert style="page"> <ErrorBoundaryAlert style="page">
<ExplorePaneContainer split={hasSplit} exploreId={ExploreId.right} urlQuery={queryParams.right} /> <ExplorePaneContainer
split={hasSplit}
exploreId={ExploreId.right}
urlQuery={queryParams.right}
eventBus={eventBus.current}
/>
</ErrorBoundaryAlert> </ErrorBoundaryAlert>
)} )}
</div> </div>