Commit b0bd242e by Andrej Ocenas Committed by GitHub

Explore/Refactor: Simplify URL handling (#29173)

* Inline datasource actions into initialisation

* Simplify url handling

* Add comments

* Remove split property from state and split Explore.tsx to 2 components

* Add comments

* Simplify and fix splitOpen and splitClose actions

* Update public/app/features/explore/ExplorePaneContainer.tsx

Co-authored-by: Giordano Ricci <gio.ricci@grafana.com>

* Update public/app/features/explore/state/explorePane.test.ts

Co-authored-by: Giordano Ricci <gio.ricci@grafana.com>

* Update public/app/features/explore/Wrapper.tsx

Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>

* Fix test

* Fix lint

Co-authored-by: Giordano Ricci <gio.ricci@grafana.com>
Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
parent 9629dded
...@@ -14,6 +14,6 @@ export { PanelOptionsEditorBuilder, FieldConfigEditorBuilder } from './OptionsUI ...@@ -14,6 +14,6 @@ export { PanelOptionsEditorBuilder, FieldConfigEditorBuilder } from './OptionsUI
export { getMappedValue } from './valueMappings'; export { getMappedValue } from './valueMappings';
export { getFlotPairs, getFlotPairsConstant } from './flotPairs'; export { getFlotPairs, getFlotPairsConstant } from './flotPairs';
export { locationUtil } from './location'; export { locationUtil } from './location';
export { urlUtil, UrlQueryMap, UrlQueryValue } from './url'; export { urlUtil, UrlQueryMap, UrlQueryValue, serializeStateToUrlParam } from './url';
export { DataLinkBuiltInVars, mapInternalLinkToExplore } from './dataLinks'; export { DataLinkBuiltInVars, mapInternalLinkToExplore } from './dataLinks';
export { DocsId } from './docs'; export { DocsId } from './docs';
...@@ -22,16 +22,7 @@ const dummyProps: ExploreProps = { ...@@ -22,16 +22,7 @@ const dummyProps: ExploreProps = {
datasourceMissing: false, datasourceMissing: false,
exploreId: ExploreId.left, exploreId: ExploreId.left,
loading: false, loading: false,
initializeExplore: jest.fn(),
initialized: true,
modifyQueries: jest.fn(), modifyQueries: jest.fn(),
update: {
datasource: false,
queries: false,
range: false,
mode: false,
},
refreshExplore: jest.fn(),
scanning: false, scanning: false,
scanRange: { scanRange: {
from: '0', from: '0',
...@@ -40,18 +31,7 @@ const dummyProps: ExploreProps = { ...@@ -40,18 +31,7 @@ const dummyProps: ExploreProps = {
scanStart: jest.fn(), scanStart: jest.fn(),
scanStopAction: scanStopAction, scanStopAction: scanStopAction,
setQueries: jest.fn(), setQueries: jest.fn(),
split: false,
queryKeys: [], queryKeys: [],
initialDatasource: 'test',
initialQueries: [],
initialRange: {
from: toUtc('2019-01-01 10:00:00'),
to: toUtc('2019-01-01 16:00:00'),
raw: {
from: 'now-6h',
to: 'now',
},
},
isLive: false, isLive: false,
syncedTimes: false, syncedTimes: false,
updateTimeRange: jest.fn(), updateTimeRange: jest.fn(),
......
...@@ -15,36 +15,24 @@ import { ...@@ -15,36 +15,24 @@ import {
LoadingState, LoadingState,
PanelData, PanelData,
RawTimeRange, RawTimeRange,
TimeRange,
TimeZone, TimeZone,
ExploreUrlState,
LogsModel, LogsModel,
EventBusExtended,
EventBusSrv,
TraceViewData, TraceViewData,
DataFrame, DataFrame,
} from '@grafana/data'; } from '@grafana/data';
import store from 'app/core/store';
import LogsContainer from './LogsContainer'; import LogsContainer from './LogsContainer';
import QueryRows from './QueryRows'; import QueryRows from './QueryRows';
import TableContainer from './TableContainer'; import TableContainer from './TableContainer';
import RichHistoryContainer from './RichHistory/RichHistoryContainer'; import RichHistoryContainer from './RichHistory/RichHistoryContainer';
import ExploreQueryInspector from './ExploreQueryInspector'; import ExploreQueryInspector from './ExploreQueryInspector';
import { splitOpen } from './state/main'; import { splitOpen } from './state/main';
import { changeSize, initializeExplore, refreshExplore } from './state/explorePane'; import { changeSize } from './state/explorePane';
import { updateTimeRange } from './state/time'; import { updateTimeRange } from './state/time';
import { scanStopAction, addQueryRow, modifyQueries, setQueries, scanStart } from './state/query'; import { scanStopAction, addQueryRow, modifyQueries, setQueries, scanStart } from './state/query';
import { ExploreId, ExploreItemState, ExploreUpdateState } from 'app/types/explore'; import { ExploreId, ExploreItemState } from 'app/types/explore';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
import { import { getFirstNonQueryRowSpecificError } from 'app/core/utils/explore';
DEFAULT_RANGE,
ensureQueries,
getFirstNonQueryRowSpecificError,
getTimeRange,
getTimeRangeFromUrl,
lastUsedDatasourceKeyForOrgId,
} from 'app/core/utils/explore';
import { ExploreToolbar } from './ExploreToolbar'; import { ExploreToolbar } from './ExploreToolbar';
import { NoDataSourceCallToAction } from './NoDataSourceCallToAction'; import { NoDataSourceCallToAction } from './NoDataSourceCallToAction';
import { getTimeZone } from '../profile/state/selectors'; import { getTimeZone } from '../profile/state/selectors';
...@@ -83,21 +71,13 @@ export interface ExploreProps { ...@@ -83,21 +71,13 @@ export interface ExploreProps {
datasourceInstance: DataSourceApi | null; datasourceInstance: DataSourceApi | null;
datasourceMissing: boolean; datasourceMissing: boolean;
exploreId: ExploreId; exploreId: ExploreId;
initializeExplore: typeof initializeExplore;
initialized: boolean;
modifyQueries: typeof modifyQueries; modifyQueries: typeof modifyQueries;
update: ExploreUpdateState;
refreshExplore: typeof refreshExplore;
scanning?: boolean; scanning?: boolean;
scanRange?: RawTimeRange; scanRange?: RawTimeRange;
scanStart: typeof scanStart; scanStart: typeof scanStart;
scanStopAction: typeof scanStopAction; scanStopAction: typeof scanStopAction;
setQueries: typeof setQueries; setQueries: typeof setQueries;
split: boolean;
queryKeys: string[]; queryKeys: string[];
initialDatasource: string;
initialQueries: DataQuery[];
initialRange: TimeRange;
isLive: boolean; isLive: boolean;
syncedTimes: boolean; syncedTimes: boolean;
updateTimeRange: typeof updateTimeRange; updateTimeRange: typeof updateTimeRange;
...@@ -153,47 +133,13 @@ interface ExploreState { ...@@ -153,47 +133,13 @@ interface ExploreState {
* `format`, to indicate eventual transformations by the datasources' result transformers. * `format`, to indicate eventual transformations by the datasources' result transformers.
*/ */
export class Explore extends React.PureComponent<ExploreProps, ExploreState> { export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
el: any;
exploreEvents: EventBusExtended;
constructor(props: ExploreProps) { constructor(props: ExploreProps) {
super(props); super(props);
this.exploreEvents = new EventBusSrv();
this.state = { this.state = {
openDrawer: undefined, openDrawer: undefined,
}; };
} }
componentDidMount() {
const { initialized, exploreId, initialDatasource, initialQueries, initialRange, originPanelId } = this.props;
const width = this.el ? this.el.offsetWidth : 0;
// initialize the whole explore first time we mount and if browser history contains a change in datasource
if (!initialized) {
this.props.initializeExplore(
exploreId,
initialDatasource,
initialQueries,
initialRange,
width,
this.exploreEvents,
originPanelId
);
}
}
componentWillUnmount() {
this.exploreEvents.removeAllListeners();
}
componentDidUpdate(prevProps: ExploreProps) {
this.refreshExplore();
}
getRef = (el: any) => {
this.el = el;
};
onChangeTime = (rawRange: RawTimeRange) => { onChangeTime = (rawRange: RawTimeRange) => {
const { updateTimeRange, exploreId } = this.props; const { updateTimeRange, exploreId } = this.props;
updateTimeRange({ exploreId, rawRange }); updateTimeRange({ exploreId, rawRange });
...@@ -271,14 +217,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> { ...@@ -271,14 +217,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
}); });
}; };
refreshExplore = () => {
const { exploreId, update } = this.props;
if (update.queries || update.range || update.datasource || update.mode) {
this.props.refreshExplore(exploreId);
}
};
renderEmptyState() { renderEmptyState() {
return ( return (
<div className="explore-container"> <div className="explore-container">
...@@ -367,7 +305,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> { ...@@ -367,7 +305,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
datasourceInstance, datasourceInstance,
datasourceMissing, datasourceMissing,
exploreId, exploreId,
split,
queryKeys, queryKeys,
graphResult, graphResult,
queryResponse, queryResponse,
...@@ -380,7 +317,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> { ...@@ -380,7 +317,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
showNodeGraph, showNodeGraph,
} = this.props; } = this.props;
const { openDrawer } = this.state; const { openDrawer } = this.state;
const exploreClass = split ? 'explore explore-split' : 'explore';
const styles = getStyles(theme); const styles = getStyles(theme);
const showPanels = queryResponse && queryResponse.state !== LoadingState.NotStarted; const showPanels = queryResponse && queryResponse.state !== LoadingState.NotStarted;
...@@ -393,13 +329,13 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> { ...@@ -393,13 +329,13 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
const showQueryInspector = openDrawer === ExploreDrawer.QueryInspector; const showQueryInspector = openDrawer === ExploreDrawer.QueryInspector;
return ( return (
<div className={exploreClass} ref={this.getRef} aria-label={selectors.pages.Explore.General.container}> <>
<ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} /> <ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} />
{datasourceMissing ? this.renderEmptyState() : null} {datasourceMissing ? this.renderEmptyState() : null}
{datasourceInstance && ( {datasourceInstance && (
<div className="explore-container"> <div className="explore-container">
<div className={cx('panel-container', styles.queryContainer)}> <div className={cx('panel-container', styles.queryContainer)}>
<QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} queryKeys={queryKeys} /> <QueryRows exploreId={exploreId} queryKeys={queryKeys} />
<SecondaryActions <SecondaryActions
addQueryRowButtonDisabled={isLive} addQueryRowButtonDisabled={isLive}
// We cannot show multiple traces at the same time right now so we do not show add query button. // We cannot show multiple traces at the same time right now so we do not show add query button.
...@@ -452,26 +388,20 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> { ...@@ -452,26 +388,20 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
</AutoSizer> </AutoSizer>
</div> </div>
)} )}
</div> </>
); );
} }
} }
const ensureQueriesMemoized = memoizeOne(ensureQueries);
const getTimeRangeFromUrlMemoized = memoizeOne(getTimeRangeFromUrl);
function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partial<ExploreProps> { function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partial<ExploreProps> {
const explore = state.explore; const explore = state.explore;
const { split, syncedTimes } = explore; const { syncedTimes } = explore;
const item: ExploreItemState = explore[exploreId]; const item: ExploreItemState = explore[exploreId]!;
const timeZone = getTimeZone(state.user); const timeZone = getTimeZone(state.user);
const { const {
datasourceInstance, datasourceInstance,
datasourceMissing, datasourceMissing,
initialized,
queryKeys, queryKeys,
urlState,
update,
isLive, isLive,
graphResult, graphResult,
logsResult, logsResult,
...@@ -485,29 +415,15 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia ...@@ -485,29 +415,15 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
loading, loading,
} = item; } = item;
const { datasource, queries, range: urlRange, originPanelId } = (urlState || {}) as ExploreUrlState;
const initialDatasource = datasource || store.get(lastUsedDatasourceKeyForOrgId(state.user.orgId));
const initialQueries: DataQuery[] = ensureQueriesMemoized(queries);
const initialRange = urlRange
? getTimeRangeFromUrlMemoized(urlRange, timeZone)
: getTimeRange(timeZone, DEFAULT_RANGE);
return { return {
datasourceInstance, datasourceInstance,
datasourceMissing, datasourceMissing,
initialized,
split,
queryKeys, queryKeys,
update,
initialDatasource,
initialQueries,
initialRange,
isLive, isLive,
graphResult, graphResult,
logsResult: logsResult ?? undefined, logsResult: logsResult ?? undefined,
absoluteRange, absoluteRange,
queryResponse, queryResponse,
originPanelId,
syncedTimes, syncedTimes,
timeZone, timeZone,
showLogs, showLogs,
...@@ -521,9 +437,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia ...@@ -521,9 +437,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
const mapDispatchToProps: Partial<ExploreProps> = { const mapDispatchToProps: Partial<ExploreProps> = {
changeSize, changeSize,
initializeExplore,
modifyQueries, modifyQueries,
refreshExplore,
scanStart, scanStart,
scanStopAction, scanStopAction,
setQueries, setQueries,
......
import React from 'react';
import { hot } from 'react-hot-loader';
import { compose } from 'redux';
import { connect, ConnectedProps } from 'react-redux';
import memoizeOne from 'memoize-one';
import { withTheme } from '@grafana/ui';
import { DataQuery, ExploreUrlState, EventBusExtended, EventBusSrv } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import store from 'app/core/store';
import { lastSavedUrl, cleanupPaneAction } from './state/main';
import { initializeExplore, refreshExplore } from './state/explorePane';
import { ExploreId } from 'app/types/explore';
import { StoreState } from 'app/types';
import {
DEFAULT_RANGE,
ensureQueries,
getTimeRange,
getTimeRangeFromUrl,
lastUsedDatasourceKeyForOrgId,
parseUrlState,
} from 'app/core/utils/explore';
import { getTimeZone } from '../profile/state/selectors';
import Explore from './Explore';
type PropsFromRedux = ConnectedProps<typeof connector>;
interface Props extends PropsFromRedux {
exploreId: ExploreId;
split: boolean;
}
/**
* This component is responsible for handling initialization of an Explore pane and triggering synchronization
* of state based on URL changes and preventing any infinite loops.
*/
export class ExplorePaneContainerUnconnected extends React.PureComponent<Props & ConnectedProps<typeof connector>> {
el: any;
exploreEvents: EventBusExtended;
constructor(props: Props) {
super(props);
this.exploreEvents = new EventBusSrv();
this.state = {
openDrawer: undefined,
};
}
componentDidMount() {
const { initialized, exploreId, initialDatasource, initialQueries, initialRange, originPanelId } = this.props;
const width = this.el?.offsetWidth ?? 0;
// initialize the whole explore first time we mount and if browser history contains a change in datasource
if (!initialized) {
this.props.initializeExplore(
exploreId,
initialDatasource,
initialQueries,
initialRange,
width,
this.exploreEvents,
originPanelId
);
}
}
componentWillUnmount() {
this.exploreEvents.removeAllListeners();
this.props.cleanupPaneAction({ exploreId: this.props.exploreId });
}
componentDidUpdate(prevProps: Props) {
this.refreshExplore(prevProps.urlQuery);
}
refreshExplore = (prevUrlQuery: string) => {
const { exploreId, urlQuery } = this.props;
// Update state from url only if it changed and only if the change wasn't initialised by redux to prevent any loops
if (urlQuery !== prevUrlQuery && urlQuery !== lastSavedUrl[exploreId]) {
this.props.refreshExplore(exploreId, urlQuery);
}
};
getRef = (el: any) => {
this.el = el;
};
render() {
const exploreClass = this.props.split ? 'explore explore-split' : 'explore';
return (
<div className={exploreClass} ref={this.getRef} aria-label={selectors.pages.Explore.General.container}>
{this.props.initialized && <Explore exploreId={this.props.exploreId} />}
</div>
);
}
}
const ensureQueriesMemoized = memoizeOne(ensureQueries);
const getTimeRangeFromUrlMemoized = memoizeOne(getTimeRangeFromUrl);
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
const urlQuery = state.location.query[exploreId] as string;
const urlState = parseUrlState(urlQuery);
const timeZone = getTimeZone(state.user);
const { datasource, queries, range: urlRange, originPanelId } = (urlState || {}) as ExploreUrlState;
const initialDatasource = datasource || store.get(lastUsedDatasourceKeyForOrgId(state.user.orgId));
const initialQueries: DataQuery[] = ensureQueriesMemoized(queries);
const initialRange = urlRange
? getTimeRangeFromUrlMemoized(urlRange, timeZone)
: getTimeRange(timeZone, DEFAULT_RANGE);
return {
initialized: state.explore[exploreId]?.initialized,
initialDatasource,
initialQueries,
initialRange,
originPanelId,
urlQuery,
};
}
const mapDispatchToProps = {
initializeExplore,
refreshExplore,
cleanupPaneAction,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export const ExplorePaneContainer = compose(hot(module), connector, withTheme)(ExplorePaneContainerUnconnected);
...@@ -172,7 +172,7 @@ export function ExploreQueryInspector(props: Props) { ...@@ -172,7 +172,7 @@ export function ExploreQueryInspector(props: Props) {
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) { function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
const explore = state.explore; const explore = state.explore;
const item: ExploreItemState = explore[exploreId]; const item: ExploreItemState = explore[exploreId]!;
const { loading, queryResponse } = item; const { loading, queryResponse } = item;
return { return {
......
...@@ -21,6 +21,7 @@ import { RunButton } from './RunButton'; ...@@ -21,6 +21,7 @@ import { RunButton } from './RunButton';
import { LiveTailControls } from './useLiveTailControls'; import { LiveTailControls } from './useLiveTailControls';
import { cancelQueries, clearQueries, runQueries } from './state/query'; import { cancelQueries, clearQueries, runQueries } from './state/query';
import ReturnToDashboardButton from './ReturnToDashboardButton'; import ReturnToDashboardButton from './ReturnToDashboardButton';
import { isSplit } from './state/selectors';
interface OwnProps { interface OwnProps {
exploreId: ExploreId; exploreId: ExploreId;
...@@ -127,7 +128,12 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> { ...@@ -127,7 +128,12 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
)} )}
</div> </div>
{splitted && ( {splitted && (
<IconButton className="explore-toolbar-header-close" onClick={() => closeSplit(exploreId)} name="times" /> <IconButton
title="Close split pane"
className="explore-toolbar-header-close"
onClick={() => closeSplit(exploreId)}
name="times"
/>
)} )}
</div> </div>
</div> </div>
...@@ -227,9 +233,8 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> { ...@@ -227,9 +233,8 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
} }
const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps => { const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps => {
const splitted = state.explore.split;
const syncedTimes = state.explore.syncedTimes; const syncedTimes = state.explore.syncedTimes;
const exploreItem: ExploreItemState = state.explore[exploreId]; const exploreItem: ExploreItemState = state.explore[exploreId]!;
const { const {
datasourceInstance, datasourceInstance,
datasourceMissing, datasourceMissing,
...@@ -249,7 +254,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps ...@@ -249,7 +254,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
loading, loading,
range, range,
timeZone: getTimeZone(state.user), timeZone: getTimeZone(state.user),
splitted, splitted: isSplit(state),
refreshInterval, refreshInterval,
hasLiveOption, hasLiveOption,
isLive, isLive,
......
...@@ -36,7 +36,7 @@ export function UnconnectedNodeGraphContainer(props: Props & ConnectedProps<type ...@@ -36,7 +36,7 @@ export function UnconnectedNodeGraphContainer(props: Props & ConnectedProps<type
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) { function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
return { return {
range: state.explore[exploreId].range, range: state.explore[exploreId]!.range,
}; };
} }
......
...@@ -31,7 +31,6 @@ import { HelpToggle } from '../query/components/HelpToggle'; ...@@ -31,7 +31,6 @@ import { HelpToggle } from '../query/components/HelpToggle';
interface PropsFromParent { interface PropsFromParent {
exploreId: ExploreId; exploreId: ExploreId;
index: number; index: number;
exploreEvents: EventBusExtended;
} }
export interface QueryRowProps extends PropsFromParent { export interface QueryRowProps extends PropsFromParent {
...@@ -49,6 +48,7 @@ export interface QueryRowProps extends PropsFromParent { ...@@ -49,6 +48,7 @@ export interface QueryRowProps extends PropsFromParent {
runQueries: typeof runQueries; runQueries: typeof runQueries;
queryResponse: PanelData; queryResponse: PanelData;
latency: number; latency: number;
exploreEvents: EventBusExtended;
} }
interface QueryRowState { interface QueryRowState {
...@@ -201,8 +201,8 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> { ...@@ -201,8 +201,8 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps) { function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps) {
const explore = state.explore; const explore = state.explore;
const item: ExploreItemState = explore[exploreId]; const item: ExploreItemState = explore[exploreId]!;
const { datasourceInstance, history, queries, range, absoluteRange, queryResponse, latency } = item; const { datasourceInstance, history, queries, range, absoluteRange, queryResponse, latency, eventBridge } = item;
const query = queries[index]; const query = queries[index];
return { return {
...@@ -213,6 +213,7 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps) ...@@ -213,6 +213,7 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps)
absoluteRange, absoluteRange,
queryResponse, queryResponse,
latency, latency,
exploreEvents: eventBridge,
}; };
} }
......
...@@ -5,23 +5,21 @@ import React, { PureComponent } from 'react'; ...@@ -5,23 +5,21 @@ import React, { PureComponent } from 'react';
import QueryRow from './QueryRow'; import QueryRow from './QueryRow';
// Types // Types
import { EventBusExtended } from '@grafana/data';
import { ExploreId } from 'app/types/explore'; import { ExploreId } from 'app/types/explore';
interface QueryRowsProps { interface QueryRowsProps {
className?: string; className?: string;
exploreEvents: EventBusExtended;
exploreId: ExploreId; exploreId: ExploreId;
queryKeys: string[]; queryKeys: string[];
} }
export default class QueryRows extends PureComponent<QueryRowsProps> { export default class QueryRows extends PureComponent<QueryRowsProps> {
render() { render() {
const { className = '', exploreEvents, exploreId, queryKeys } = this.props; const { className = '', exploreId, queryKeys } = this.props;
return ( return (
<div className={className}> <div className={className}>
{queryKeys.map((key, index) => { {queryKeys.map((key, index) => {
return <QueryRow key={key} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />; return <QueryRow key={key} exploreId={exploreId} index={index} />;
})} })}
</div> </div>
); );
......
...@@ -10,6 +10,7 @@ import { StoreState } from 'app/types'; ...@@ -10,6 +10,7 @@ import { StoreState } from 'app/types';
import { ExploreId } from 'app/types/explore'; import { ExploreId } from 'app/types/explore';
import { updateLocation } from 'app/core/actions'; import { updateLocation } from 'app/core/actions';
import { setDashboardQueriesToUpdateOnLoad } from '../dashboard/state/reducers'; import { setDashboardQueriesToUpdateOnLoad } from '../dashboard/state/reducers';
import { isSplit } from './state/selectors';
interface Props { interface Props {
exploreId: ExploreId; exploreId: ExploreId;
...@@ -83,8 +84,8 @@ export const UnconnectedReturnToDashboardButton: FC<Props> = ({ ...@@ -83,8 +84,8 @@ export const UnconnectedReturnToDashboardButton: FC<Props> = ({
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) { function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
const explore = state.explore; const explore = state.explore;
const splitted = state.explore.split; const splitted = isSplit(state);
const { datasourceInstance, queries, originPanelId } = explore[exploreId]; const { datasourceInstance, queries, originPanelId } = explore[exploreId]!;
return { return {
exploreId, exploreId,
......
...@@ -5,7 +5,7 @@ import { css, cx } from 'emotion'; ...@@ -5,7 +5,7 @@ import { css, cx } from 'emotion';
import { stylesFactory, useTheme, TextArea, Button, IconButton } from '@grafana/ui'; import { stylesFactory, useTheme, TextArea, Button, IconButton } from '@grafana/ui';
import { getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import { GrafanaTheme, AppEvents, DataSourceApi } from '@grafana/data'; import { GrafanaTheme, AppEvents, DataSourceApi } from '@grafana/data';
import { RichHistoryQuery, ExploreId, ExploreItemState } from 'app/types/explore'; import { RichHistoryQuery, ExploreId } from 'app/types/explore';
import { createUrlFromRichHistory, createQueryText } from 'app/core/utils/richHistory'; import { createUrlFromRichHistory, createQueryText } from 'app/core/utils/richHistory';
import { createAndCopyShortLink } from 'app/core/utils/shortLinks'; import { createAndCopyShortLink } from 'app/core/utils/shortLinks';
import { copyStringToClipboard } from 'app/core/utils/explore'; import { copyStringToClipboard } from 'app/core/utils/explore';
...@@ -313,9 +313,7 @@ export function RichHistoryCard(props: Props) { ...@@ -313,9 +313,7 @@ export function RichHistoryCard(props: Props) {
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) { function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
const explore = state.explore; const explore = state.explore;
const { datasourceInstance } = explore[exploreId]; const { datasourceInstance } = explore[exploreId]!;
// @ts-ignore
const item: ExploreItemState = explore[exploreId];
return { return {
exploreId, exploreId,
datasourceInstance, datasourceInstance,
......
...@@ -3,7 +3,6 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; ...@@ -3,7 +3,6 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import Wrapper from './Wrapper'; import Wrapper from './Wrapper';
import { configureStore } from '../../store/configureStore'; import { configureStore } from '../../store/configureStore';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { store } from '../../store/store';
import { setDataSourceSrv } from '@grafana/runtime'; import { setDataSourceSrv } from '@grafana/runtime';
import { import {
ArrayDataFrame, ArrayDataFrame,
...@@ -22,6 +21,9 @@ import { updateLocation } from '../../core/reducers/location'; ...@@ -22,6 +21,9 @@ import { updateLocation } from '../../core/reducers/location';
import { LokiDatasource } from '../../plugins/datasource/loki/datasource'; import { LokiDatasource } from '../../plugins/datasource/loki/datasource';
import { LokiQuery } from '../../plugins/datasource/loki/types'; import { LokiQuery } from '../../plugins/datasource/loki/types';
import { fromPairs } from 'lodash'; import { fromPairs } from 'lodash';
import { EnhancedStore } from '@reduxjs/toolkit';
import userEvent from '@testing-library/user-event';
import { splitOpen } from './state/main';
type Mock = jest.Mock; type Mock = jest.Mock;
...@@ -42,7 +44,7 @@ describe('Wrapper', () => { ...@@ -42,7 +44,7 @@ describe('Wrapper', () => {
}); });
it('inits url and renders editor but does not call query on empty url', async () => { it('inits url and renders editor but does not call query on empty url', async () => {
const { datasources } = setup(); const { datasources, store } = setup();
// Wait for rendering the editor // Wait for rendering the editor
await screen.findByText(/Editor/i); await screen.findByText(/Editor/i);
...@@ -57,7 +59,7 @@ describe('Wrapper', () => { ...@@ -57,7 +59,7 @@ describe('Wrapper', () => {
it('runs query when url contains query and renders results', async () => { it('runs query when url contains query and renders results', async () => {
const query = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]) }; const query = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]) };
const { datasources } = setup({ query }); const { datasources, store } = setup({ query });
(datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse()); (datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse());
// Make sure we render the logs panel // Make sure we render the logs panel
...@@ -90,7 +92,7 @@ describe('Wrapper', () => { ...@@ -90,7 +92,7 @@ describe('Wrapper', () => {
it('handles url change and runs the new query', async () => { it('handles url change and runs the new query', async () => {
const query = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]) }; const query = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]) };
const { datasources } = setup({ query }); const { datasources, store } = setup({ query });
(datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse()); (datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse());
// Wait for rendering the logs // Wait for rendering the logs
await screen.findByText(/custom log line/i); await screen.findByText(/custom log line/i);
...@@ -111,7 +113,7 @@ describe('Wrapper', () => { ...@@ -111,7 +113,7 @@ describe('Wrapper', () => {
it('handles url change and runs the new query with different datasource', async () => { it('handles url change and runs the new query with different datasource', async () => {
const query = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]) }; const query = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]) };
const { datasources } = setup({ query }); const { datasources, store } = setup({ query });
(datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse()); (datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse());
// Wait for rendering the logs // Wait for rendering the logs
await screen.findByText(/custom log line/i); await screen.findByText(/custom log line/i);
...@@ -133,7 +135,7 @@ describe('Wrapper', () => { ...@@ -133,7 +135,7 @@ describe('Wrapper', () => {
it('handles changing the datasource manually', async () => { it('handles changing the datasource manually', async () => {
const query = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]) }; const query = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]) };
const { datasources } = setup({ query }); const { datasources, store } = setup({ query });
(datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse()); (datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse());
// Wait for rendering the editor // Wait for rendering the editor
await screen.findByText(/Editor/i); await screen.findByText(/Editor/i);
...@@ -147,15 +149,15 @@ describe('Wrapper', () => { ...@@ -147,15 +149,15 @@ describe('Wrapper', () => {
}); });
}); });
it('opens the split pane', async () => { it('opens the split pane when split button is clicked', async () => {
const { datasources } = setup(); setup();
// Wait for rendering the editor // Wait for rendering the editor
const splitButton = await screen.findByText(/split/i); const splitButton = await screen.findByText(/split/i);
fireEvent.click(splitButton); fireEvent.click(splitButton);
const editors = await screen.findAllByText('loki Editor input:'); await waitFor(() => {
const editors = screen.getAllByText('loki Editor input:');
expect(editors.length).toBe(2); expect(editors.length).toBe(2);
expect(datasources.loki.query).not.toBeCalled(); });
}); });
it('inits with two panes if specified in url', async () => { it('inits with two panes if specified in url', async () => {
...@@ -164,7 +166,7 @@ describe('Wrapper', () => { ...@@ -164,7 +166,7 @@ describe('Wrapper', () => {
right: JSON.stringify(['now-1h', 'now', 'elastic', { expr: 'error' }]), right: JSON.stringify(['now-1h', 'now', 'elastic', { expr: 'error' }]),
}; };
const { datasources } = setup({ query }); const { datasources, store } = setup({ query });
(datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse()); (datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse());
(datasources.elastic.query as Mock).mockReturnValueOnce(makeLogsQueryResponse()); (datasources.elastic.query as Mock).mockReturnValueOnce(makeLogsQueryResponse());
...@@ -199,6 +201,65 @@ describe('Wrapper', () => { ...@@ -199,6 +201,65 @@ describe('Wrapper', () => {
targets: [{ expr: 'error' }], targets: [{ expr: 'error' }],
}); });
}); });
it('can close a pane from a split', async () => {
const query = {
left: JSON.stringify(['now-1h', 'now', 'loki', {}]),
right: JSON.stringify(['now-1h', 'now', 'elastic', {}]),
};
setup({ query });
const closeButtons = await screen.findAllByTitle(/Close split pane/i);
userEvent.click(closeButtons[1]);
await waitFor(() => {
const logsPanels = screen.queryAllByTitle(/Close split pane/i);
expect(logsPanels.length).toBe(0);
});
});
it('handles url change to split view', async () => {
const query = {
left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]),
};
const { datasources, store } = setup({ query });
(datasources.loki.query as Mock).mockReturnValue(makeLogsQueryResponse());
(datasources.elastic.query as Mock).mockReturnValue(makeLogsQueryResponse());
store.dispatch(
updateLocation({
path: '/explore',
query: {
left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]),
right: JSON.stringify(['now-1h', 'now', 'elastic', { expr: 'error' }]),
},
})
);
// Editor renders the new query
await screen.findByText(`loki Editor input: { label="value"}`);
await screen.findByText(`elastic Editor input: error`);
});
it('handles opening split with split open func', async () => {
const query = {
left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]),
};
const { datasources, store } = setup({ query });
(datasources.loki.query as Mock).mockReturnValue(makeLogsQueryResponse());
(datasources.elastic.query as Mock).mockReturnValue(makeLogsQueryResponse());
// This is mainly to wait for render so that the left pane state is initialized as that is needed for splitOpen
// to work
await screen.findByText(`loki Editor input: { label="value"}`);
store.dispatch(
splitOpen<any>({ datasourceUid: 'elastic', query: { expr: 'error' } }) as any
);
// Editor renders the new query
await screen.findByText(`elastic Editor input: error`);
await screen.findByText(`loki Editor input: { label="value"}`);
});
}); });
type DatasourceSetup = { settings: DataSourceInstanceSettings; api: DataSourceApi }; type DatasourceSetup = { settings: DataSourceInstanceSettings; api: DataSourceApi };
...@@ -206,7 +267,7 @@ type SetupOptions = { ...@@ -206,7 +267,7 @@ type SetupOptions = {
datasources?: DatasourceSetup[]; datasources?: DatasourceSetup[];
query?: any; query?: any;
}; };
function setup(options?: SetupOptions): { datasources: { [name: string]: DataSourceApi } } { function setup(options?: SetupOptions): { datasources: { [name: string]: DataSourceApi }; store: EnhancedStore } {
// Clear this up otherwise it persists data source selection // Clear this up otherwise it persists data source selection
// TODO: probably add test for that too // TODO: probably add test for that too
window.localStorage.clear(); window.localStorage.clear();
...@@ -238,15 +299,18 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou ...@@ -238,15 +299,18 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou
}, },
} as any); } as any);
configureStore(); const store = configureStore();
store.getState().user = { store.getState().user = {
orgId: 1, orgId: 1,
timeZone: 'utc', timeZone: 'utc',
}; };
store.getState().location.path = '/explore';
if (options?.query) { if (options?.query) {
// We have to dispatch cause right now we take the url state from the action not from the store store.getState().location = {
store.dispatch(updateLocation({ query: options.query, path: '/explore' })); ...store.getState().location,
query: options.query,
};
} }
render( render(
...@@ -254,7 +318,7 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou ...@@ -254,7 +318,7 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou
<Wrapper /> <Wrapper />
</Provider> </Provider>
); );
return { datasources: fromPairs(dsSettings.map((d) => [d.api.name, d.api])) }; return { datasources: fromPairs(dsSettings.map((d) => [d.api.name, d.api])), store };
} }
function makeDatasourceSetup({ name = 'loki', id = 1 }: { name?: string; id?: number } = {}): DatasourceSetup { function makeDatasourceSetup({ name = 'loki', id = 1 }: { name?: string; id?: number } = {}): DatasourceSetup {
......
...@@ -6,9 +6,9 @@ import { StoreState } from 'app/types'; ...@@ -6,9 +6,9 @@ import { StoreState } from 'app/types';
import { ExploreId } from 'app/types/explore'; import { ExploreId } from 'app/types/explore';
import { CustomScrollbar, ErrorBoundaryAlert } from '@grafana/ui'; import { CustomScrollbar, ErrorBoundaryAlert } from '@grafana/ui';
import { resetExploreAction, richHistoryUpdatedAction } from './state/main'; import { lastSavedUrl, resetExploreAction, richHistoryUpdatedAction } from './state/main';
import Explore from './Explore';
import { getRichHistory } from '../../core/utils/richHistory'; import { getRichHistory } from '../../core/utils/richHistory';
import { ExplorePaneContainer } from './ExplorePaneContainer';
interface WrapperProps { interface WrapperProps {
split: boolean; split: boolean;
...@@ -22,6 +22,9 @@ export class Wrapper extends Component<WrapperProps> { ...@@ -22,6 +22,9 @@ export class Wrapper extends Component<WrapperProps> {
} }
componentDidMount() { componentDidMount() {
lastSavedUrl.left = undefined;
lastSavedUrl.right = undefined;
const richHistory = getRichHistory(); const richHistory = getRichHistory();
this.props.richHistoryUpdatedAction({ richHistory }); this.props.richHistoryUpdatedAction({ richHistory });
} }
...@@ -34,11 +37,11 @@ export class Wrapper extends Component<WrapperProps> { ...@@ -34,11 +37,11 @@ export class Wrapper extends Component<WrapperProps> {
<CustomScrollbar autoHeightMin={'100%'}> <CustomScrollbar autoHeightMin={'100%'}>
<div className="explore-wrapper"> <div className="explore-wrapper">
<ErrorBoundaryAlert style="page"> <ErrorBoundaryAlert style="page">
<Explore exploreId={ExploreId.left} /> <ExplorePaneContainer split={split} exploreId={ExploreId.left} />
</ErrorBoundaryAlert> </ErrorBoundaryAlert>
{split && ( {split && (
<ErrorBoundaryAlert style="page"> <ErrorBoundaryAlert style="page">
<Explore exploreId={ExploreId.right} /> <ExplorePaneContainer split={split} exploreId={ExploreId.right} />
</ErrorBoundaryAlert> </ErrorBoundaryAlert>
)} )}
</div> </div>
...@@ -49,8 +52,11 @@ export class Wrapper extends Component<WrapperProps> { ...@@ -49,8 +52,11 @@ export class Wrapper extends Component<WrapperProps> {
} }
const mapStateToProps = (state: StoreState) => { const mapStateToProps = (state: StoreState) => {
const { split } = state.explore; // Here we use URL to say if we should split or not which is different than in other places. Reason is if we change
return { split }; // the URL first there is no internal state saying we should split. So this triggers render of ExplorePaneContainer
// and initialisation of each pane state.
const isUrlSplit = Boolean(state.location.query[ExploreId.left] && state.location.query[ExploreId.right]);
return { split: isUrlSplit };
}; };
const mapDispatchToProps = { const mapDispatchToProps = {
......
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Explore should render component 1`] = ` exports[`Explore should render component 1`] = `
<div <Fragment>
aria-label="Explore"
className="explore"
>
<Connect(UnConnectedExploreToolbar) <Connect(UnConnectedExploreToolbar)
exploreId="left" exploreId="left"
onChangeTime={[Function]} onChangeTime={[Function]}
...@@ -16,14 +13,6 @@ exports[`Explore should render component 1`] = ` ...@@ -16,14 +13,6 @@ exports[`Explore should render component 1`] = `
className="panel-container css-kj45dn-queryContainer" className="panel-container css-kj45dn-queryContainer"
> >
<QueryRows <QueryRows
exploreEvents={
EventBusSrv {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
}
}
exploreId="left" exploreId="left"
queryKeys={Array []} queryKeys={Array []}
/> />
...@@ -47,5 +36,5 @@ exports[`Explore should render component 1`] = ` ...@@ -47,5 +36,5 @@ exports[`Explore should render component 1`] = `
<Component /> <Component />
</AutoSizer> </AutoSizer>
</div> </div>
</div> </Fragment>
`; `;
...@@ -8,7 +8,7 @@ import { ExploreItemState, ThunkResult } from 'app/types'; ...@@ -8,7 +8,7 @@ import { ExploreItemState, ThunkResult } from 'app/types';
import { ExploreId } from 'app/types/explore'; import { ExploreId } from 'app/types/explore';
import { importQueries, runQueries } from './query'; import { importQueries, runQueries } from './query';
import { changeRefreshInterval } from './time'; import { changeRefreshInterval } from './time';
import { createEmptyQueryResponse, loadAndInitDatasource, makeInitialUpdateState } from './utils'; import { createEmptyQueryResponse, loadAndInitDatasource } from './utils';
// //
// Actions and Payloads // Actions and Payloads
...@@ -41,7 +41,7 @@ export function changeDatasource( ...@@ -41,7 +41,7 @@ export function changeDatasource(
return async (dispatch, getState) => { return async (dispatch, getState) => {
const orgId = getState().user.orgId; const orgId = getState().user.orgId;
const { history, instance } = await loadAndInitDatasource(orgId, datasourceName); const { history, instance } = await loadAndInitDatasource(orgId, datasourceName);
const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance; const currentDataSourceInstance = getState().explore[exploreId]!.datasourceInstance;
dispatch( dispatch(
updateDatasourceInstanceAction({ updateDatasourceInstanceAction({
...@@ -51,13 +51,13 @@ export function changeDatasource( ...@@ -51,13 +51,13 @@ export function changeDatasource(
}) })
); );
const queries = getState().explore[exploreId].queries; const queries = getState().explore[exploreId]!.queries;
if (options?.importQueries) { if (options?.importQueries) {
await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, instance)); await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, instance));
} }
if (getState().explore[exploreId].isLive) { if (getState().explore[exploreId]!.isLive) {
dispatch(changeRefreshInterval(exploreId, RefreshPicker.offOption.value)); dispatch(changeRefreshInterval(exploreId, RefreshPicker.offOption.value));
} }
...@@ -97,11 +97,9 @@ export const datasourceReducer = (state: ExploreItemState, action: AnyAction): E ...@@ -97,11 +97,9 @@ export const datasourceReducer = (state: ExploreItemState, action: AnyAction): E
queryResponse: createEmptyQueryResponse(), queryResponse: createEmptyQueryResponse(),
loading: false, loading: false,
queryKeys: [], queryKeys: [],
originPanelId: state.urlState && state.urlState.originPanelId,
history, history,
datasourceMissing: false, datasourceMissing: false,
logsHighlighterExpressions: undefined, logsHighlighterExpressions: undefined,
update: makeInitialUpdateState(),
}; };
} }
......
import { PayloadAction } from '@reduxjs/toolkit'; import { DataQuery, DefaultTimeZone, EventBusExtended, serializeStateToUrlParam, toUtc } from '@grafana/data';
import { DataQuery, DefaultTimeZone, EventBusExtended, ExploreUrlState, LogsDedupStrategy, toUtc } from '@grafana/data'; import { ExploreId } from 'app/types';
import { ExploreId, ExploreItemState, ExploreUpdateState } from 'app/types'; import { refreshExplore } from './explorePane';
import { thunkTester } from 'test/core/thunk/thunkTester';
import {
changeDedupStrategyAction,
initializeExploreAction,
InitializeExplorePayload,
paneReducer,
refreshExplore,
} from './explorePane';
import { setQueriesAction } from './query';
import { makeExplorePaneState, makeInitialUpdateState } from './utils';
import { reducerTester } from '../../../../test/core/redux/reducerTester';
import { setDataSourceSrv } from '@grafana/runtime'; import { setDataSourceSrv } from '@grafana/runtime';
import { configureStore } from '../../../store/configureStore';
import { of } from 'rxjs';
jest.mock('../../dashboard/services/TimeSrv', () => ({ jest.mock('../../dashboard/services/TimeSrv', () => ({
getTimeSrv: jest.fn().mockReturnValue({ getTimeSrv: jest.fn().mockReturnValue({
...@@ -30,134 +21,125 @@ const testRange = { ...@@ -30,134 +21,125 @@ const testRange = {
}, },
}; };
setDataSourceSrv({ const defaultInitialState = {
getList() { user: {
return []; orgId: '1',
timeZone: DefaultTimeZone,
}, },
getInstanceSettings(name: string) { explore: {
return { name: 'hello' }; [ExploreId.left]: {
}, initialized: true,
get() { containerWidth: 1920,
return Promise.resolve({ eventBridge: {} as EventBusExtended,
testDatasource: jest.fn(), queries: [] as DataQuery[],
init: jest.fn(), range: testRange,
}); refreshInterval: {
label: 'Off',
value: 0,
},
},
}, },
} as any); };
const setup = (updateOverides?: Partial<ExploreUpdateState>) => { function setupStore(state?: any) {
const exploreId = ExploreId.left; return configureStore({
const containerWidth = 1920; ...defaultInitialState,
const eventBridge = {} as EventBusExtended;
const timeZone = DefaultTimeZone;
const range = testRange;
const urlState: ExploreUrlState = {
datasource: 'some-datasource',
queries: [],
range: range.raw,
};
const updateDefaults = makeInitialUpdateState();
const update = { ...updateDefaults, ...updateOverides };
const initialState = {
user: {
orgId: '1',
timeZone,
},
explore: { explore: {
[exploreId]: { [ExploreId.left]: {
initialized: true, ...defaultInitialState.explore[ExploreId.left],
urlState, ...(state || {}),
containerWidth,
eventBridge,
update,
datasourceInstance: { name: 'some-datasource' },
queries: [] as DataQuery[],
range,
refreshInterval: {
label: 'Off',
value: 0,
},
}, },
}, },
}; } as any);
}
return { function setup(state?: any) {
initialState, const datasources: Record<string, any> = {
exploreId, newDs: {
range, testDatasource: jest.fn(),
containerWidth, init: jest.fn(),
eventBridge, query: jest.fn(),
name: 'newDs',
meta: { id: 'newDs' },
},
someDs: {
testDatasource: jest.fn(),
init: jest.fn(),
query: jest.fn(),
name: 'someDs',
meta: { id: 'someDs' },
},
}; };
};
describe('refreshExplore', () => { setDataSourceSrv({
describe('when explore is initialized', () => { getList() {
describe('and update datasource is set', () => { return Object.values(datasources).map((d) => ({ name: d.name }));
it('then it should dispatch initializeExplore', async () => { },
const { exploreId, initialState, containerWidth, eventBridge } = setup({ datasource: true }); getInstanceSettings(name: string) {
return { name: 'hello' };
const dispatchedActions = await thunkTester(initialState) },
.givenThunk(refreshExplore) get(name?: string) {
.whenThunkIsDispatched(exploreId); return Promise.resolve(
name
const initializeExplore = dispatchedActions.find((action) => action.type === initializeExploreAction.type); ? datasources[name]
const { type, payload } = initializeExplore as PayloadAction<InitializeExplorePayload>; : {
testDatasource: jest.fn(),
expect(type).toEqual(initializeExploreAction.type); init: jest.fn(),
expect(payload.containerWidth).toEqual(containerWidth); name: 'default',
expect(payload.eventBridge).toEqual(eventBridge); }
expect(payload.queries.length).toBe(1); // Queries have generated keys hard to expect on );
expect(payload.range.from).toEqual(testRange.from); },
expect(payload.range.to).toEqual(testRange.to); } as any);
expect(payload.range.raw.from).toEqual(testRange.raw.from);
expect(payload.range.raw.to).toEqual(testRange.raw.to);
});
});
describe('and update queries is set', () => {
it('then it should dispatch setQueriesAction', async () => {
const { exploreId, initialState } = setup({ queries: true });
const dispatchedActions = await thunkTester(initialState)
.givenThunk(refreshExplore)
.whenThunkIsDispatched(exploreId);
expect(dispatchedActions[0].type).toEqual(setQueriesAction.type); const store = setupStore({
expect(dispatchedActions[0].payload).toEqual({ exploreId, queries: [] }); datasourceInstance: datasources.someDs,
}); ...(state || {}),
});
}); });
describe('when update is not initialized', () => { return {
it('then it should not dispatch any actions', async () => { store,
const exploreId = ExploreId.left; datasources,
const initialState = { explore: { [exploreId]: { initialized: false } } }; };
}
const dispatchedActions = await thunkTester(initialState)
.givenThunk(refreshExplore)
.whenThunkIsDispatched(exploreId);
expect(dispatchedActions).toEqual([]); describe('refreshExplore', () => {
}); it('should change data source when datasource in url changes', async () => {
const { store } = setup();
await store.dispatch(
refreshExplore(ExploreId.left, serializeStateToUrlParam({ datasource: 'newDs', queries: [], range: testRange }))
);
expect(store.getState().explore[ExploreId.left].datasourceInstance?.name).toBe('newDs');
}); });
});
describe('Explore pane reducer', () => { it('should change and run new queries from the URL', async () => {
describe('changing dedup strategy', () => { const { store, datasources } = setup();
describe('when changeDedupStrategyAction is dispatched', () => { datasources.someDs.query.mockReturnValueOnce(of({}));
it('then it should set correct dedup strategy in state', () => { await store.dispatch(
const initialState = makeExplorePaneState(); refreshExplore(
ExploreId.left,
serializeStateToUrlParam({ datasource: 'someDs', queries: [{ expr: 'count()' }], range: testRange })
)
);
// same
const state = store.getState().explore[ExploreId.left];
expect(state.datasourceInstance?.name).toBe('someDs');
expect(state.queries.length).toBe(1);
expect(state.queries).toMatchObject([{ expr: 'count()' }]);
expect(datasources.someDs.query).toHaveBeenCalledTimes(1);
});
reducerTester<ExploreItemState>() it('should not do anything if pane is not initialized', async () => {
.givenReducer(paneReducer, initialState) const { store } = setup({
.whenActionIsDispatched( initialized: false,
changeDedupStrategyAction({ exploreId: ExploreId.left, dedupStrategy: LogsDedupStrategy.exact })
)
.thenStateShouldEqual({
...initialState,
dedupStrategy: LogsDedupStrategy.exact,
});
});
}); });
const state = store.getState();
await store.dispatch(
refreshExplore(
ExploreId.left,
serializeStateToUrlParam({ datasource: 'newDs', queries: [{ expr: 'count()' }], range: testRange })
)
);
expect(state).toEqual(store.getState());
}); });
}); });
import { AnyAction } from 'redux'; import { AnyAction } from 'redux';
import { isEqual } from 'lodash'; import isEqual from 'lodash/isEqual';
import {
DEFAULT_RANGE,
getQueryKeys,
parseUrlState,
ensureQueries,
generateNewKeyAndAddRefIdIfMissing,
getTimeRangeFromUrl,
} from 'app/core/utils/explore';
import { ExploreId, ExploreItemState } from 'app/types/explore'; import { ExploreId, ExploreItemState } from 'app/types/explore';
import { queryReducer, runQueries, setQueriesAction } from './query'; import { queryReducer, runQueries, setQueriesAction } from './query';
import { datasourceReducer } from './datasource'; import { datasourceReducer } from './datasource';
import { timeReducer, updateTime } from './time'; import { timeReducer, updateTime } from './time';
import { historyReducer } from './history'; import { historyReducer } from './history';
import { makeExplorePaneState, makeInitialUpdateState, loadAndInitDatasource, createEmptyQueryResponse } from './utils'; import {
makeExplorePaneState,
loadAndInitDatasource,
createEmptyQueryResponse,
getUrlStateFromPaneState,
} from './utils';
import { createAction, PayloadAction } from '@reduxjs/toolkit'; import { createAction, PayloadAction } from '@reduxjs/toolkit';
import { import {
EventBusExtended, EventBusExtended,
...@@ -17,20 +31,12 @@ import { ...@@ -17,20 +31,12 @@ import {
HistoryItem, HistoryItem,
DataSourceApi, DataSourceApi,
} from '@grafana/data'; } from '@grafana/data';
import {
clearQueryKeys,
ensureQueries,
generateNewKeyAndAddRefIdIfMissing,
getTimeRangeFromUrl,
getQueryKeys,
} from 'app/core/utils/explore';
// Types // Types
import { ThunkResult } from 'app/types'; import { ThunkResult } from 'app/types';
import { getTimeZone } from 'app/features/profile/state/selectors'; import { getTimeZone } from 'app/features/profile/state/selectors';
import { updateLocation } from '../../../core/actions';
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
import { toRawTimeRange } from '../utils/time';
import { getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import { getRichHistory } from '../../../core/utils/richHistory';
import { richHistoryUpdatedAction } from './main';
// //
// Actions and Payloads // Actions and Payloads
...@@ -155,52 +161,35 @@ export function initializeExplore( ...@@ -155,52 +161,35 @@ export function initializeExplore(
dispatch(updateTime({ exploreId })); dispatch(updateTime({ exploreId }));
if (instance) { if (instance) {
dispatch(runQueries(exploreId)); // We do not want to add the url to browser history on init because when the pane is initialised it's because
} // we already have something in the url. Adding basically the same state as additional history item prevents
}; // user to go back to previous url.
} dispatch(runQueries(exploreId, { replaceUrl: true }));
/**
* Save local redux state back to the URL. Should be called when there is some change that should affect the URL.
* Not all of the redux state is reflected in URL though.
*/
export const stateSave = (): ThunkResult<void> => {
return (dispatch, getState) => {
const { left, right, split } = getState().explore;
const orgId = getState().user.orgId.toString();
const replace = left && left.urlReplaced === false;
const urlStates: { [index: string]: string } = { orgId };
urlStates.left = serializeStateToUrlParam(getUrlStateFromPaneState(left), true);
if (split) {
urlStates.right = serializeStateToUrlParam(getUrlStateFromPaneState(right), true);
} }
dispatch(updateLocation({ query: urlStates, replace })); const richHistory = getRichHistory();
if (replace) { dispatch(richHistoryUpdatedAction({ richHistory }));
dispatch(setUrlReplacedAction({ exploreId: ExploreId.left }));
}
}; };
}; }
/** /**
* Reacts to changes in URL state that we need to sync back to our redux state. Checks the internal update variable * Reacts to changes in URL state that we need to sync back to our redux state. Computes diff of newUrlQuery vs current
* to see which parts change and need to be synced. * state and runs update actions for relevant parts.
* @param exploreId
*/ */
export function refreshExplore(exploreId: ExploreId): ThunkResult<void> { export function refreshExplore(exploreId: ExploreId, newUrlQuery: string): ThunkResult<void> {
return (dispatch, getState) => { return async (dispatch, getState) => {
const itemState = getState().explore[exploreId]; const itemState = getState().explore[exploreId]!;
if (!itemState.initialized) { if (!itemState.initialized) {
return; return;
} }
const { urlState, update, containerWidth, eventBridge } = itemState; // Get diff of what should be updated
const newUrlState = parseUrlState(newUrlQuery);
const update = urlDiff(newUrlState, getUrlStateFromPaneState(itemState));
if (!urlState) { const { containerWidth, eventBridge } = itemState;
return;
}
const { datasource, queries, range: urlRange, originPanelId } = urlState; const { datasource, queries, range: urlRange, originPanelId } = newUrlState;
const refreshQueries: DataQuery[] = []; const refreshQueries: DataQuery[] = [];
for (let index = 0; index < queries.length; index++) { for (let index = 0; index < queries.length; index++) {
...@@ -211,10 +200,11 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> { ...@@ -211,10 +200,11 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
const timeZone = getTimeZone(getState().user); const timeZone = getTimeZone(getState().user);
const range = getTimeRangeFromUrl(urlRange, timeZone); const range = getTimeRangeFromUrl(urlRange, timeZone);
// need to refresh datasource // commit changes based on the diff of new url vs old url
if (update.datasource) { if (update.datasource) {
const initialQueries = ensureQueries(queries); const initialQueries = ensureQueries(queries);
dispatch( await dispatch(
initializeExplore(exploreId, datasource, initialQueries, range, containerWidth, eventBridge, originPanelId) initializeExplore(exploreId, datasource, initialQueries, range, containerWidth, eventBridge, originPanelId)
); );
return; return;
...@@ -224,7 +214,6 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> { ...@@ -224,7 +214,6 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
dispatch(updateTime({ exploreId, rawRange: range.raw })); dispatch(updateTime({ exploreId, rawRange: range.raw }));
} }
// need to refresh queries
if (update.queries) { if (update.queries) {
dispatch(setQueriesAction({ exploreId, queries: refreshQueries })); dispatch(setQueriesAction({ exploreId, queries: refreshQueries }));
} }
...@@ -286,7 +275,6 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac ...@@ -286,7 +275,6 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
initialized: true, initialized: true,
queryKeys: getQueryKeys(queries, datasourceInstance), queryKeys: getQueryKeys(queries, datasourceInstance),
originPanelId, originPanelId,
update: makeInitialUpdateState(),
datasourceInstance, datasourceInstance,
history, history,
datasourceMissing: !datasourceInstance, datasourceMissing: !datasourceInstance,
...@@ -303,22 +291,28 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac ...@@ -303,22 +291,28 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
}; };
} }
if (setUrlReplacedAction.match(action)) {
return {
...state,
urlReplaced: true,
};
}
return state; return state;
}; };
function getUrlStateFromPaneState(pane: ExploreItemState): ExploreUrlState { /**
* Compare 2 explore urls and return a map of what changed. Used to update the local state with all the
* side effects needed.
*/
export const urlDiff = (
oldUrlState: ExploreUrlState | undefined,
currentUrlState: ExploreUrlState | undefined
): {
datasource: boolean;
queries: boolean;
range: boolean;
} => {
const datasource = !isEqual(currentUrlState?.datasource, oldUrlState?.datasource);
const queries = !isEqual(currentUrlState?.queries, oldUrlState?.queries);
const range = !isEqual(currentUrlState?.range || DEFAULT_RANGE, oldUrlState?.range || DEFAULT_RANGE);
return { return {
// It can happen that if we are in a split and initial load also runs queries we can be here before the second pane datasource,
// is initialized so datasourceInstance will be still undefined. queries,
datasource: pane.datasourceInstance?.name || pane.urlState!.datasource, range,
queries: pane.queries.map(clearQueryKeys),
range: toRawTimeRange(pane.range),
}; };
} };
...@@ -35,12 +35,11 @@ import { ...@@ -35,12 +35,11 @@ import {
decorateWithTableResult, decorateWithTableResult,
} from '../utils/decorators'; } from '../utils/decorators';
import { createErrorNotification } from '../../../core/copy/appNotification'; import { createErrorNotification } from '../../../core/copy/appNotification';
import { richHistoryUpdatedAction } from './main'; import { richHistoryUpdatedAction, stateSave } from './main';
import { stateSave } from './explorePane';
import { AnyAction, createAction, PayloadAction } from '@reduxjs/toolkit'; import { AnyAction, createAction, PayloadAction } from '@reduxjs/toolkit';
import { updateTime } from './time'; import { updateTime } from './time';
import { historyUpdatedAction } from './history'; import { historyUpdatedAction } from './history';
import { createEmptyQueryResponse, makeInitialUpdateState } from './utils'; import { createEmptyQueryResponse } from './utils';
// //
// Actions and Payloads // Actions and Payloads
...@@ -174,7 +173,7 @@ export const scanStopAction = createAction<ScanStopPayload>('explore/scanStop'); ...@@ -174,7 +173,7 @@ export const scanStopAction = createAction<ScanStopPayload>('explore/scanStop');
*/ */
export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> { export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
return (dispatch, getState) => { return (dispatch, getState) => {
const queries = getState().explore[exploreId].queries; const queries = getState().explore[exploreId]!.queries;
const query = generateEmptyQuery(queries, index); const query = generateEmptyQuery(queries, index);
dispatch(addQueryRowAction({ exploreId, index, query })); dispatch(addQueryRowAction({ exploreId, index, query }));
...@@ -194,7 +193,7 @@ export function changeQuery( ...@@ -194,7 +193,7 @@ export function changeQuery(
return (dispatch, getState) => { return (dispatch, getState) => {
// Null query means reset // Null query means reset
if (query === null) { if (query === null) {
const queries = getState().explore[exploreId].queries; const queries = getState().explore[exploreId]!.queries;
const { refId, key } = queries[index]; const { refId, key } = queries[index];
query = generateNewKeyAndAddRefIdIfMissing({ refId, key }, queries, index); query = generateNewKeyAndAddRefIdIfMissing({ refId, key }, queries, index);
} }
...@@ -292,12 +291,12 @@ export function modifyQueries( ...@@ -292,12 +291,12 @@ export function modifyQueries(
/** /**
* Main action to run queries and dispatches sub-actions based on which result viewers are active * Main action to run queries and dispatches sub-actions based on which result viewers are active
*/ */
export const runQueries = (exploreId: ExploreId): ThunkResult<void> => { export const runQueries = (exploreId: ExploreId, options?: { replaceUrl?: boolean }): ThunkResult<void> => {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(updateTime({ exploreId })); dispatch(updateTime({ exploreId }));
const richHistory = getState().explore.richHistory; const richHistory = getState().explore.richHistory;
const exploreItemState = getState().explore[exploreId]; const exploreItemState = getState().explore[exploreId]!;
const { const {
datasourceInstance, datasourceInstance,
queries, queries,
...@@ -314,7 +313,7 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => { ...@@ -314,7 +313,7 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
if (!hasNonEmptyQuery(queries)) { if (!hasNonEmptyQuery(queries)) {
dispatch(clearQueriesAction({ exploreId })); dispatch(clearQueriesAction({ exploreId }));
dispatch(stateSave()); // Remember to save to state and update location dispatch(stateSave({ replace: options?.replaceUrl })); // Remember to save to state and update location
return; return;
} }
...@@ -379,7 +378,7 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => { ...@@ -379,7 +378,7 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
dispatch(richHistoryUpdatedAction({ richHistory: nextRichHistory })); dispatch(richHistoryUpdatedAction({ richHistory: nextRichHistory }));
// We save queries to the URL here so that only successfully run queries change the URL. // We save queries to the URL here so that only successfully run queries change the URL.
dispatch(stateSave()); dispatch(stateSave({ replace: options?.replaceUrl }));
} }
firstResponse = false; firstResponse = false;
...@@ -387,9 +386,9 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => { ...@@ -387,9 +386,9 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
dispatch(queryStreamUpdatedAction({ exploreId, response: data })); dispatch(queryStreamUpdatedAction({ exploreId, response: data }));
// Keep scanning for results if this was the last scanning transaction // Keep scanning for results if this was the last scanning transaction
if (getState().explore[exploreId].scanning) { if (getState().explore[exploreId]!.scanning) {
if (data.state === LoadingState.Done && data.series.length === 0) { if (data.state === LoadingState.Done && data.series.length === 0) {
const range = getShiftedTimeRange(-1, getState().explore[exploreId].range); const range = getShiftedTimeRange(-1, getState().explore[exploreId]!.range);
dispatch(updateTime({ exploreId, absoluteRange: range })); dispatch(updateTime({ exploreId, absoluteRange: range }));
dispatch(runQueries(exploreId)); dispatch(runQueries(exploreId));
} else { } else {
...@@ -416,7 +415,7 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => { ...@@ -416,7 +415,7 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult<void> { export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult<void> {
return (dispatch, getState) => { return (dispatch, getState) => {
// Inject react keys into query objects // Inject react keys into query objects
const queries = getState().explore[exploreId].queries; const queries = getState().explore[exploreId]!.queries;
const nextQueries = rawQueries.map((query, index) => generateNewKeyAndAddRefIdIfMissing(query, queries, index)); const nextQueries = rawQueries.map((query, index) => generateNewKeyAndAddRefIdIfMissing(query, queries, index));
dispatch(setQueriesAction({ exploreId, queries: nextQueries })); dispatch(setQueriesAction({ exploreId, queries: nextQueries }));
dispatch(runQueries(exploreId)); dispatch(runQueries(exploreId));
...@@ -433,7 +432,7 @@ export function scanStart(exploreId: ExploreId): ThunkResult<void> { ...@@ -433,7 +432,7 @@ export function scanStart(exploreId: ExploreId): ThunkResult<void> {
// Register the scanner // Register the scanner
dispatch(scanStartAction({ exploreId })); dispatch(scanStartAction({ exploreId }));
// Scanning must trigger query run, and return the new range // Scanning must trigger query run, and return the new range
const range = getShiftedTimeRange(-1, getState().explore[exploreId].range); const range = getShiftedTimeRange(-1, getState().explore[exploreId]!.range);
// Set the new range to be displayed // Set the new range to be displayed
dispatch(updateTime({ exploreId, absoluteRange: range })); dispatch(updateTime({ exploreId, absoluteRange: range }));
dispatch(runQueries(exploreId)); dispatch(runQueries(exploreId));
...@@ -627,7 +626,6 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor ...@@ -627,7 +626,6 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor
...state, ...state,
scanning: false, scanning: false,
scanRange: undefined, scanRange: undefined,
update: makeInitialUpdateState(),
}; };
} }
...@@ -687,7 +685,6 @@ export const processQueryResponse = ( ...@@ -687,7 +685,6 @@ export const processQueryResponse = (
tableResult, tableResult,
logsResult, logsResult,
loading: loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming, loading: loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming,
update: makeInitialUpdateState(),
showLogs: !!logsResult, showLogs: !!logsResult,
showMetrics: !!graphResult, showMetrics: !!graphResult,
showTable: !!tableResult, showTable: !!tableResult,
......
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { ExploreItemState } from 'app/types'; import { ExploreId, ExploreItemState, StoreState } from 'app/types';
import { filterLogLevels, dedupLogRows } from 'app/core/logs_model'; import { filterLogLevels, dedupLogRows } from 'app/core/logs_model';
const logsRowsSelector = (state: ExploreItemState) => state.logsResult && state.logsResult.rows; const logsRowsSelector = (state: ExploreItemState) => state.logsResult && state.logsResult.rows;
...@@ -17,3 +17,5 @@ export const deduplicatedRowsSelector = createSelector( ...@@ -17,3 +17,5 @@ export const deduplicatedRowsSelector = createSelector(
return dedupLogRows(filteredRows, dedupStrategy); return dedupLogRows(filteredRows, dedupStrategy);
} }
); );
export const isSplit = (state: StoreState) => Boolean(state.explore[ExploreId.left] && state.explore[ExploreId.right]);
import { dateTime, LoadingState } from '@grafana/data'; import { dateTime, LoadingState } from '@grafana/data';
import { makeExplorePaneState, makeInitialUpdateState } from './utils'; import { makeExplorePaneState } from './utils';
import { ExploreId, ExploreItemState } from 'app/types/explore'; import { ExploreId, ExploreItemState } from 'app/types/explore';
import { reducerTester } from 'test/core/redux/reducerTester'; import { reducerTester } from 'test/core/redux/reducerTester';
import { changeRangeAction, changeRefreshIntervalAction, timeReducer } from './time'; import { changeRangeAction, changeRefreshIntervalAction, timeReducer } from './time';
...@@ -55,7 +55,6 @@ describe('Explore item reducer', () => { ...@@ -55,7 +55,6 @@ describe('Explore item reducer', () => {
it('then it should set correct state', () => { it('then it should set correct state', () => {
reducerTester<ExploreItemState>() reducerTester<ExploreItemState>()
.givenReducer(timeReducer, ({ .givenReducer(timeReducer, ({
update: { ...makeInitialUpdateState(), range: true },
range: null, range: null,
absoluteRange: null, absoluteRange: null,
} as unknown) as ExploreItemState) } as unknown) as ExploreItemState)
...@@ -67,7 +66,6 @@ describe('Explore item reducer', () => { ...@@ -67,7 +66,6 @@ describe('Explore item reducer', () => {
}) })
) )
.thenStateShouldEqual(({ .thenStateShouldEqual(({
update: { ...makeInitialUpdateState(), range: false },
absoluteRange: { from: 1546297200000, to: 1546383600000 }, absoluteRange: { from: 1546297200000, to: 1546383600000 },
range: { from: dateTime('2019-01-01'), to: dateTime('2019-01-02'), raw: { from: 'now-1d', to: 'now' } }, range: { from: dateTime('2019-01-01'), to: dateTime('2019-01-02'), raw: { from: 'now-1d', to: 'now' } },
} as unknown) as ExploreItemState); } as unknown) as ExploreItemState);
......
...@@ -16,9 +16,7 @@ import { getTimeZone } from 'app/features/profile/state/selectors'; ...@@ -16,9 +16,7 @@ import { getTimeZone } from 'app/features/profile/state/selectors';
import { getTimeSrv } from '../../dashboard/services/TimeSrv'; import { getTimeSrv } from '../../dashboard/services/TimeSrv';
import { DashboardModel } from 'app/features/dashboard/state'; import { DashboardModel } from 'app/features/dashboard/state';
import { runQueries } from './query'; import { runQueries } from './query';
import { syncTimesAction } from './main'; import { syncTimesAction, stateSave } from './main';
import { stateSave } from './explorePane';
import { makeInitialUpdateState } from './utils';
// //
// Actions and Payloads // Actions and Payloads
...@@ -76,7 +74,7 @@ export const updateTime = (config: { ...@@ -76,7 +74,7 @@ export const updateTime = (config: {
}): ThunkResult<void> => { }): ThunkResult<void> => {
return (dispatch, getState) => { return (dispatch, getState) => {
const { exploreId, absoluteRange: absRange, rawRange: actionRange } = config; const { exploreId, absoluteRange: absRange, rawRange: actionRange } = config;
const itemState = getState().explore[exploreId]; const itemState = getState().explore[exploreId]!;
const timeZone = getTimeZone(getState().user); const timeZone = getTimeZone(getState().user);
const { range: rangeInState } = itemState; const { range: rangeInState } = itemState;
let rawRange: RawTimeRange = rangeInState.raw; let rawRange: RawTimeRange = rangeInState.raw;
...@@ -117,7 +115,7 @@ export function syncTimes(exploreId: ExploreId): ThunkResult<void> { ...@@ -117,7 +115,7 @@ export function syncTimes(exploreId: ExploreId): ThunkResult<void> {
const leftState = getState().explore.left; const leftState = getState().explore.left;
dispatch(updateTimeRange({ exploreId: ExploreId.right, rawRange: leftState.range.raw })); dispatch(updateTimeRange({ exploreId: ExploreId.right, rawRange: leftState.range.raw }));
} else { } else {
const rightState = getState().explore.right; const rightState = getState().explore.right!;
dispatch(updateTimeRange({ exploreId: ExploreId.left, rawRange: rightState.range.raw })); dispatch(updateTimeRange({ exploreId: ExploreId.left, rawRange: rightState.range.raw }));
} }
const isTimeSynced = getState().explore.syncedTimes; const isTimeSynced = getState().explore.syncedTimes;
...@@ -165,7 +163,6 @@ export const timeReducer = (state: ExploreItemState, action: AnyAction): Explore ...@@ -165,7 +163,6 @@ export const timeReducer = (state: ExploreItemState, action: AnyAction): Explore
...state, ...state,
range, range,
absoluteRange, absoluteRange,
update: makeInitialUpdateState(),
}; };
} }
......
import { import {
DataSourceApi, DataSourceApi,
EventBusExtended, EventBusExtended,
ExploreUrlState,
getDefaultTimeRange, getDefaultTimeRange,
HistoryItem, HistoryItem,
LoadingState, LoadingState,
...@@ -8,23 +9,17 @@ import { ...@@ -8,23 +9,17 @@ import {
PanelData, PanelData,
} from '@grafana/data'; } from '@grafana/data';
import { ExploreItemState, ExploreUpdateState } from 'app/types/explore'; import { ExploreItemState } from 'app/types/explore';
import { getDatasourceSrv } from '../../plugins/datasource_srv'; import { getDatasourceSrv } from '../../plugins/datasource_srv';
import store from '../../../core/store'; import store from '../../../core/store';
import { lastUsedDatasourceKeyForOrgId } from '../../../core/utils/explore'; import { clearQueryKeys, lastUsedDatasourceKeyForOrgId } from '../../../core/utils/explore';
import { toRawTimeRange } from '../utils/time';
export const DEFAULT_RANGE = { export const DEFAULT_RANGE = {
from: 'now-6h', from: 'now-6h',
to: 'now', to: 'now',
}; };
export const makeInitialUpdateState = (): ExploreUpdateState => ({
datasource: false,
queries: false,
range: false,
mode: false,
});
/** /**
* Returns a fresh Explore area state * Returns a fresh Explore area state
*/ */
...@@ -47,12 +42,9 @@ export const makeExplorePaneState = (): ExploreItemState => ({ ...@@ -47,12 +42,9 @@ export const makeExplorePaneState = (): ExploreItemState => ({
scanning: false, scanning: false,
loading: false, loading: false,
queryKeys: [], queryKeys: [],
urlState: null,
update: makeInitialUpdateState(),
latency: 0, latency: 0,
isLive: false, isLive: false,
isPaused: false, isPaused: false,
urlReplaced: false,
queryResponse: createEmptyQueryResponse(), queryResponse: createEmptyQueryResponse(),
tableResult: null, tableResult: null,
graphResult: null, graphResult: null,
...@@ -88,3 +80,13 @@ export async function loadAndInitDatasource( ...@@ -88,3 +80,13 @@ export async function loadAndInitDatasource(
store.set(lastUsedDatasourceKeyForOrgId(orgId), instance.name); store.set(lastUsedDatasourceKeyForOrgId(orgId), instance.name);
return { history, instance }; return { history, instance };
} }
export function getUrlStateFromPaneState(pane: ExploreItemState): ExploreUrlState {
return {
// datasourceInstance should not be undefined anymore here but in case there is some path for it to be undefined
// lets just fallback instead of crashing.
datasource: pane.datasourceInstance?.name || '',
queries: pane.queries.map(clearQueryKeys),
range: toRawTimeRange(pane.range),
};
}
...@@ -5,7 +5,6 @@ import { ...@@ -5,7 +5,6 @@ import {
DataQuery, DataQuery,
DataQueryRequest, DataQueryRequest,
DataSourceApi, DataSourceApi,
ExploreUrlState,
HistoryItem, HistoryItem,
LogLevel, LogLevel,
LogsDedupStrategy, LogsDedupStrategy,
...@@ -27,10 +26,6 @@ export enum ExploreId { ...@@ -27,10 +26,6 @@ export enum ExploreId {
*/ */
export interface ExploreState { export interface ExploreState {
/** /**
* True if split view is active.
*/
split: boolean;
/**
* True if time interval for panels are synced. Only possible with split mode. * True if time interval for panels are synced. Only possible with split mode.
*/ */
syncedTimes: boolean; syncedTimes: boolean;
...@@ -41,7 +36,7 @@ export interface ExploreState { ...@@ -41,7 +36,7 @@ export interface ExploreState {
/** /**
* Explore state of the right area in split view. * Explore state of the right area in split view.
*/ */
right: ExploreItemState; right?: ExploreItemState;
/** /**
* History of all queries * History of all queries
*/ */
...@@ -134,17 +129,6 @@ export interface ExploreItemState { ...@@ -134,17 +129,6 @@ export interface ExploreItemState {
*/ */
refreshInterval?: string; refreshInterval?: string;
/**
* Copy of the state of the URL which is in store.location.query. This is duplicated here so we can diff the two
* after a change to see if we need to sync url state back to redux store (like on clicking Back in browser).
*/
urlState: ExploreUrlState | null;
/**
* Map of what changed between real url and local urlState so we can partially update just the things that are needed.
*/
update: ExploreUpdateState;
latency: number; latency: number;
/** /**
...@@ -156,7 +140,6 @@ export interface ExploreItemState { ...@@ -156,7 +140,6 @@ export interface ExploreItemState {
* If true, the live tailing view is paused. * If true, the live tailing view is paused.
*/ */
isPaused: boolean; isPaused: boolean;
urlReplaced: boolean;
querySubscription?: Unsubscribable; querySubscription?: Unsubscribable;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment