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