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>
This commit is contained in:
Andrej Ocenas 2021-02-12 21:33:26 +01:00 committed by GitHub
parent 9629dded42
commit b0bd242eda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 556 additions and 835 deletions

View File

@ -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';

View File

@ -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(),

View File

@ -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,

View File

@ -0,0 +1,130 @@
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);

View File

@ -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 {

View File

@ -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,

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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>
);

View File

@ -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,

View File

@ -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,

View File

@ -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 {

View File

@ -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 = {

View File

@ -1,10 +1,7 @@
// 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>
`;

View File

@ -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(),
};
}

View File

@ -1,18 +1,9 @@
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(),
});
},
} 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,
},
explore: {
[exploreId]: {
initialized: true,
urlState,
containerWidth,
eventBridge,
update,
datasourceInstance: { name: 'some-datasource' },
queries: [] as DataQuery[],
range,
refreshInterval: {
label: 'Off',
value: 0,
},
explore: {
[ExploreId.left]: {
initialized: true,
containerWidth: 1920,
eventBridge: {} as EventBusExtended,
queries: [] as DataQuery[],
range: testRange,
refreshInterval: {
label: 'Off',
value: 0,
},
},
};
return {
initialState,
exploreId,
range,
containerWidth,
eventBridge,
};
},
};
function setupStore(state?: any) {
return configureStore({
...defaultInitialState,
explore: {
[ExploreId.left]: {
...defaultInitialState.explore[ExploreId.left],
...(state || {}),
},
},
} as any);
}
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' },
},
};
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);
const store = setupStore({
datasourceInstance: datasources.someDs,
...(state || {}),
});
return {
store,
datasources,
};
}
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);
expect(dispatchedActions[0].type).toEqual(setQueriesAction.type);
expect(dispatchedActions[0].payload).toEqual({ exploreId, queries: [] });
});
});
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('when update is not initialized', () => {
it('then it should not dispatch any actions', async () => {
const exploreId = ExploreId.left;
const initialState = { explore: { [exploreId]: { initialized: false } } };
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);
});
const dispatchedActions = await thunkTester(initialState)
.givenThunk(refreshExplore)
.whenThunkIsDispatched(exploreId);
expect(dispatchedActions).toEqual([]);
});
});
});
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();
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());
});
});

View File

@ -1,11 +1,25 @@
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));
// 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 }));
}
const richHistory = getRichHistory();
dispatch(richHistoryUpdatedAction({ richHistory }));
};
}
/**
* 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.
* 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 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 }));
if (replace) {
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
* to see which parts change and need to be synced.
* @param exploreId
*/
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,
};
}
};

View File

@ -1,13 +1,12 @@
import { serializeStateToUrlParam } from '@grafana/data/src/utils/url';
import { exploreReducer, initialExploreState, navigateToExplore, splitCloseAction, splitOpenAction } from './main';
import { exploreReducer, navigateToExplore, splitCloseAction } from './main';
import { thunkTester } from 'test/core/thunk/thunkTester';
import { PanelModel } from 'app/features/dashboard/state';
import { updateLocation } from '../../../core/actions';
import { MockDataSourceApi } from '../../../../test/mocks/datasource_srv';
import { ExploreId, ExploreItemState, ExploreState } from '../../../types';
import { makeExplorePaneState, makeInitialUpdateState } from './utils';
import { reducerTester } from '../../../../test/core/redux/reducerTester';
import { ExploreUrlState, UrlQueryMap } from '@grafana/data';
import { ExploreUrlState } from '@grafana/data';
const getNavigateToExploreContext = async (openInNewWindow?: (url: string) => void) => {
const url = 'http://www.someurl.com';
@ -137,27 +136,6 @@ describe('navigateToExplore', () => {
describe('Explore reducer', () => {
describe('split view', () => {
it("should make right pane a duplicate of the given item's state on split open", () => {
const leftItemMock = ({
containerWidth: 100,
} as unknown) as ExploreItemState;
const initialState = ({
split: null,
left: leftItemMock as ExploreItemState,
right: makeExplorePaneState(),
} as unknown) as ExploreState;
reducerTester<ExploreState>()
.givenReducer(exploreReducer, initialState)
.whenActionIsDispatched(splitOpenAction({ itemState: leftItemMock }))
.thenStateShouldEqual(({
split: true,
left: leftItemMock,
right: leftItemMock,
} as unknown) as ExploreState);
});
describe('split close', () => {
it('should keep right pane as left when left is closed', () => {
const leftItemMock = ({
@ -169,7 +147,6 @@ describe('Explore reducer', () => {
} as unknown) as ExploreItemState;
const initialState = ({
split: null,
left: leftItemMock,
right: rightItemMock,
} as unknown) as ExploreState;
@ -179,9 +156,8 @@ describe('Explore reducer', () => {
.givenReducer(exploreReducer, initialState)
.whenActionIsDispatched(splitCloseAction({ itemId: ExploreId.left }))
.thenStateShouldEqual(({
split: false,
left: rightItemMock,
right: initialExploreState.right,
right: undefined,
} as unknown) as ExploreState);
});
it('should reset right pane when it is closed ', () => {
@ -194,7 +170,6 @@ describe('Explore reducer', () => {
} as unknown) as ExploreItemState;
const initialState = ({
split: null,
left: leftItemMock,
right: rightItemMock,
} as unknown) as ExploreState;
@ -204,238 +179,15 @@ describe('Explore reducer', () => {
.givenReducer(exploreReducer, initialState)
.whenActionIsDispatched(splitCloseAction({ itemId: ExploreId.right }))
.thenStateShouldEqual(({
split: false,
left: leftItemMock,
right: initialExploreState.right,
right: undefined,
} as unknown) as ExploreState);
});
});
});
describe('when updateLocation is dispatched', () => {
describe('and payload does not contain a query', () => {
it('then it should just return state', () => {
reducerTester<ExploreState>()
.givenReducer(exploreReducer, ({} as unknown) as ExploreState)
.whenActionIsDispatched(updateLocation({ query: (null as unknown) as UrlQueryMap }))
.thenStateShouldEqual(({} as unknown) as ExploreState);
});
});
describe('and payload contains a query', () => {
describe("but does not contain 'left'", () => {
it('then it should just return state', () => {
reducerTester<ExploreState>()
.givenReducer(exploreReducer, ({} as unknown) as ExploreState)
.whenActionIsDispatched(updateLocation({ query: {} }))
.thenStateShouldEqual(({} as unknown) as ExploreState);
});
});
describe("and query contains a 'right'", () => {
it('then it should add split in state', () => {
const { initialState, serializedUrlState } = setup();
const expectedState = { ...initialState, split: true };
reducerTester<ExploreState>()
.givenReducer(exploreReducer, initialState)
.whenActionIsDispatched(
updateLocation({
query: {
left: serializedUrlState,
right: serializedUrlState,
},
})
)
.thenStateShouldEqual(expectedState);
});
});
describe("and query contains a 'left'", () => {
describe('but urlState is not set in state', () => {
it('then it should just add urlState and update in state', () => {
const { initialState, serializedUrlState } = setup();
const urlState: ExploreUrlState = (null as unknown) as ExploreUrlState;
const stateWithoutUrlState = ({ ...initialState, left: { urlState } } as unknown) as ExploreState;
const expectedState = { ...initialState };
reducerTester<ExploreState>()
.givenReducer(exploreReducer, stateWithoutUrlState)
.whenActionIsDispatched(
updateLocation({
query: {
left: serializedUrlState,
},
path: '/explore',
})
)
.thenStateShouldEqual(expectedState);
});
});
describe("but '/explore' is missing in path", () => {
it('then it should just add urlState and update in state', () => {
const { initialState, serializedUrlState } = setup();
const expectedState = { ...initialState };
reducerTester<ExploreState>()
.givenReducer(exploreReducer, initialState)
.whenActionIsDispatched(
updateLocation({
query: {
left: serializedUrlState,
},
path: '/dashboard',
})
)
.thenStateShouldEqual(expectedState);
});
});
describe("and '/explore' is in path", () => {
describe('and datasource differs', () => {
it('then it should return update datasource', () => {
const { initialState, serializedUrlState } = setup();
const expectedState = {
...initialState,
left: {
...initialState.left,
update: {
...initialState.left.update,
datasource: true,
},
},
};
const stateWithDifferentDataSource: any = {
...initialState,
left: {
...initialState.left,
urlState: {
...initialState.left.urlState,
datasource: 'different datasource',
},
},
};
reducerTester<ExploreState>()
.givenReducer(exploreReducer, stateWithDifferentDataSource)
.whenActionIsDispatched(
updateLocation({
query: {
left: serializedUrlState,
},
path: '/explore',
})
)
.thenStateShouldEqual(expectedState);
});
});
describe('and range differs', () => {
it('then it should return update range', () => {
const { initialState, serializedUrlState } = setup();
const expectedState = {
...initialState,
left: {
...initialState.left,
update: {
...initialState.left.update,
range: true,
},
},
};
const stateWithDifferentDataSource: any = {
...initialState,
left: {
...initialState.left,
urlState: {
...initialState.left.urlState,
range: {
from: 'now',
to: 'now-6h',
},
},
},
};
reducerTester<ExploreState>()
.givenReducer(exploreReducer, stateWithDifferentDataSource)
.whenActionIsDispatched(
updateLocation({
query: {
left: serializedUrlState,
},
path: '/explore',
})
)
.thenStateShouldEqual(expectedState);
});
});
describe('and queries differs', () => {
it('then it should return update queries', () => {
const { initialState, serializedUrlState } = setup();
const expectedState = {
...initialState,
left: {
...initialState.left,
update: {
...initialState.left.update,
queries: true,
},
},
};
const stateWithDifferentDataSource: any = {
...initialState,
left: {
...initialState.left,
urlState: {
...initialState.left.urlState,
queries: [{ expr: '{__filename__="some.log"}' }],
},
},
};
reducerTester<ExploreState>()
.givenReducer(exploreReducer, stateWithDifferentDataSource)
.whenActionIsDispatched(
updateLocation({
query: {
left: serializedUrlState,
},
path: '/explore',
})
)
.thenStateShouldEqual(expectedState);
});
});
describe('and nothing differs', () => {
it('then it should return update ui', () => {
const { initialState, serializedUrlState } = setup();
const expectedState = { ...initialState };
reducerTester<ExploreState>()
.givenReducer(exploreReducer, initialState)
.whenActionIsDispatched(
updateLocation({
query: {
left: serializedUrlState,
},
path: '/explore',
})
)
.thenStateShouldEqual(expectedState);
});
});
});
});
});
});
});
export const setup = (urlStateOverrides?: any) => {
const update = makeInitialUpdateState();
const urlStateDefaults: ExploreUrlState = {
datasource: 'some-datasource',
queries: [],
@ -448,8 +200,6 @@ export const setup = (urlStateOverrides?: any) => {
const serializedUrlState = serializeStateToUrlParam(urlState);
const initialState = ({
split: false,
left: { urlState, update },
right: { urlState, update },
} as unknown) as ExploreState;
return {

View File

@ -1,18 +1,14 @@
import _ from 'lodash';
import { AnyAction } from 'redux';
import { DataSourceSrv, LocationUpdate } from '@grafana/runtime';
import { DataSourceSrv, getDataSourceSrv } from '@grafana/runtime';
import { DataQuery, ExploreUrlState, serializeStateToUrlParam, TimeRange, UrlQueryMap } from '@grafana/data';
import { stopQueryState, parseUrlState, DEFAULT_RANGE, GetExploreUrlArguments } from 'app/core/utils/explore';
import { GetExploreUrlArguments, stopQueryState } from 'app/core/utils/explore';
import { ExploreId, ExploreItemState, ExploreState } from 'app/types/explore';
import { updateLocation } from '../../../core/actions';
import { paneReducer, stateSave } from './explorePane';
import { paneReducer } from './explorePane';
import { createAction } from '@reduxjs/toolkit';
import { makeExplorePaneState } from './utils';
import { DataQuery, TimeRange } from '@grafana/data';
import { getUrlStateFromPaneState, makeExplorePaneState } from './utils';
import { ThunkResult } from '../../../types';
import { getDatasourceSrv } from '../../plugins/datasource_srv';
import { changeDatasource } from './datasource';
import { runQueries, setQueriesAction } from './query';
import { TimeSrv } from '../../dashboard/services/TimeSrv';
import { PanelModel } from 'app/features/dashboard/state';
@ -20,24 +16,6 @@ import { PanelModel } from 'app/features/dashboard/state';
// Actions and Payloads
//
/**
* Close the split view and save URL state.
*/
export interface SplitCloseActionPayload {
itemId: ExploreId;
}
export const splitCloseAction = createAction<SplitCloseActionPayload>('explore/splitClose');
/**
* Open the split view and copy the left state to be the right state.
* The right state is automatically initialized.
* The copy keeps all query modifications but wipes the query results.
*/
export interface SplitOpenPayload {
itemState: ExploreItemState;
}
export const splitOpenAction = createAction<SplitOpenPayload>('explore/splitOpen');
export interface SyncTimesPayload {
syncedTimes: boolean;
}
@ -53,15 +31,56 @@ export interface ResetExplorePayload {
}
export const resetExploreAction = createAction<ResetExplorePayload>('explore/resetExplore');
/**
* Close the split view and save URL state.
*/
export interface SplitCloseActionPayload {
itemId: ExploreId;
}
export const splitCloseAction = createAction<SplitCloseActionPayload>('explore/splitClose');
/**
* Cleans up a pane state. Could seem like this should be in explorePane.ts actions but in case we area closing
* left pane we need to move right state to the left.
* Also this may seem redundant as we have splitClose actions which clears up state but that action is not called on
* URL change.
*/
export interface CleanupPanePayload {
exploreId: ExploreId;
}
export const cleanupPaneAction = createAction<CleanupPanePayload>('explore/cleanupPane');
//
// Action creators
//
/**
* Open the split view and the right state is automatically initialized.
* If options are specified it initializes that pane with the datasource and query from options.
* Otherwise it copies the left state to be the right state. The copy keeps all query modifications but wipes the query
* results.
* 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 = (options?: { replace?: boolean }): ThunkResult<void> => {
return (dispatch, getState) => {
const { left, right } = getState().explore;
const orgId = getState().user.orgId.toString();
const urlStates: { [index: string]: string } = { orgId };
urlStates.left = serializeStateToUrlParam(getUrlStateFromPaneState(left), true);
if (right) {
urlStates.right = serializeStateToUrlParam(getUrlStateFromPaneState(right), true);
}
lastSavedUrl.right = urlStates.right;
lastSavedUrl.left = urlStates.left;
dispatch(updateLocation({ query: urlStates, replace: options?.replace }));
};
};
// Store the url we saved last se we are not trying to update local state based on that.
export const lastSavedUrl: UrlQueryMap = {};
/**
* Opens a new right split pane by navigating to appropriate URL. It either copies existing state of the left pane
* or uses values from options arg. This does only navigation each pane is then responsible for initialization from
* the URL.
*/
export function splitOpen<T extends DataQuery = any>(options?: {
datasourceUid: string;
@ -70,70 +89,31 @@ export function splitOpen<T extends DataQuery = any>(options?: {
range?: TimeRange;
}): ThunkResult<void> {
return async (dispatch, getState) => {
// Clone left state to become the right state
const leftState: ExploreItemState = getState().explore[ExploreId.left];
const rightState: ExploreItemState = {
...leftState,
};
const queryState = getState().location.query[ExploreId.left] as string;
const urlState = parseUrlState(queryState);
const leftUrlState = getUrlStateFromPaneState(leftState);
let rightUrlState: ExploreUrlState = leftUrlState;
if (options) {
rightState.queries = [];
rightState.graphResult = null;
rightState.logsResult = null;
rightState.tableResult = null;
rightState.queryKeys = [];
urlState.queries = [];
rightState.urlState = urlState;
rightState.showLogs = false;
rightState.showMetrics = false;
rightState.showNodeGraph = false;
rightState.showTrace = false;
rightState.showTable = false;
if (options.range) {
urlState.range = options.range.raw;
// This is super hacky. In traces to logs we want to create a link but also internally open split window.
// We use the same range object but the raw part is treated differently because it's parsed differently during
// init depending on whether we open split or new window.
rightState.range = {
...options.range,
raw: {
from: options.range.from.utc().toISOString(),
to: options.range.to.utc().toISOString(),
},
};
}
dispatch(splitOpenAction({ itemState: rightState }));
const queries = [
{
...options.query,
refId: 'A',
} as DataQuery,
];
const dataSourceSettings = getDatasourceSrv().getInstanceSettings(options.datasourceUid);
await dispatch(changeDatasource(ExploreId.right, dataSourceSettings!.name));
await dispatch(setQueriesAction({ exploreId: ExploreId.right, queries }));
await dispatch(runQueries(ExploreId.right));
} else {
rightState.queries = leftState.queries.slice();
rightState.urlState = urlState;
dispatch(splitOpenAction({ itemState: rightState }));
const datasourceName = getDataSourceSrv().getInstanceSettings(options.datasourceUid)?.name || '';
rightUrlState = {
datasource: datasourceName,
queries: [options.query],
range: options.range || leftState.range,
};
}
dispatch(stateSave());
const urlState = serializeStateToUrlParam(rightUrlState, true);
dispatch(updateLocation({ query: { right: urlState }, partial: true }));
};
}
/**
* Close the split view and save URL state.
* Close the split view and save URL state. We need to update the state here because when closing we cannot just
* update the URL and let the components handle it because if we swap panes from right to left it is not easily apparent
* from the URL.
*/
export function splitClose(itemId: ExploreId): ThunkResult<void> {
return (dispatch) => {
return (dispatch, getState) => {
dispatch(splitCloseAction({ itemId }));
dispatch(stateSave());
};
@ -177,10 +157,9 @@ export const navigateToExplore = (
*/
const initialExploreItemState = makeExplorePaneState();
export const initialExploreState: ExploreState = {
split: false,
syncedTimes: false,
left: initialExploreItemState,
right: initialExploreItemState,
right: undefined,
richHistory: [],
};
@ -192,18 +171,29 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
if (splitCloseAction.match(action)) {
const { itemId } = action.payload as SplitCloseActionPayload;
const targetSplit = {
left: itemId === ExploreId.left ? state.right : state.left,
right: initialExploreState.right,
left: itemId === ExploreId.left ? state.right! : state.left,
right: undefined,
};
return {
...state,
...targetSplit,
split: false,
};
}
if (splitOpenAction.match(action)) {
return { ...state, split: true, right: { ...action.payload.itemState } };
if (cleanupPaneAction.match(action)) {
const { exploreId } = action.payload as CleanupPanePayload;
if (exploreId === ExploreId.left) {
return {
...state,
[ExploreId.left]: state[ExploreId.right]!,
[ExploreId.right]: undefined,
};
} else {
return {
...state,
[ExploreId.right]: undefined,
};
}
}
if (syncTimesAction.match(action)) {
@ -222,7 +212,9 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
const leftState = state[ExploreId.left];
const rightState = state[ExploreId.right];
stopQueryState(leftState.querySubscription);
stopQueryState(rightState.querySubscription);
if (rightState) {
stopQueryState(rightState.querySubscription);
}
if (payload.force || !Number.isInteger(state.left.originPanelId)) {
return initialExploreState;
@ -238,25 +230,6 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
};
}
if (updateLocation.match(action)) {
const payload: LocationUpdate = action.payload;
const { query } = payload;
if (!query || !query[ExploreId.left]) {
return state;
}
const split = query[ExploreId.right] ? true : false;
const leftState = state[ExploreId.left];
const rightState = state[ExploreId.right];
return {
...state,
split,
[ExploreId.left]: updatePaneRefreshState(leftState, payload, ExploreId.left),
[ExploreId.right]: updatePaneRefreshState(rightState, payload, ExploreId.right),
};
}
if (action.payload) {
const { exploreId } = action.payload;
if (exploreId !== undefined) {
@ -272,44 +245,3 @@ export const exploreReducer = (state = initialExploreState, action: AnyAction):
export default {
explore: exploreReducer,
};
export const updatePaneRefreshState = (
state: Readonly<ExploreItemState>,
payload: LocationUpdate,
exploreId: ExploreId
): ExploreItemState => {
const path = payload.path || '';
if (!payload.query) {
return state;
}
const queryState = payload.query[exploreId] as string;
if (!queryState) {
return state;
}
const urlState = parseUrlState(queryState);
if (!state.urlState || path !== '/explore') {
// we only want to refresh when browser back/forward
return {
...state,
urlState,
update: { datasource: false, queries: false, range: false, mode: false },
};
}
const datasource = _.isEqual(urlState ? urlState.datasource : '', state.urlState.datasource) === false;
const queries = _.isEqual(urlState ? urlState.queries : [], state.urlState.queries) === false;
const range = _.isEqual(urlState ? urlState.range : DEFAULT_RANGE, state.urlState.range) === false;
return {
...state,
urlState,
update: {
...state.update,
datasource,
queries,
range,
},
};
};

View File

@ -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,

View File

@ -1,5 +1,5 @@
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]);

View File

@ -1,6 +1,6 @@
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);

View File

@ -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(),
};
}

View File

@ -1,6 +1,7 @@
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),
};
}

View File

@ -5,7 +5,6 @@ import {
DataQuery,
DataQueryRequest,
DataSourceApi,
ExploreUrlState,
HistoryItem,
LogLevel,
LogsDedupStrategy,
@ -26,10 +25,6 @@ export enum ExploreId {
* Global Explore state
*/
export interface ExploreState {
/**
* True if split view is active.
*/
split: boolean;
/**
* True if time interval for panels are synced. Only possible with split mode.
*/
@ -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;