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
export { getMappedValue } from './valueMappings';
export { getFlotPairs, getFlotPairsConstant } from './flotPairs';
export { locationUtil } from './location';
export { urlUtil, UrlQueryMap, UrlQueryValue } from './url';
export { urlUtil, UrlQueryMap, UrlQueryValue, serializeStateToUrlParam } from './url';
export { DataLinkBuiltInVars, mapInternalLinkToExplore } from './dataLinks';
export { DocsId } from './docs';
......@@ -22,16 +22,7 @@ const dummyProps: ExploreProps = {
datasourceMissing: false,
exploreId: ExploreId.left,
loading: false,
initializeExplore: jest.fn(),
initialized: true,
modifyQueries: jest.fn(),
update: {
datasource: false,
queries: false,
range: false,
mode: false,
},
refreshExplore: jest.fn(),
scanning: false,
scanRange: {
from: '0',
......@@ -40,18 +31,7 @@ const dummyProps: ExploreProps = {
scanStart: jest.fn(),
scanStopAction: scanStopAction,
setQueries: jest.fn(),
split: false,
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,
syncedTimes: false,
updateTimeRange: jest.fn(),
......
......@@ -15,36 +15,24 @@ import {
LoadingState,
PanelData,
RawTimeRange,
TimeRange,
TimeZone,
ExploreUrlState,
LogsModel,
EventBusExtended,
EventBusSrv,
TraceViewData,
DataFrame,
} from '@grafana/data';
import store from 'app/core/store';
import LogsContainer from './LogsContainer';
import QueryRows from './QueryRows';
import TableContainer from './TableContainer';
import RichHistoryContainer from './RichHistory/RichHistoryContainer';
import ExploreQueryInspector from './ExploreQueryInspector';
import { splitOpen } from './state/main';
import { changeSize, initializeExplore, refreshExplore } from './state/explorePane';
import { changeSize } from './state/explorePane';
import { updateTimeRange } from './state/time';
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 {
DEFAULT_RANGE,
ensureQueries,
getFirstNonQueryRowSpecificError,
getTimeRange,
getTimeRangeFromUrl,
lastUsedDatasourceKeyForOrgId,
} from 'app/core/utils/explore';
import { getFirstNonQueryRowSpecificError } from 'app/core/utils/explore';
import { ExploreToolbar } from './ExploreToolbar';
import { NoDataSourceCallToAction } from './NoDataSourceCallToAction';
import { getTimeZone } from '../profile/state/selectors';
......@@ -83,21 +71,13 @@ export interface ExploreProps {
datasourceInstance: DataSourceApi | null;
datasourceMissing: boolean;
exploreId: ExploreId;
initializeExplore: typeof initializeExplore;
initialized: boolean;
modifyQueries: typeof modifyQueries;
update: ExploreUpdateState;
refreshExplore: typeof refreshExplore;
scanning?: boolean;
scanRange?: RawTimeRange;
scanStart: typeof scanStart;
scanStopAction: typeof scanStopAction;
setQueries: typeof setQueries;
split: boolean;
queryKeys: string[];
initialDatasource: string;
initialQueries: DataQuery[];
initialRange: TimeRange;
isLive: boolean;
syncedTimes: boolean;
updateTimeRange: typeof updateTimeRange;
......@@ -153,47 +133,13 @@ interface ExploreState {
* `format`, to indicate eventual transformations by the datasources' result transformers.
*/
export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
el: any;
exploreEvents: EventBusExtended;
constructor(props: ExploreProps) {
super(props);
this.exploreEvents = new EventBusSrv();
this.state = {
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) => {
const { updateTimeRange, exploreId } = this.props;
updateTimeRange({ exploreId, rawRange });
......@@ -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() {
return (
<div className="explore-container">
......@@ -367,7 +305,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
datasourceInstance,
datasourceMissing,
exploreId,
split,
queryKeys,
graphResult,
queryResponse,
......@@ -380,7 +317,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
showNodeGraph,
} = this.props;
const { openDrawer } = this.state;
const exploreClass = split ? 'explore explore-split' : 'explore';
const styles = getStyles(theme);
const showPanels = queryResponse && queryResponse.state !== LoadingState.NotStarted;
......@@ -393,13 +329,13 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
const showQueryInspector = openDrawer === ExploreDrawer.QueryInspector;
return (
<div className={exploreClass} ref={this.getRef} aria-label={selectors.pages.Explore.General.container}>
<>
<ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} />
{datasourceMissing ? this.renderEmptyState() : null}
{datasourceInstance && (
<div className="explore-container">
<div className={cx('panel-container', styles.queryContainer)}>
<QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} queryKeys={queryKeys} />
<QueryRows exploreId={exploreId} queryKeys={queryKeys} />
<SecondaryActions
addQueryRowButtonDisabled={isLive}
// 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> {
</AutoSizer>
</div>
)}
</div>
</>
);
}
}
const ensureQueriesMemoized = memoizeOne(ensureQueries);
const getTimeRangeFromUrlMemoized = memoizeOne(getTimeRangeFromUrl);
function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partial<ExploreProps> {
const explore = state.explore;
const { split, syncedTimes } = explore;
const item: ExploreItemState = explore[exploreId];
const { syncedTimes } = explore;
const item: ExploreItemState = explore[exploreId]!;
const timeZone = getTimeZone(state.user);
const {
datasourceInstance,
datasourceMissing,
initialized,
queryKeys,
urlState,
update,
isLive,
graphResult,
logsResult,
......@@ -485,29 +415,15 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
loading,
} = 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 {
datasourceInstance,
datasourceMissing,
initialized,
split,
queryKeys,
update,
initialDatasource,
initialQueries,
initialRange,
isLive,
graphResult,
logsResult: logsResult ?? undefined,
absoluteRange,
queryResponse,
originPanelId,
syncedTimes,
timeZone,
showLogs,
......@@ -521,9 +437,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partia
const mapDispatchToProps: Partial<ExploreProps> = {
changeSize,
initializeExplore,
modifyQueries,
refreshExplore,
scanStart,
scanStopAction,
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) {
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
const explore = state.explore;
const item: ExploreItemState = explore[exploreId];
const item: ExploreItemState = explore[exploreId]!;
const { loading, queryResponse } = item;
return {
......
......@@ -21,6 +21,7 @@ import { RunButton } from './RunButton';
import { LiveTailControls } from './useLiveTailControls';
import { cancelQueries, clearQueries, runQueries } from './state/query';
import ReturnToDashboardButton from './ReturnToDashboardButton';
import { isSplit } from './state/selectors';
interface OwnProps {
exploreId: ExploreId;
......@@ -127,7 +128,12 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
)}
</div>
{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>
......@@ -227,9 +233,8 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
}
const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps => {
const splitted = state.explore.split;
const syncedTimes = state.explore.syncedTimes;
const exploreItem: ExploreItemState = state.explore[exploreId];
const exploreItem: ExploreItemState = state.explore[exploreId]!;
const {
datasourceInstance,
datasourceMissing,
......@@ -249,7 +254,7 @@ const mapStateToProps = (state: StoreState, { exploreId }: OwnProps): StateProps
loading,
range,
timeZone: getTimeZone(state.user),
splitted,
splitted: isSplit(state),
refreshInterval,
hasLiveOption,
isLive,
......
......@@ -36,7 +36,7 @@ export function UnconnectedNodeGraphContainer(props: Props & ConnectedProps<type
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
return {
range: state.explore[exploreId].range,
range: state.explore[exploreId]!.range,
};
}
......
......@@ -31,7 +31,6 @@ import { HelpToggle } from '../query/components/HelpToggle';
interface PropsFromParent {
exploreId: ExploreId;
index: number;
exploreEvents: EventBusExtended;
}
export interface QueryRowProps extends PropsFromParent {
......@@ -49,6 +48,7 @@ export interface QueryRowProps extends PropsFromParent {
runQueries: typeof runQueries;
queryResponse: PanelData;
latency: number;
exploreEvents: EventBusExtended;
}
interface QueryRowState {
......@@ -201,8 +201,8 @@ export class QueryRow extends PureComponent<QueryRowProps, QueryRowState> {
function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps) {
const explore = state.explore;
const item: ExploreItemState = explore[exploreId];
const { datasourceInstance, history, queries, range, absoluteRange, queryResponse, latency } = item;
const item: ExploreItemState = explore[exploreId]!;
const { datasourceInstance, history, queries, range, absoluteRange, queryResponse, latency, eventBridge } = item;
const query = queries[index];
return {
......@@ -213,6 +213,7 @@ function mapStateToProps(state: StoreState, { exploreId, index }: QueryRowProps)
absoluteRange,
queryResponse,
latency,
exploreEvents: eventBridge,
};
}
......
......@@ -5,23 +5,21 @@ import React, { PureComponent } from 'react';
import QueryRow from './QueryRow';
// Types
import { EventBusExtended } from '@grafana/data';
import { ExploreId } from 'app/types/explore';
interface QueryRowsProps {
className?: string;
exploreEvents: EventBusExtended;
exploreId: ExploreId;
queryKeys: string[];
}
export default class QueryRows extends PureComponent<QueryRowsProps> {
render() {
const { className = '', exploreEvents, exploreId, queryKeys } = this.props;
const { className = '', exploreId, queryKeys } = this.props;
return (
<div className={className}>
{queryKeys.map((key, index) => {
return <QueryRow key={key} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />;
return <QueryRow key={key} exploreId={exploreId} index={index} />;
})}
</div>
);
......
......@@ -10,6 +10,7 @@ import { StoreState } from 'app/types';
import { ExploreId } from 'app/types/explore';
import { updateLocation } from 'app/core/actions';
import { setDashboardQueriesToUpdateOnLoad } from '../dashboard/state/reducers';
import { isSplit } from './state/selectors';
interface Props {
exploreId: ExploreId;
......@@ -83,8 +84,8 @@ export const UnconnectedReturnToDashboardButton: FC<Props> = ({
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
const explore = state.explore;
const splitted = state.explore.split;
const { datasourceInstance, queries, originPanelId } = explore[exploreId];
const splitted = isSplit(state);
const { datasourceInstance, queries, originPanelId } = explore[exploreId]!;
return {
exploreId,
......
......@@ -5,7 +5,7 @@ import { css, cx } from 'emotion';
import { stylesFactory, useTheme, TextArea, Button, IconButton } from '@grafana/ui';
import { getDataSourceSrv } from '@grafana/runtime';
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 { createAndCopyShortLink } from 'app/core/utils/shortLinks';
import { copyStringToClipboard } from 'app/core/utils/explore';
......@@ -313,9 +313,7 @@ export function RichHistoryCard(props: Props) {
function mapStateToProps(state: StoreState, { exploreId }: { exploreId: ExploreId }) {
const explore = state.explore;
const { datasourceInstance } = explore[exploreId];
// @ts-ignore
const item: ExploreItemState = explore[exploreId];
const { datasourceInstance } = explore[exploreId]!;
return {
exploreId,
datasourceInstance,
......
......@@ -3,7 +3,6 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import Wrapper from './Wrapper';
import { configureStore } from '../../store/configureStore';
import { Provider } from 'react-redux';
import { store } from '../../store/store';
import { setDataSourceSrv } from '@grafana/runtime';
import {
ArrayDataFrame,
......@@ -22,6 +21,9 @@ import { updateLocation } from '../../core/reducers/location';
import { LokiDatasource } from '../../plugins/datasource/loki/datasource';
import { LokiQuery } from '../../plugins/datasource/loki/types';
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;
......@@ -42,7 +44,7 @@ describe('Wrapper', () => {
});
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
await screen.findByText(/Editor/i);
......@@ -57,7 +59,7 @@ describe('Wrapper', () => {
it('runs query when url contains query and renders results', async () => {
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());
// Make sure we render the logs panel
......@@ -90,7 +92,7 @@ describe('Wrapper', () => {
it('handles url change and runs the new query', async () => {
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());
// Wait for rendering the logs
await screen.findByText(/custom log line/i);
......@@ -111,7 +113,7 @@ describe('Wrapper', () => {
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 { datasources } = setup({ query });
const { datasources, store } = setup({ query });
(datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse());
// Wait for rendering the logs
await screen.findByText(/custom log line/i);
......@@ -133,7 +135,7 @@ describe('Wrapper', () => {
it('handles changing the datasource manually', async () => {
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());
// Wait for rendering the editor
await screen.findByText(/Editor/i);
......@@ -147,15 +149,15 @@ describe('Wrapper', () => {
});
});
it('opens the split pane', async () => {
const { datasources } = setup();
it('opens the split pane when split button is clicked', async () => {
setup();
// Wait for rendering the editor
const splitButton = await screen.findByText(/split/i);
fireEvent.click(splitButton);
const editors = await screen.findAllByText('loki Editor input:');
expect(editors.length).toBe(2);
expect(datasources.loki.query).not.toBeCalled();
await waitFor(() => {
const editors = screen.getAllByText('loki Editor input:');
expect(editors.length).toBe(2);
});
});
it('inits with two panes if specified in url', async () => {
......@@ -164,7 +166,7 @@ describe('Wrapper', () => {
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.elastic.query as Mock).mockReturnValueOnce(makeLogsQueryResponse());
......@@ -199,6 +201,65 @@ describe('Wrapper', () => {
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 };
......@@ -206,7 +267,7 @@ type SetupOptions = {
datasources?: DatasourceSetup[];
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
// TODO: probably add test for that too
window.localStorage.clear();
......@@ -238,15 +299,18 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou
},
} as any);
configureStore();
const store = configureStore();
store.getState().user = {
orgId: 1,
timeZone: 'utc',
};
store.getState().location.path = '/explore';
if (options?.query) {
// We have to dispatch cause right now we take the url state from the action not from the store
store.dispatch(updateLocation({ query: options.query, path: '/explore' }));
store.getState().location = {
...store.getState().location,
query: options.query,
};
}
render(
......@@ -254,7 +318,7 @@ function setup(options?: SetupOptions): { datasources: { [name: string]: DataSou
<Wrapper />
</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 {
......
......@@ -6,9 +6,9 @@ import { StoreState } from 'app/types';
import { ExploreId } from 'app/types/explore';
import { CustomScrollbar, ErrorBoundaryAlert } from '@grafana/ui';
import { resetExploreAction, richHistoryUpdatedAction } from './state/main';
import Explore from './Explore';
import { lastSavedUrl, resetExploreAction, richHistoryUpdatedAction } from './state/main';
import { getRichHistory } from '../../core/utils/richHistory';
import { ExplorePaneContainer } from './ExplorePaneContainer';
interface WrapperProps {
split: boolean;
......@@ -22,6 +22,9 @@ export class Wrapper extends Component<WrapperProps> {
}
componentDidMount() {
lastSavedUrl.left = undefined;
lastSavedUrl.right = undefined;
const richHistory = getRichHistory();
this.props.richHistoryUpdatedAction({ richHistory });
}
......@@ -34,11 +37,11 @@ export class Wrapper extends Component<WrapperProps> {
<CustomScrollbar autoHeightMin={'100%'}>
<div className="explore-wrapper">
<ErrorBoundaryAlert style="page">
<Explore exploreId={ExploreId.left} />
<ExplorePaneContainer split={split} exploreId={ExploreId.left} />
</ErrorBoundaryAlert>
{split && (
<ErrorBoundaryAlert style="page">
<Explore exploreId={ExploreId.right} />
<ExplorePaneContainer split={split} exploreId={ExploreId.right} />
</ErrorBoundaryAlert>
)}
</div>
......@@ -49,8 +52,11 @@ export class Wrapper extends Component<WrapperProps> {
}
const mapStateToProps = (state: StoreState) => {
const { split } = state.explore;
return { split };
// Here we use URL to say if we should split or not which is different than in other places. Reason is if we change
// 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 = {
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Explore should render component 1`] = `
<div
aria-label="Explore"
className="explore"
>
<Fragment>
<Connect(UnConnectedExploreToolbar)
exploreId="left"
onChangeTime={[Function]}
......@@ -16,14 +13,6 @@ exports[`Explore should render component 1`] = `
className="panel-container css-kj45dn-queryContainer"
>
<QueryRows
exploreEvents={
EventBusSrv {
"emitter": EventEmitter {
"_events": Object {},
"_eventsCount": 0,
},
}
}
exploreId="left"
queryKeys={Array []}
/>
......@@ -47,5 +36,5 @@ exports[`Explore should render component 1`] = `
<Component />
</AutoSizer>
</div>
</div>
</Fragment>
`;
......@@ -8,7 +8,7 @@ import { ExploreItemState, ThunkResult } from 'app/types';
import { ExploreId } from 'app/types/explore';
import { importQueries, runQueries } from './query';
import { changeRefreshInterval } from './time';
import { createEmptyQueryResponse, loadAndInitDatasource, makeInitialUpdateState } from './utils';
import { createEmptyQueryResponse, loadAndInitDatasource } from './utils';
//
// Actions and Payloads
......@@ -41,7 +41,7 @@ export function changeDatasource(
return async (dispatch, getState) => {
const orgId = getState().user.orgId;
const { history, instance } = await loadAndInitDatasource(orgId, datasourceName);
const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance;
const currentDataSourceInstance = getState().explore[exploreId]!.datasourceInstance;
dispatch(
updateDatasourceInstanceAction({
......@@ -51,13 +51,13 @@ export function changeDatasource(
})
);
const queries = getState().explore[exploreId].queries;
const queries = getState().explore[exploreId]!.queries;
if (options?.importQueries) {
await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, instance));
}
if (getState().explore[exploreId].isLive) {
if (getState().explore[exploreId]!.isLive) {
dispatch(changeRefreshInterval(exploreId, RefreshPicker.offOption.value));
}
......@@ -97,11 +97,9 @@ export const datasourceReducer = (state: ExploreItemState, action: AnyAction): E
queryResponse: createEmptyQueryResponse(),
loading: false,
queryKeys: [],
originPanelId: state.urlState && state.urlState.originPanelId,
history,
datasourceMissing: false,
logsHighlighterExpressions: undefined,
update: makeInitialUpdateState(),
};
}
......
import { PayloadAction } from '@reduxjs/toolkit';
import { DataQuery, DefaultTimeZone, EventBusExtended, ExploreUrlState, LogsDedupStrategy, toUtc } from '@grafana/data';
import { ExploreId, ExploreItemState, ExploreUpdateState } from 'app/types';
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 { DataQuery, DefaultTimeZone, EventBusExtended, serializeStateToUrlParam, toUtc } from '@grafana/data';
import { ExploreId } from 'app/types';
import { refreshExplore } from './explorePane';
import { setDataSourceSrv } from '@grafana/runtime';
import { configureStore } from '../../../store/configureStore';
import { of } from 'rxjs';
jest.mock('../../dashboard/services/TimeSrv', () => ({
getTimeSrv: jest.fn().mockReturnValue({
......@@ -30,134 +21,125 @@ const testRange = {
},
};
setDataSourceSrv({
getList() {
return [];
const defaultInitialState = {
user: {
orgId: '1',
timeZone: DefaultTimeZone,
},
getInstanceSettings(name: string) {
return { name: 'hello' };
},
get() {
return Promise.resolve({
testDatasource: jest.fn(),
init: jest.fn(),
});
explore: {
[ExploreId.left]: {
initialized: true,
containerWidth: 1920,
eventBridge: {} as EventBusExtended,
queries: [] as DataQuery[],
range: testRange,
refreshInterval: {
label: 'Off',
value: 0,
},
},
},
} as any);
};
const setup = (updateOverides?: Partial<ExploreUpdateState>) => {
const exploreId = ExploreId.left;
const containerWidth = 1920;
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,
},
function setupStore(state?: any) {
return configureStore({
...defaultInitialState,
explore: {
[exploreId]: {
initialized: true,
urlState,
containerWidth,
eventBridge,
update,
datasourceInstance: { name: 'some-datasource' },
queries: [] as DataQuery[],
range,
refreshInterval: {
label: 'Off',
value: 0,
},
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
...(state || {}),
},
},
};
} as any);
}
return {
initialState,
exploreId,
range,
containerWidth,
eventBridge,
function setup(state?: any) {
const datasources: Record<string, any> = {
newDs: {
testDatasource: jest.fn(),
init: jest.fn(),
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', () => {
describe('when explore is initialized', () => {
describe('and update datasource is set', () => {
it('then it should dispatch initializeExplore', async () => {
const { exploreId, initialState, containerWidth, eventBridge } = setup({ datasource: true });
const dispatchedActions = await thunkTester(initialState)
.givenThunk(refreshExplore)
.whenThunkIsDispatched(exploreId);
const initializeExplore = dispatchedActions.find((action) => action.type === initializeExploreAction.type);
const { type, payload } = initializeExplore as PayloadAction<InitializeExplorePayload>;
expect(type).toEqual(initializeExploreAction.type);
expect(payload.containerWidth).toEqual(containerWidth);
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);
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);
setDataSourceSrv({
getList() {
return Object.values(datasources).map((d) => ({ name: d.name }));
},
getInstanceSettings(name: string) {
return { name: 'hello' };
},
get(name?: string) {
return Promise.resolve(
name
? datasources[name]
: {
testDatasource: jest.fn(),
init: jest.fn(),
name: 'default',
}
);
},
} as any);
expect(dispatchedActions[0].type).toEqual(setQueriesAction.type);
expect(dispatchedActions[0].payload).toEqual({ exploreId, queries: [] });
});
});
const store = setupStore({
datasourceInstance: datasources.someDs,
...(state || {}),
});
describe('when update is not initialized', () => {
it('then it should not dispatch any actions', async () => {
const exploreId = ExploreId.left;
const initialState = { explore: { [exploreId]: { initialized: false } } };
const dispatchedActions = await thunkTester(initialState)
.givenThunk(refreshExplore)
.whenThunkIsDispatched(exploreId);
return {
store,
datasources,
};
}
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', () => {
describe('changing dedup strategy', () => {
describe('when changeDedupStrategyAction is dispatched', () => {
it('then it should set correct dedup strategy in state', () => {
const initialState = makeExplorePaneState();
it('should change and run new queries from the URL', async () => {
const { store, datasources } = setup();
datasources.someDs.query.mockReturnValueOnce(of({}));
await store.dispatch(
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>()
.givenReducer(paneReducer, initialState)
.whenActionIsDispatched(
changeDedupStrategyAction({ exploreId: ExploreId.left, dedupStrategy: LogsDedupStrategy.exact })
)
.thenStateShouldEqual({
...initialState,
dedupStrategy: LogsDedupStrategy.exact,
});
});
it('should not do anything if pane is not initialized', async () => {
const { store } = setup({
initialized: false,
});
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 { 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 { queryReducer, runQueries, setQueriesAction } from './query';
import { datasourceReducer } from './datasource';
import { timeReducer, updateTime } from './time';
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 {
EventBusExtended,
......@@ -17,20 +31,12 @@ import {
HistoryItem,
DataSourceApi,
} from '@grafana/data';
import {
clearQueryKeys,
ensureQueries,
generateNewKeyAndAddRefIdIfMissing,
getTimeRangeFromUrl,
getQueryKeys,
} from 'app/core/utils/explore';
// Types
import { ThunkResult } from 'app/types';
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 { getRichHistory } from '../../../core/utils/richHistory';
import { richHistoryUpdatedAction } from './main';
//
// Actions and Payloads
......@@ -155,52 +161,35 @@ export function initializeExplore(
dispatch(updateTime({ exploreId }));
if (instance) {
dispatch(runQueries(exploreId));
}
};
}
/**
* 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);
// 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 }));
}
dispatch(updateLocation({ query: urlStates, replace }));
if (replace) {
dispatch(setUrlReplacedAction({ exploreId: ExploreId.left }));
}
const richHistory = getRichHistory();
dispatch(richHistoryUpdatedAction({ richHistory }));
};
};
}
/**
* Reacts to changes in URL state that we need to sync back to our redux state. Checks the internal update variable
* to see which parts change and need to be synced.
* @param exploreId
* Reacts to changes in URL state that we need to sync back to our redux state. Computes diff of newUrlQuery vs current
* state and runs update actions for relevant parts.
*/
export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
return (dispatch, getState) => {
const itemState = getState().explore[exploreId];
export function refreshExplore(exploreId: ExploreId, newUrlQuery: string): ThunkResult<void> {
return async (dispatch, getState) => {
const itemState = getState().explore[exploreId]!;
if (!itemState.initialized) {
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) {
return;
}
const { containerWidth, eventBridge } = itemState;
const { datasource, queries, range: urlRange, originPanelId } = urlState;
const { datasource, queries, range: urlRange, originPanelId } = newUrlState;
const refreshQueries: DataQuery[] = [];
for (let index = 0; index < queries.length; index++) {
......@@ -211,10 +200,11 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
const timeZone = getTimeZone(getState().user);
const range = getTimeRangeFromUrl(urlRange, timeZone);
// need to refresh datasource
// commit changes based on the diff of new url vs old url
if (update.datasource) {
const initialQueries = ensureQueries(queries);
dispatch(
await dispatch(
initializeExplore(exploreId, datasource, initialQueries, range, containerWidth, eventBridge, originPanelId)
);
return;
......@@ -224,7 +214,6 @@ export function refreshExplore(exploreId: ExploreId): ThunkResult<void> {
dispatch(updateTime({ exploreId, rawRange: range.raw }));
}
// need to refresh queries
if (update.queries) {
dispatch(setQueriesAction({ exploreId, queries: refreshQueries }));
}
......@@ -286,7 +275,6 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
initialized: true,
queryKeys: getQueryKeys(queries, datasourceInstance),
originPanelId,
update: makeInitialUpdateState(),
datasourceInstance,
history,
datasourceMissing: !datasourceInstance,
......@@ -303,22 +291,28 @@ export const paneReducer = (state: ExploreItemState = makeExplorePaneState(), ac
};
}
if (setUrlReplacedAction.match(action)) {
return {
...state,
urlReplaced: true,
};
}
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 {
// It can happen that if we are in a split and initial load also runs queries we can be here before the second pane
// is initialized so datasourceInstance will be still undefined.
datasource: pane.datasourceInstance?.name || pane.urlState!.datasource,
queries: pane.queries.map(clearQueryKeys),
range: toRawTimeRange(pane.range),
datasource,
queries,
range,
};
}
};
......@@ -35,12 +35,11 @@ import {
decorateWithTableResult,
} from '../utils/decorators';
import { createErrorNotification } from '../../../core/copy/appNotification';
import { richHistoryUpdatedAction } from './main';
import { stateSave } from './explorePane';
import { richHistoryUpdatedAction, stateSave } from './main';
import { AnyAction, createAction, PayloadAction } from '@reduxjs/toolkit';
import { updateTime } from './time';
import { historyUpdatedAction } from './history';
import { createEmptyQueryResponse, makeInitialUpdateState } from './utils';
import { createEmptyQueryResponse } from './utils';
//
// Actions and Payloads
......@@ -174,7 +173,7 @@ export const scanStopAction = createAction<ScanStopPayload>('explore/scanStop');
*/
export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
return (dispatch, getState) => {
const queries = getState().explore[exploreId].queries;
const queries = getState().explore[exploreId]!.queries;
const query = generateEmptyQuery(queries, index);
dispatch(addQueryRowAction({ exploreId, index, query }));
......@@ -194,7 +193,7 @@ export function changeQuery(
return (dispatch, getState) => {
// Null query means reset
if (query === null) {
const queries = getState().explore[exploreId].queries;
const queries = getState().explore[exploreId]!.queries;
const { refId, key } = queries[index];
query = generateNewKeyAndAddRefIdIfMissing({ refId, key }, queries, index);
}
......@@ -292,12 +291,12 @@ export function modifyQueries(
/**
* 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) => {
dispatch(updateTime({ exploreId }));
const richHistory = getState().explore.richHistory;
const exploreItemState = getState().explore[exploreId];
const exploreItemState = getState().explore[exploreId]!;
const {
datasourceInstance,
queries,
......@@ -314,7 +313,7 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
if (!hasNonEmptyQuery(queries)) {
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;
}
......@@ -379,7 +378,7 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
dispatch(richHistoryUpdatedAction({ richHistory: nextRichHistory }));
// 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;
......@@ -387,9 +386,9 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
dispatch(queryStreamUpdatedAction({ exploreId, response: data }));
// 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) {
const range = getShiftedTimeRange(-1, getState().explore[exploreId].range);
const range = getShiftedTimeRange(-1, getState().explore[exploreId]!.range);
dispatch(updateTime({ exploreId, absoluteRange: range }));
dispatch(runQueries(exploreId));
} else {
......@@ -416,7 +415,7 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult<void> {
return (dispatch, getState) => {
// 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));
dispatch(setQueriesAction({ exploreId, queries: nextQueries }));
dispatch(runQueries(exploreId));
......@@ -433,7 +432,7 @@ export function scanStart(exploreId: ExploreId): ThunkResult<void> {
// Register the scanner
dispatch(scanStartAction({ exploreId }));
// 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
dispatch(updateTime({ exploreId, absoluteRange: range }));
dispatch(runQueries(exploreId));
......@@ -627,7 +626,6 @@ export const queryReducer = (state: ExploreItemState, action: AnyAction): Explor
...state,
scanning: false,
scanRange: undefined,
update: makeInitialUpdateState(),
};
}
......@@ -687,7 +685,6 @@ export const processQueryResponse = (
tableResult,
logsResult,
loading: loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming,
update: makeInitialUpdateState(),
showLogs: !!logsResult,
showMetrics: !!graphResult,
showTable: !!tableResult,
......
import { createSelector } from 'reselect';
import { ExploreItemState } from 'app/types';
import { ExploreId, ExploreItemState, StoreState } from 'app/types';
import { filterLogLevels, dedupLogRows } from 'app/core/logs_model';
const logsRowsSelector = (state: ExploreItemState) => state.logsResult && state.logsResult.rows;
......@@ -17,3 +17,5 @@ export const deduplicatedRowsSelector = createSelector(
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 { makeExplorePaneState, makeInitialUpdateState } from './utils';
import { makeExplorePaneState } from './utils';
import { ExploreId, ExploreItemState } from 'app/types/explore';
import { reducerTester } from 'test/core/redux/reducerTester';
import { changeRangeAction, changeRefreshIntervalAction, timeReducer } from './time';
......@@ -55,7 +55,6 @@ describe('Explore item reducer', () => {
it('then it should set correct state', () => {
reducerTester<ExploreItemState>()
.givenReducer(timeReducer, ({
update: { ...makeInitialUpdateState(), range: true },
range: null,
absoluteRange: null,
} as unknown) as ExploreItemState)
......@@ -67,7 +66,6 @@ describe('Explore item reducer', () => {
})
)
.thenStateShouldEqual(({
update: { ...makeInitialUpdateState(), range: false },
absoluteRange: { from: 1546297200000, to: 1546383600000 },
range: { from: dateTime('2019-01-01'), to: dateTime('2019-01-02'), raw: { from: 'now-1d', to: 'now' } },
} as unknown) as ExploreItemState);
......
......@@ -16,9 +16,7 @@ import { getTimeZone } from 'app/features/profile/state/selectors';
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
import { DashboardModel } from 'app/features/dashboard/state';
import { runQueries } from './query';
import { syncTimesAction } from './main';
import { stateSave } from './explorePane';
import { makeInitialUpdateState } from './utils';
import { syncTimesAction, stateSave } from './main';
//
// Actions and Payloads
......@@ -76,7 +74,7 @@ export const updateTime = (config: {
}): ThunkResult<void> => {
return (dispatch, getState) => {
const { exploreId, absoluteRange: absRange, rawRange: actionRange } = config;
const itemState = getState().explore[exploreId];
const itemState = getState().explore[exploreId]!;
const timeZone = getTimeZone(getState().user);
const { range: rangeInState } = itemState;
let rawRange: RawTimeRange = rangeInState.raw;
......@@ -117,7 +115,7 @@ export function syncTimes(exploreId: ExploreId): ThunkResult<void> {
const leftState = getState().explore.left;
dispatch(updateTimeRange({ exploreId: ExploreId.right, rawRange: leftState.range.raw }));
} else {
const rightState = getState().explore.right;
const rightState = getState().explore.right!;
dispatch(updateTimeRange({ exploreId: ExploreId.left, rawRange: rightState.range.raw }));
}
const isTimeSynced = getState().explore.syncedTimes;
......@@ -165,7 +163,6 @@ export const timeReducer = (state: ExploreItemState, action: AnyAction): Explore
...state,
range,
absoluteRange,
update: makeInitialUpdateState(),
};
}
......
import {
DataSourceApi,
EventBusExtended,
ExploreUrlState,
getDefaultTimeRange,
HistoryItem,
LoadingState,
......@@ -8,23 +9,17 @@ import {
PanelData,
} from '@grafana/data';
import { ExploreItemState, ExploreUpdateState } from 'app/types/explore';
import { ExploreItemState } from 'app/types/explore';
import { getDatasourceSrv } from '../../plugins/datasource_srv';
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 = {
from: 'now-6h',
to: 'now',
};
export const makeInitialUpdateState = (): ExploreUpdateState => ({
datasource: false,
queries: false,
range: false,
mode: false,
});
/**
* Returns a fresh Explore area state
*/
......@@ -47,12 +42,9 @@ export const makeExplorePaneState = (): ExploreItemState => ({
scanning: false,
loading: false,
queryKeys: [],
urlState: null,
update: makeInitialUpdateState(),
latency: 0,
isLive: false,
isPaused: false,
urlReplaced: false,
queryResponse: createEmptyQueryResponse(),
tableResult: null,
graphResult: null,
......@@ -88,3 +80,13 @@ export async function loadAndInitDatasource(
store.set(lastUsedDatasourceKeyForOrgId(orgId), instance.name);
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 {
DataQuery,
DataQueryRequest,
DataSourceApi,
ExploreUrlState,
HistoryItem,
LogLevel,
LogsDedupStrategy,
......@@ -27,10 +26,6 @@ export enum ExploreId {
*/
export interface ExploreState {
/**
* True if split view is active.
*/
split: boolean;
/**
* True if time interval for panels are synced. Only possible with split mode.
*/
syncedTimes: boolean;
......@@ -41,7 +36,7 @@ export interface ExploreState {
/**
* Explore state of the right area in split view.
*/
right: ExploreItemState;
right?: ExploreItemState;
/**
* History of all queries
*/
......@@ -134,17 +129,6 @@ export interface ExploreItemState {
*/
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;
/**
......@@ -156,7 +140,6 @@ export interface ExploreItemState {
* If true, the live tailing view is paused.
*/
isPaused: boolean;
urlReplaced: boolean;
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