mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Improve Explore performance but removing unnecessary re-renders (#29752)
* Fix Explore and Logs rerenders * Create ReturnToDashboardButton * Add test for new component * Remove queryKeys change on keystroke * Move rendering conditions to Button * Remove redundant logic
This commit is contained in:
parent
681cefbb18
commit
3ee2e49d9a
@ -6,28 +6,23 @@ import classNames from 'classnames';
|
|||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
|
|
||||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||||
import { Icon, IconButton, LegacyForms, SetInterval, Tooltip } from '@grafana/ui';
|
import { Icon, IconButton, SetInterval, Tooltip } from '@grafana/ui';
|
||||||
import { DataQuery, DataSourceInstanceSettings, RawTimeRange, TimeRange, TimeZone } from '@grafana/data';
|
import { DataSourceInstanceSettings, RawTimeRange, TimeRange, TimeZone } from '@grafana/data';
|
||||||
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
|
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
|
||||||
import { StoreState } from 'app/types/store';
|
import { StoreState } from 'app/types/store';
|
||||||
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
|
||||||
import { changeDatasource } from './state/datasource';
|
import { changeDatasource } from './state/datasource';
|
||||||
import { splitClose, splitOpen } from './state/main';
|
import { splitClose, splitOpen } from './state/main';
|
||||||
import { syncTimes, changeRefreshInterval } from './state/time';
|
import { syncTimes, changeRefreshInterval } from './state/time';
|
||||||
import { updateLocation } from 'app/core/actions';
|
|
||||||
import { getTimeZone } from '../profile/state/selectors';
|
import { getTimeZone } from '../profile/state/selectors';
|
||||||
import { updateTimeZoneForSession } from '../profile/state/reducers';
|
import { updateTimeZoneForSession } from '../profile/state/reducers';
|
||||||
import { getDashboardSrv } from '../dashboard/services/DashboardSrv';
|
|
||||||
import kbn from '../../core/utils/kbn';
|
|
||||||
import { ExploreTimeControls } from './ExploreTimeControls';
|
import { ExploreTimeControls } from './ExploreTimeControls';
|
||||||
import { LiveTailButton } from './LiveTailButton';
|
import { LiveTailButton } from './LiveTailButton';
|
||||||
import { ResponsiveButton } from './ResponsiveButton';
|
import { ResponsiveButton } from './ResponsiveButton';
|
||||||
import { RunButton } from './RunButton';
|
import { RunButton } from './RunButton';
|
||||||
import { LiveTailControls } from './useLiveTailControls';
|
import { LiveTailControls } from './useLiveTailControls';
|
||||||
import { setDashboardQueriesToUpdateOnLoad } from '../dashboard/state/reducers';
|
|
||||||
import { cancelQueries, clearQueries, runQueries } from './state/query';
|
import { cancelQueries, clearQueries, runQueries } from './state/query';
|
||||||
|
import ReturnToDashboardButton from './ReturnToDashboardButton';
|
||||||
const { ButtonSelect } = LegacyForms;
|
|
||||||
|
|
||||||
const getStyles = memoizeOne(() => {
|
const getStyles = memoizeOne(() => {
|
||||||
return {
|
return {
|
||||||
@ -56,8 +51,6 @@ interface StateProps {
|
|||||||
hasLiveOption: boolean;
|
hasLiveOption: boolean;
|
||||||
isLive: boolean;
|
isLive: boolean;
|
||||||
isPaused: boolean;
|
isPaused: boolean;
|
||||||
originPanelId?: number | null;
|
|
||||||
queries: DataQuery[];
|
|
||||||
datasourceLoading?: boolean | null;
|
datasourceLoading?: boolean | null;
|
||||||
containerWidth: number;
|
containerWidth: number;
|
||||||
datasourceName?: string;
|
datasourceName?: string;
|
||||||
@ -72,8 +65,6 @@ interface DispatchProps {
|
|||||||
split: typeof splitOpen;
|
split: typeof splitOpen;
|
||||||
syncTimes: typeof syncTimes;
|
syncTimes: typeof syncTimes;
|
||||||
changeRefreshInterval: typeof changeRefreshInterval;
|
changeRefreshInterval: typeof changeRefreshInterval;
|
||||||
updateLocation: typeof updateLocation;
|
|
||||||
setDashboardQueriesToUpdateOnLoad: typeof setDashboardQueriesToUpdateOnLoad;
|
|
||||||
onChangeTimeZone: typeof updateTimeZoneForSession;
|
onChangeTimeZone: typeof updateTimeZoneForSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,40 +97,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
|||||||
syncTimes(exploreId);
|
syncTimes(exploreId);
|
||||||
};
|
};
|
||||||
|
|
||||||
returnToPanel = async ({ withChanges = false } = {}) => {
|
|
||||||
const { originPanelId, queries } = this.props;
|
|
||||||
|
|
||||||
const dashboardSrv = getDashboardSrv();
|
|
||||||
const dash = dashboardSrv.getCurrent();
|
|
||||||
const titleSlug = kbn.slugifyForUrl(dash.title);
|
|
||||||
|
|
||||||
if (withChanges) {
|
|
||||||
this.props.setDashboardQueriesToUpdateOnLoad({
|
|
||||||
panelId: originPanelId!,
|
|
||||||
queries: this.cleanQueries(queries),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const query: any = {};
|
|
||||||
|
|
||||||
if (withChanges || dash.panelInEdit) {
|
|
||||||
query.editPanel = originPanelId;
|
|
||||||
} else if (dash.panelInView) {
|
|
||||||
query.viewPanel = originPanelId;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.updateLocation({ path: `/d/${dash.uid}/:${titleSlug}`, query });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove explore specific parameters from queries
|
|
||||||
private cleanQueries(queries: DataQuery[]) {
|
|
||||||
return queries.map((query: DataQuery & { context?: string }) => {
|
|
||||||
delete query.context;
|
|
||||||
delete query.key;
|
|
||||||
return query;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
datasourceMissing,
|
datasourceMissing,
|
||||||
@ -156,18 +113,11 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
|||||||
hasLiveOption,
|
hasLiveOption,
|
||||||
isLive,
|
isLive,
|
||||||
isPaused,
|
isPaused,
|
||||||
originPanelId,
|
|
||||||
containerWidth,
|
containerWidth,
|
||||||
onChangeTimeZone,
|
onChangeTimeZone,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const styles = getStyles();
|
const styles = getStyles();
|
||||||
const originDashboardIsEditable = originPanelId && Number.isInteger(originPanelId);
|
|
||||||
const panelReturnClasses = classNames('btn', 'navbar-button', {
|
|
||||||
'btn--radius-right-0': originDashboardIsEditable,
|
|
||||||
'navbar-button navbar-button--border-right-0': originDashboardIsEditable,
|
|
||||||
});
|
|
||||||
|
|
||||||
const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false;
|
const showSmallDataSourcePicker = (splitted ? containerWidth < 700 : containerWidth < 800) || false;
|
||||||
const showSmallTimePicker = splitted || containerWidth < 1210;
|
const showSmallTimePicker = splitted || containerWidth < 1210;
|
||||||
|
|
||||||
@ -213,25 +163,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
<ReturnToDashboardButton exploreId={exploreId} />
|
||||||
{originPanelId && Number.isInteger(originPanelId) && !splitted && (
|
|
||||||
<div className="explore-toolbar-content-item">
|
|
||||||
<Tooltip content={'Return to panel'} placement="bottom">
|
|
||||||
<button className={panelReturnClasses} onClick={() => this.returnToPanel()}>
|
|
||||||
<Icon name="arrow-left" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
{originDashboardIsEditable && (
|
|
||||||
<ButtonSelect
|
|
||||||
className="navbar-button--attached btn--radius-left-0$"
|
|
||||||
options={[{ label: 'Return to panel with changes', value: '' }]}
|
|
||||||
onChange={() => this.returnToPanel({ withChanges: true })}
|
|
||||||
maxMenuHeight={380}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{exploreId === 'left' && !splitted ? (
|
{exploreId === 'left' && !splitted ? (
|
||||||
<div className="explore-toolbar-content-item explore-icon-align">
|
<div className="explore-toolbar-content-item explore-icon-align">
|
||||||
<ResponsiveButton
|
<ResponsiveButton
|
||||||
@ -330,8 +262,6 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
|
|||||||
loading,
|
loading,
|
||||||
isLive,
|
isLive,
|
||||||
isPaused,
|
isPaused,
|
||||||
originPanelId,
|
|
||||||
queries,
|
|
||||||
containerWidth,
|
containerWidth,
|
||||||
} = exploreItem;
|
} = exploreItem;
|
||||||
|
|
||||||
@ -348,8 +278,6 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
|
|||||||
hasLiveOption,
|
hasLiveOption,
|
||||||
isLive,
|
isLive,
|
||||||
isPaused,
|
isPaused,
|
||||||
originPanelId,
|
|
||||||
queries,
|
|
||||||
syncedTimes,
|
syncedTimes,
|
||||||
containerWidth,
|
containerWidth,
|
||||||
};
|
};
|
||||||
@ -357,7 +285,6 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
|
|||||||
|
|
||||||
const mapDispatchToProps: DispatchProps = {
|
const mapDispatchToProps: DispatchProps = {
|
||||||
changeDatasource,
|
changeDatasource,
|
||||||
updateLocation,
|
|
||||||
changeRefreshInterval,
|
changeRefreshInterval,
|
||||||
clearAll: clearQueries,
|
clearAll: clearQueries,
|
||||||
cancelQueries,
|
cancelQueries,
|
||||||
@ -365,7 +292,6 @@ const mapDispatchToProps: DispatchProps = {
|
|||||||
closeSplit: splitClose,
|
closeSplit: splitClose,
|
||||||
split: splitOpen,
|
split: splitOpen,
|
||||||
syncTimes,
|
syncTimes,
|
||||||
setDashboardQueriesToUpdateOnLoad,
|
|
||||||
onChangeTimeZone: updateTimeZoneForSession,
|
onChangeTimeZone: updateTimeZoneForSession,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
41
public/app/features/explore/ReturnToDashboardButton.test.tsx
Normal file
41
public/app/features/explore/ReturnToDashboardButton.test.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React, { ComponentProps } from 'react';
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import { UnconnectedReturnToDashboardButton as ReturnToDashboardButton } from './ReturnToDashboardButton';
|
||||||
|
import { ExploreId } from 'app/types/explore';
|
||||||
|
|
||||||
|
const createProps = (propsOverride?: Partial<ComponentProps<typeof ReturnToDashboardButton>>) => {
|
||||||
|
const defaultProps = {
|
||||||
|
originPanelId: 1,
|
||||||
|
splitted: false,
|
||||||
|
exploreId: ExploreId.left,
|
||||||
|
queries: [],
|
||||||
|
updateLocation: jest.fn(),
|
||||||
|
setDashboardQueriesToUpdateOnLoad: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Object.assign(defaultProps, propsOverride) as ComponentProps<typeof ReturnToDashboardButton>;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ReturnToDashboardButton', () => {
|
||||||
|
it('should render 2 buttons if originPanelId is provided', () => {
|
||||||
|
render(<ReturnToDashboardButton {...createProps()} />);
|
||||||
|
expect(screen.getAllByTestId(/returnButton/i)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
it('should not render any button if originPanelId is not provided', () => {
|
||||||
|
render(<ReturnToDashboardButton {...createProps({ originPanelId: undefined })} />);
|
||||||
|
expect(screen.queryByTestId(/returnButton/i)).toBeNull();
|
||||||
|
});
|
||||||
|
it('should not render any button if split view', () => {
|
||||||
|
render(<ReturnToDashboardButton {...createProps({ splitted: true })} />);
|
||||||
|
expect(screen.queryByTestId(/returnButton/i)).toBeNull();
|
||||||
|
});
|
||||||
|
it('should show option to return to dashboard with changes', () => {
|
||||||
|
render(<ReturnToDashboardButton {...createProps()} />);
|
||||||
|
const returnWithChangesButton = screen.getByTestId('returnButtonWithChanges');
|
||||||
|
const selectButton = returnWithChangesButton.querySelector('.select-button');
|
||||||
|
if (selectButton) {
|
||||||
|
fireEvent.click(selectButton);
|
||||||
|
}
|
||||||
|
expect(screen.getAllByText('Return to panel with changes')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
118
public/app/features/explore/ReturnToDashboardButton.tsx
Normal file
118
public/app/features/explore/ReturnToDashboardButton.tsx
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import { Icon, Tooltip, LegacyForms } from '@grafana/ui';
|
||||||
|
import { DataQuery } from '@grafana/data';
|
||||||
|
|
||||||
|
import kbn from '../../core/utils/kbn';
|
||||||
|
import { getDashboardSrv } from '../dashboard/services/DashboardSrv';
|
||||||
|
import { StoreState } from 'app/types';
|
||||||
|
import { ExploreId } from 'app/types/explore';
|
||||||
|
import { updateLocation } from 'app/core/actions';
|
||||||
|
import { setDashboardQueriesToUpdateOnLoad } from '../dashboard/state/reducers';
|
||||||
|
|
||||||
|
const { ButtonSelect } = LegacyForms;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
exploreId: ExploreId;
|
||||||
|
splitted: boolean;
|
||||||
|
queries: DataQuery[];
|
||||||
|
originPanelId?: number | null;
|
||||||
|
updateLocation: typeof updateLocation;
|
||||||
|
setDashboardQueriesToUpdateOnLoad: typeof setDashboardQueriesToUpdateOnLoad;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UnconnectedReturnToDashboardButton: FC<Props> = ({
|
||||||
|
originPanelId,
|
||||||
|
updateLocation,
|
||||||
|
setDashboardQueriesToUpdateOnLoad,
|
||||||
|
queries,
|
||||||
|
splitted,
|
||||||
|
}) => {
|
||||||
|
const withOriginId = originPanelId && Number.isInteger(originPanelId);
|
||||||
|
|
||||||
|
// If in split mode, or no origin id, escape early and return null
|
||||||
|
if (splitted || !withOriginId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelReturnClasses = classNames('btn', 'navbar-button', {
|
||||||
|
'btn--radius-right-0': withOriginId,
|
||||||
|
'navbar-button navbar-button--border-right-0': withOriginId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cleanQueries = (queries: DataQuery[]) => {
|
||||||
|
return queries.map((query: DataQuery & { context?: string }) => {
|
||||||
|
delete query.context;
|
||||||
|
delete query.key;
|
||||||
|
return query;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const returnToPanel = async ({ withChanges = false } = {}) => {
|
||||||
|
const dashboardSrv = getDashboardSrv();
|
||||||
|
const dash = dashboardSrv.getCurrent();
|
||||||
|
const titleSlug = kbn.slugifyForUrl(dash.title);
|
||||||
|
|
||||||
|
if (withChanges) {
|
||||||
|
setDashboardQueriesToUpdateOnLoad({
|
||||||
|
panelId: originPanelId!,
|
||||||
|
queries: cleanQueries(queries),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const query: any = {};
|
||||||
|
|
||||||
|
if (withChanges || dash.panelInEdit) {
|
||||||
|
query.editPanel = originPanelId;
|
||||||
|
} else if (dash.panelInView) {
|
||||||
|
query.viewPanel = originPanelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLocation({ path: `/d/${dash.uid}/:${titleSlug}`, query });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="explore-toolbar-content-item">
|
||||||
|
<Tooltip content={'Return to panel'} placement="bottom">
|
||||||
|
<button
|
||||||
|
data-testid="returnButton"
|
||||||
|
title={'Return to panel'}
|
||||||
|
className={panelReturnClasses}
|
||||||
|
onClick={() => returnToPanel()}
|
||||||
|
>
|
||||||
|
<Icon name="arrow-left" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<div data-testid="returnButtonWithChanges">
|
||||||
|
<ButtonSelect
|
||||||
|
className="navbar-button--attached btn--radius-left-0$"
|
||||||
|
options={[{ label: 'Return to panel with changes', value: '' }]}
|
||||||
|
onChange={() => returnToPanel({ withChanges: true })}
|
||||||
|
maxMenuHeight={380}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
|
||||||
|
const explore = state.explore;
|
||||||
|
const splitted = state.explore.split;
|
||||||
|
const { datasourceInstance, queries, originPanelId } = explore[exploreId];
|
||||||
|
|
||||||
|
return {
|
||||||
|
exploreId,
|
||||||
|
datasourceInstance,
|
||||||
|
queries,
|
||||||
|
originPanelId,
|
||||||
|
splitted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
updateLocation,
|
||||||
|
setDashboardQueriesToUpdateOnLoad,
|
||||||
|
};
|
||||||
|
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(UnconnectedReturnToDashboardButton));
|
@ -1,5 +1,5 @@
|
|||||||
import { AnyAction } from 'redux';
|
import { AnyAction } from 'redux';
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
import { getQueryKeys } from 'app/core/utils/explore';
|
import { getQueryKeys } from 'app/core/utils/explore';
|
||||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||||
import { queryReducer } from './query';
|
import { queryReducer } from './query';
|
||||||
@ -258,8 +258,14 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (highlightLogsExpressionAction.match(action)) {
|
if (highlightLogsExpressionAction.match(action)) {
|
||||||
const { expressions } = action.payload;
|
const { expressions: newExpressions } = action.payload;
|
||||||
return { ...state, logsHighlighterExpressions: expressions };
|
const { logsHighlighterExpressions: currentExpressions } = state;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
// Prevents re-renders. As logsHighlighterExpressions [] comes from datasource, we cannot control if it returns new array or not.
|
||||||
|
logsHighlighterExpressions: isEqual(newExpressions, currentExpressions) ? currentExpressions : newExpressions,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changeDedupStrategyAction.match(action)) {
|
if (changeDedupStrategyAction.match(action)) {
|
||||||
|
@ -477,7 +477,6 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
queries: nextQueries,
|
queries: nextQueries,
|
||||||
queryKeys: getQueryKeys(nextQueries, state.datasourceInstance),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user