diff --git a/packages/grafana-data/src/utils/index.ts b/packages/grafana-data/src/utils/index.ts index 31aff6be711..71059dfc799 100644 --- a/packages/grafana-data/src/utils/index.ts +++ b/packages/grafana-data/src/utils/index.ts @@ -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'; diff --git a/public/app/features/explore/Explore.test.tsx b/public/app/features/explore/Explore.test.tsx index 3581c7c99d1..921127e870e 100644 --- a/public/app/features/explore/Explore.test.tsx +++ b/public/app/features/explore/Explore.test.tsx @@ -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(), diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 842c36c3c3e..fcbbea177b5 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -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 { - 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 { }); }; - refreshExplore = () => { - const { exploreId, update } = this.props; - - if (update.queries || update.range || update.datasource || update.mode) { - this.props.refreshExplore(exploreId); - } - }; - renderEmptyState() { return (
@@ -367,7 +305,6 @@ export class Explore extends React.PureComponent { datasourceInstance, datasourceMissing, exploreId, - split, queryKeys, graphResult, queryResponse, @@ -380,7 +317,6 @@ export class Explore extends React.PureComponent { 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 { const showQueryInspector = openDrawer === ExploreDrawer.QueryInspector; return ( -
+ <> {datasourceMissing ? this.renderEmptyState() : null} {datasourceInstance && (
- + {
)} -
+ ); } } -const ensureQueriesMemoized = memoizeOne(ensureQueries); -const getTimeRangeFromUrlMemoized = memoizeOne(getTimeRangeFromUrl); - function mapStateToProps(state: StoreState, { exploreId }: ExploreProps): Partial { 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 = { changeSize, - initializeExplore, modifyQueries, - refreshExplore, scanStart, scanStopAction, setQueries, diff --git a/public/app/features/explore/ExplorePaneContainer.tsx b/public/app/features/explore/ExplorePaneContainer.tsx new file mode 100644 index 00000000000..fe55a4cb3b8 --- /dev/null +++ b/public/app/features/explore/ExplorePaneContainer.tsx @@ -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; +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> { + 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 ( +
+ {this.props.initialized && } +
+ ); + } +} + +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); diff --git a/public/app/features/explore/ExploreQueryInspector.tsx b/public/app/features/explore/ExploreQueryInspector.tsx index ef797422e77..824f8221407 100644 --- a/public/app/features/explore/ExploreQueryInspector.tsx +++ b/public/app/features/explore/ExploreQueryInspector.tsx @@ -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 { diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 90d34da3da1..c57ffeda9f1 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -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 { )}
{splitted && ( - closeSplit(exploreId)} name="times" /> + closeSplit(exploreId)} + name="times" + /> )}
@@ -227,9 +233,8 @@ export class UnConnectedExploreToolbar extends PureComponent { } 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, diff --git a/public/app/features/explore/NodeGraphContainer.tsx b/public/app/features/explore/NodeGraphContainer.tsx index f121512cf62..3a06af9f699 100644 --- a/public/app/features/explore/NodeGraphContainer.tsx +++ b/public/app/features/explore/NodeGraphContainer.tsx @@ -36,7 +36,7 @@ export function UnconnectedNodeGraphContainer(props: Props & ConnectedProps { 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, }; } diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index 264131f44ef..1957263119a 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -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 { render() { - const { className = '', exploreEvents, exploreId, queryKeys } = this.props; + const { className = '', exploreId, queryKeys } = this.props; return (
{queryKeys.map((key, index) => { - return ; + return ; })}
); diff --git a/public/app/features/explore/ReturnToDashboardButton.tsx b/public/app/features/explore/ReturnToDashboardButton.tsx index c1c3a4f8f3e..3d21a8ceca2 100644 --- a/public/app/features/explore/ReturnToDashboardButton.tsx +++ b/public/app/features/explore/ReturnToDashboardButton.tsx @@ -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 = ({ 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, diff --git a/public/app/features/explore/RichHistory/RichHistoryCard.tsx b/public/app/features/explore/RichHistory/RichHistoryCard.tsx index f137f88dea8..ad4d080aee6 100644 --- a/public/app/features/explore/RichHistory/RichHistoryCard.tsx +++ b/public/app/features/explore/RichHistory/RichHistoryCard.tsx @@ -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, diff --git a/public/app/features/explore/Wrapper.test.tsx b/public/app/features/explore/Wrapper.test.tsx index 4c4af3e7c56..30e2e298cde 100644 --- a/public/app/features/explore/Wrapper.test.tsx +++ b/public/app/features/explore/Wrapper.test.tsx @@ -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({ 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 ); - 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 { diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index eac32ca2f2e..4925e04e8d0 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -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 { } componentDidMount() { + lastSavedUrl.left = undefined; + lastSavedUrl.right = undefined; + const richHistory = getRichHistory(); this.props.richHistoryUpdatedAction({ richHistory }); } @@ -34,11 +37,11 @@ export class Wrapper extends Component {
- + {split && ( - + )}
@@ -49,8 +52,11 @@ export class Wrapper extends Component { } 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 = { diff --git a/public/app/features/explore/__snapshots__/Explore.test.tsx.snap b/public/app/features/explore/__snapshots__/Explore.test.tsx.snap index 501de41bab2..5898b8cd187 100644 --- a/public/app/features/explore/__snapshots__/Explore.test.tsx.snap +++ b/public/app/features/explore/__snapshots__/Explore.test.tsx.snap @@ -1,10 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Explore should render component 1`] = ` -
+ @@ -47,5 +36,5 @@ exports[`Explore should render component 1`] = `
- + `; diff --git a/public/app/features/explore/state/datasource.ts b/public/app/features/explore/state/datasource.ts index f48024768ba..e6256e8d100 100644 --- a/public/app/features/explore/state/datasource.ts +++ b/public/app/features/explore/state/datasource.ts @@ -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(), }; } diff --git a/public/app/features/explore/state/explorePane.test.ts b/public/app/features/explore/state/explorePane.test.ts index 737ae53ea69..6b2d28e7f8d 100644 --- a/public/app/features/explore/state/explorePane.test.ts +++ b/public/app/features/explore/state/explorePane.test.ts @@ -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) => { - 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 = { + 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; - - 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() - .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()); }); }); diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts index 44f4eff737c..b82abd4b8df 100644 --- a/public/app/features/explore/state/explorePane.ts +++ b/public/app/features/explore/state/explorePane.ts @@ -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 => { - 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 { - return (dispatch, getState) => { - const itemState = getState().explore[exploreId]; +export function refreshExplore(exploreId: ExploreId, newUrlQuery: string): ThunkResult { + 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 { 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 { 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, }; -} +}; diff --git a/public/app/features/explore/state/main.test.ts b/public/app/features/explore/state/main.test.ts index 85f4d2dab72..e3ff1b1c1e6 100644 --- a/public/app/features/explore/state/main.test.ts +++ b/public/app/features/explore/state/main.test.ts @@ -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() - .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() - .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() - .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() - .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() - .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() - .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() - .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() - .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() - .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() - .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 { diff --git a/public/app/features/explore/state/main.ts b/public/app/features/explore/state/main.ts index 211ac019e31..d8c84324329 100644 --- a/public/app/features/explore/state/main.ts +++ b/public/app/features/explore/state/main.ts @@ -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('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('explore/splitOpen'); - export interface SyncTimesPayload { syncedTimes: boolean; } @@ -53,15 +31,56 @@ export interface ResetExplorePayload { } export const resetExploreAction = createAction('explore/resetExplore'); +/** + * Close the split view and save URL state. + */ +export interface SplitCloseActionPayload { + itemId: ExploreId; +} +export const splitCloseAction = createAction('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('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 => { + 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(options?: { datasourceUid: string; @@ -70,70 +89,31 @@ export function splitOpen(options?: { range?: TimeRange; }): ThunkResult { 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 { - 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, - 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, - }, - }; -}; diff --git a/public/app/features/explore/state/query.ts b/public/app/features/explore/state/query.ts index dc7f37123f8..fdf85d83fa3 100644 --- a/public/app/features/explore/state/query.ts +++ b/public/app/features/explore/state/query.ts @@ -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('explore/scanStop'); */ export function addQueryRow(exploreId: ExploreId, index: number): ThunkResult { 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 => { +export const runQueries = (exploreId: ExploreId, options?: { replaceUrl?: boolean }): ThunkResult => { 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 => { 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 => { 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 => { 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 => { export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): ThunkResult { 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 { // 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, diff --git a/public/app/features/explore/state/selectors.ts b/public/app/features/explore/state/selectors.ts index 32e097b4f62..c1e8c0010ee 100644 --- a/public/app/features/explore/state/selectors.ts +++ b/public/app/features/explore/state/selectors.ts @@ -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]); diff --git a/public/app/features/explore/state/time.test.ts b/public/app/features/explore/state/time.test.ts index 4d64875d161..93aef3f4db5 100644 --- a/public/app/features/explore/state/time.test.ts +++ b/public/app/features/explore/state/time.test.ts @@ -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() .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); diff --git a/public/app/features/explore/state/time.ts b/public/app/features/explore/state/time.ts index 000a103630b..e3217008d90 100644 --- a/public/app/features/explore/state/time.ts +++ b/public/app/features/explore/state/time.ts @@ -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 => { 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 { 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(), }; } diff --git a/public/app/features/explore/state/utils.ts b/public/app/features/explore/state/utils.ts index e5a924db708..c25d49199a3 100644 --- a/public/app/features/explore/state/utils.ts +++ b/public/app/features/explore/state/utils.ts @@ -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), + }; +} diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index c2d9c2f35ec..3b3c83f3a51 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -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;