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": [
[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": [
[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 { 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 { ExploreId } from 'app/types/explore';
@ -86,6 +86,7 @@ const dummyProps: Props = {
splitted: false,
changeGraphStyle: () => {},
graphStyle: 'lines',
eventBus: new EventBusSrv(),
};
jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => {

View File

@ -4,7 +4,6 @@ import memoizeOne from 'memoize-one';
import React, { createRef } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import AutoSizer from 'react-virtualized-auto-sizer';
import { compose } from 'redux';
import { Unsubscribable } from 'rxjs';
import {
@ -14,6 +13,7 @@ import {
LoadingState,
QueryFixAction,
RawTimeRange,
EventBus,
SplitOpenOptions,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
@ -87,6 +87,7 @@ const getStyles = (theme: GrafanaTheme2) => {
export interface ExploreProps extends Themeable2 {
exploreId: ExploreId;
theme: GrafanaTheme2;
eventBus: EventBus;
}
enum ExploreDrawer {
@ -128,12 +129,16 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
scrollElement: HTMLDivElement | undefined;
absoluteTimeUnsubsciber: Unsubscribable | undefined;
topOfViewRef = createRef<HTMLDivElement>();
graphEventBus: EventBus;
logsEventBus: EventBus;
constructor(props: Props) {
super(props);
this.state = {
openDrawer: undefined,
};
this.graphEventBus = props.eventBus.newScopedBus('graph', { onlyLocal: false });
this.logsEventBus = props.eventBus.newScopedBus('logs', { onlyLocal: false });
}
componentDidMount() {
@ -291,6 +296,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
splitOpenFn={this.onSplitOpen('graph')}
loadingState={queryResponse.state}
anchorToZero={false}
eventBus={this.graphEventBus}
/>
</Collapse>
);
@ -324,6 +330,7 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
onStartScanning={this.onStartScanning}
onStopScanning={this.onStopScanning}
scrollElement={this.scrollElement}
eventBus={this.logsEventBus}
splitOpenFn={this.onSplitOpen('logs')}
/>
);
@ -550,4 +557,4 @@ const 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,
SplitOpen,
TimeZone,
DashboardCursorSync,
EventBus,
} from '@grafana/data';
import { PanelRenderer } from '@grafana/runtime';
import { GraphDrawStyle, LegendDisplayMode, TooltipDisplayMode, SortOrder } from '@grafana/schema';
@ -29,7 +31,6 @@ import {
useStyles2,
useTheme2,
} from '@grafana/ui';
import appEvents from 'app/core/app_events';
import { defaultGraphConfig, getGraphFieldConfig } from 'app/plugins/panel/timeseries/config';
import { TimeSeriesOptions } from 'app/plugins/panel/timeseries/types';
@ -54,6 +55,7 @@ interface Props {
onChangeTime: (timeRange: AbsoluteTimeRange) => void;
graphStyle: ExploreGraphStyle;
anchorToZero: boolean;
eventBus: EventBus;
}
export function ExploreGraph({
@ -70,6 +72,7 @@ export function ExploreGraph({
graphStyle,
tooltipDisplayMode = TooltipDisplayMode.Single,
anchorToZero,
eventBus,
}: Props) {
const theme = useTheme2();
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 panelContext: PanelContext = {
eventBus: appEvents,
eventBus,
sync: () => DashboardCursorSync.Crosshair,
onSplitOpen: splitOpenFn,
onToggleSeriesVisibility(label: string, mode: SeriesVisibilityChangeMode) {
setBaseStructureRev((r) => r + 1);

View File

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

View File

@ -1,7 +1,7 @@
import { render, screen, fireEvent } from '@testing-library/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 { Logs } from './Logs';
@ -37,6 +37,7 @@ describe('Logs', () => {
getFieldLinks={() => {
return [];
}}
eventBus={new EventBusSrv()}
/>
);
};

View File

@ -23,6 +23,9 @@ import {
SplitOpen,
DataQueryResponse,
CoreApp,
DataHoverEvent,
DataHoverClearEvent,
EventBus,
} from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import {
@ -79,6 +82,7 @@ interface Props extends Themeable2 {
getFieldLinks: (field: Field, rowIndex: number) => Array<LinkModel<Field>>;
addResultsToCache: () => void;
clearCache: () => void;
eventBus: EventBus;
}
interface State {
@ -108,6 +112,7 @@ class UnthemedLogs extends PureComponent<Props, State> {
flipOrderTimer?: number;
cancelFlippingTimer?: number;
topLogsRef = createRef<HTMLDivElement>();
logsVolumeEventBus: EventBus;
state: State = {
showLabels: store.getBool(SETTINGS_KEYS.showLabels, false),
@ -122,6 +127,11 @@ class UnthemedLogs extends PureComponent<Props, State> {
forceEscape: false,
};
constructor(props: Props) {
super(props);
this.logsVolumeEventBus = props.eventBus.newScopedBus('logsvolume', { onlyLocal: false });
}
componentWillUnmount() {
if (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 = () => {
this.setState({ isFlipping: true });
// 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}
onLoadLogsVolume={() => loadLogsVolumeData(exploreId)}
onHiddenSeriesChanged={this.onToggleLogLevel}
eventBus={this.logsVolumeEventBus}
/>
)}
</Collapse>
@ -480,6 +505,7 @@ class UnthemedLogs extends PureComponent<Props, State> {
onClickHideDetectedField={this.hideDetectedField}
app={CoreApp.Explore}
scrollElement={scrollElement}
onLogRowHover={this.onLogRowHover}
/>
</div>
<LogsNavigation

View File

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

View File

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

View File

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

View File

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