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