diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 871a020ccc2..026d1ba324c 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -206,11 +206,14 @@ export function ensureQueries(queries?: DataQuery[]): DataQuery[] { * A target is non-empty when it has keys (with non-empty values) other than refId and key. */ export function hasNonEmptyQuery(queries: DataQuery[]): boolean { - return queries.some( - query => - Object.keys(query) - .map(k => query[k]) - .filter(v => v).length > 2 + return ( + queries && + queries.some( + query => + Object.keys(query) + .map(k => query[k]) + .filter(v => v).length > 2 + ) ); } diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 64e9c66ece5..a3177bebe79 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -2,14 +2,15 @@ import React from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import _ from 'lodash'; -import { withSize } from 'react-sizeme'; +import { AutoSizer } from 'react-virtualized'; import { RawTimeRange, TimeRange } from '@grafana/ui'; import { DataSourceSelectItem } from 'app/types/datasources'; -import { ExploreUrlState, HistoryItem, QueryTransaction, RangeScanner } from 'app/types/explore'; +import { ExploreUrlState, HistoryItem, QueryTransaction, RangeScanner, ExploreId } from 'app/types/explore'; import { DataQuery } from 'app/types/series'; +import { StoreState } from 'app/types'; import store from 'app/core/store'; -import { LAST_USED_DATASOURCE_KEY, ensureQueries } from 'app/core/utils/explore'; +import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { Emitter } from 'app/core/utils/emitter'; @@ -20,9 +21,11 @@ import { changeSize, changeTime, clickClear, + clickCloseSplit, clickExample, clickGraphButton, clickLogsButton, + clickSplit, clickTableButton, highlightLogsExpression, initializeExplore, @@ -32,7 +35,7 @@ import { scanStart, scanStop, } from './state/actions'; -import { ExploreState } from './state/reducers'; +import { ExploreItemState } from './state/reducers'; import Panel from './Panel'; import QueryRows from './QueryRows'; @@ -50,17 +53,21 @@ interface ExploreProps { addQueryRow: typeof addQueryRow; changeDatasource: typeof changeDatasource; changeQuery: typeof changeQuery; + changeSize: typeof changeSize; changeTime: typeof changeTime; clickClear: typeof clickClear; + clickCloseSplit: typeof clickCloseSplit; clickExample: typeof clickExample; clickGraphButton: typeof clickGraphButton; clickLogsButton: typeof clickLogsButton; + clickSplit: typeof clickSplit; clickTableButton: typeof clickTableButton; datasourceError: string; datasourceInstance: any; datasourceLoading: boolean | null; datasourceMissing: boolean; exploreDatasources: DataSourceSelectItem[]; + exploreId: ExploreId; graphResult?: any[]; highlightLogsExpression: typeof highlightLogsExpression; history: HistoryItem[]; @@ -70,9 +77,6 @@ interface ExploreProps { logsHighlighterExpressions?: string[]; logsResult?: LogsModel; modifyQueries: typeof modifyQueries; - onChangeSplit: (split: boolean, state?: ExploreState) => void; - onSaveState: (key: string, state: ExploreState) => void; - position: string; queryTransactions: QueryTransaction[]; removeQueryRow: typeof removeQueryRow; range: RawTimeRange; @@ -83,8 +87,6 @@ interface ExploreProps { scanStart: typeof scanStart; scanStop: typeof scanStop; split: boolean; - splitState?: ExploreState; - stateKey: string; showingGraph: boolean; showingLogs: boolean; showingStartPage?: boolean; @@ -132,7 +134,7 @@ interface ExploreProps { * The result viewers determine some of the query options sent to the datasource, e.g., * `format`, to indicate eventual transformations by the datasources' result transformers. */ -export class Explore extends React.PureComponent { +export class Explore extends React.PureComponent { el: any; exploreEvents: Emitter; /** @@ -147,13 +149,23 @@ export class Explore extends React.PureComponent { } async componentDidMount() { - // Load URL state and parse range - const { datasource, queries, range } = this.props.urlState as ExploreUrlState; - const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY); - const initialQueries: DataQuery[] = ensureQueries(queries); - const initialRange = { from: parseTime(range.from), to: parseTime(range.to) }; - const width = this.el ? this.el.offsetWidth : 0; - this.props.initializeExplore(initialDatasource, initialQueries, initialRange, width, this.exploreEvents); + const { exploreId, split, urlState } = this.props; + if (!split) { + // Load URL state and parse range + const { datasource, queries, range = DEFAULT_RANGE } = (urlState || {}) as ExploreUrlState; + const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY); + const initialQueries: DataQuery[] = ensureQueries(queries); + const initialRange = { from: parseTime(range.from), to: parseTime(range.to) }; + const width = this.el ? this.el.offsetWidth : 0; + this.props.initializeExplore( + exploreId, + initialDatasource, + initialQueries, + initialRange, + width, + this.exploreEvents + ); + } } componentWillUnmount() { @@ -165,17 +177,17 @@ export class Explore extends React.PureComponent { }; onAddQueryRow = index => { - this.props.addQueryRow(index); + this.props.addQueryRow(this.props.exploreId, index); }; onChangeDatasource = async option => { - this.props.changeDatasource(option.value); + this.props.changeDatasource(this.props.exploreId, option.value); }; onChangeQuery = (query: DataQuery, index: number, override?: boolean) => { - const { changeQuery, datasourceInstance } = this.props; + const { changeQuery, datasourceInstance, exploreId } = this.props; - changeQuery(query, index, override); + changeQuery(exploreId, query, index, override); if (query && !override && datasourceInstance.getHighlighterExpression && index === 0) { // Live preview of log search matches. Only use on first row for now this.updateLogsHighlights(query); @@ -186,43 +198,36 @@ export class Explore extends React.PureComponent { if (this.props.scanning && !changedByScanner) { this.onStopScanning(); } - this.props.changeTime(range); + this.props.changeTime(this.props.exploreId, range); }; onClickClear = () => { - this.props.clickClear(); + this.props.clickClear(this.props.exploreId); }; onClickCloseSplit = () => { - const { onChangeSplit } = this.props; - if (onChangeSplit) { - onChangeSplit(false); - } + this.props.clickCloseSplit(); }; onClickGraphButton = () => { - this.props.clickGraphButton(); + this.props.clickGraphButton(this.props.exploreId); }; onClickLogsButton = () => { - this.props.clickLogsButton(); + this.props.clickLogsButton(this.props.exploreId); }; // Use this in help pages to set page to a single query onClickExample = (query: DataQuery) => { - this.props.clickExample(query); + this.props.clickExample(this.props.exploreId, query); }; onClickSplit = () => { - const { onChangeSplit } = this.props; - if (onChangeSplit) { - // const state = this.cloneState(); - // onChangeSplit(true, state); - } + this.props.clickSplit(); }; onClickTableButton = () => { - this.props.clickTableButton(); + this.props.clickTableButton(this.props.exploreId); }; onClickLabel = (key: string, value: string) => { @@ -233,18 +238,22 @@ export class Explore extends React.PureComponent { const { datasourceInstance } = this.props; if (datasourceInstance && datasourceInstance.modifyQuery) { const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action); - this.props.modifyQueries(action, index, modifier); + this.props.modifyQueries(this.props.exploreId, action, index, modifier); } }; onRemoveQueryRow = index => { - this.props.removeQueryRow(index); + this.props.removeQueryRow(this.props.exploreId, index); + }; + + onResize = (size: { height: number; width: number }) => { + this.props.changeSize(this.props.exploreId, size); }; onStartScanning = () => { // Scanner will trigger a query const scanner = this.scanPreviousRange; - this.props.scanStart(scanner); + this.props.scanStart(this.props.exploreId, scanner); }; scanPreviousRange = (): RawTimeRange => { @@ -253,30 +262,21 @@ export class Explore extends React.PureComponent { }; onStopScanning = () => { - this.props.scanStop(); + this.props.scanStop(this.props.exploreId); }; onSubmit = () => { - this.props.runQueries(); + this.props.runQueries(this.props.exploreId); }; updateLogsHighlights = _.debounce((value: DataQuery) => { const { datasourceInstance } = this.props; if (datasourceInstance.getHighlighterExpression) { const expressions = [datasourceInstance.getHighlighterExpression(value)]; - this.props.highlightLogsExpression(expressions); + this.props.highlightLogsExpression(this.props.exploreId, expressions); } }, 500); - // cloneState(): ExploreState { - // // Copy state, but copy queries including modifications - // return { - // ...this.state, - // queryTransactions: [], - // initialQueries: [...this.modifiedQueries], - // }; - // } - // saveState = () => { // const { stateKey, onSaveState } = this.props; // onSaveState(stateKey, this.cloneState()); @@ -290,13 +290,13 @@ export class Explore extends React.PureComponent { datasourceLoading, datasourceMissing, exploreDatasources, + exploreId, graphResult, history, initialQueries, logsHighlighterExpressions, logsResult, queryTransactions, - position, range, scanning, scanRange, @@ -323,7 +323,7 @@ export class Explore extends React.PureComponent { return (
- {position === 'left' ? ( + {exploreId === 'left' ? ( ) : null}
- {position === 'left' && !split ? ( + {exploreId === 'left' && !split ? (
)} - {datasourceInstance && !datasourceError ? ( -
- -
- - {showingStartPage && } - {!showingStartPage && ( - <> - {supportsGraph && ( - - - - )} - {supportsTable && ( - - - - )} - {supportsLogs && ( - - - - )} - + {datasourceInstance && + !datasourceError && ( +
+ + + {({ width }) => ( +
+ + {showingStartPage && } + {!showingStartPage && ( + <> + {supportsGraph && ( + + + + )} + {supportsTable && ( + +
+ + )} + {supportsLogs && ( + + + + )} + + )} + + )} - - - - ) : null} + + + )} ); } } -function mapStateToProps({ explore }) { +function mapStateToProps(state: StoreState, { exploreId }) { + const explore = state.explore; + const { split } = explore; + const item: ExploreItemState = explore[exploreId]; const { StartPage, datasourceError, @@ -480,7 +493,7 @@ function mapStateToProps({ explore }) { supportsLogs, supportsTable, tableResult, - } = explore as ExploreState; + } = item; return { StartPage, datasourceError, @@ -502,6 +515,7 @@ function mapStateToProps({ explore }) { showingLogs, showingStartPage, showingTable, + split, supportsGraph, supportsLogs, supportsTable, @@ -513,20 +527,22 @@ const mapDispatchToProps = { addQueryRow, changeDatasource, changeQuery, + changeSize, changeTime, clickClear, + clickCloseSplit, clickExample, clickGraphButton, clickLogsButton, + clickSplit, clickTableButton, highlightLogsExpression, initializeExplore, modifyQueries, - onSize: changeSize, // used by withSize HOC removeQueryRow, runQueries, scanStart, scanStop, }; -export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(withSize()(Explore))); +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Explore)); diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 1a384cf011d..d07b31e2ff1 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -241,9 +241,9 @@ function renderMetaItem(value: any, kind: LogsMetaKind) { interface LogsProps { data: LogsModel; + exploreId: string; highlighterExpressions: string[]; loading: boolean; - position: string; range?: RawTimeRange; scanning?: boolean; scanRange?: RawTimeRange; @@ -348,10 +348,10 @@ export default class Logs extends PureComponent { render() { const { data, + exploreId, highlighterExpressions, loading = false, onClickLabel, - position, range, scanning, scanRange, @@ -400,7 +400,7 @@ export default class Logs extends PureComponent { data={data.series} height="100px" range={range} - id={`explore-logs-graph-${position}`} + id={`explore-logs-graph-${exploreId}`} onChangeTime={this.props.onChangeTime} onToggleSeries={this.onToggleLogLevel} userOptions={graphOptions} diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index de1eee4c662..04f189749bf 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -3,9 +3,9 @@ import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import { updateLocation } from 'app/core/actions'; -import { serializeStateToUrlParam, parseUrlState } from 'app/core/utils/explore'; +// import { serializeStateToUrlParam, parseUrlState } from 'app/core/utils/explore'; import { StoreState } from 'app/types'; -import { ExploreState } from 'app/types/explore'; +import { ExploreId } from 'app/types/explore'; import ErrorBoundary from './ErrorBoundary'; import Explore from './Explore'; @@ -13,81 +13,41 @@ import Explore from './Explore'; interface WrapperProps { backendSrv?: any; datasourceSrv?: any; - updateLocation: typeof updateLocation; - urlStates: { [key: string]: string }; -} - -interface WrapperState { split: boolean; - splitState: ExploreState; + updateLocation: typeof updateLocation; + // urlStates: { [key: string]: string }; } -const STATE_KEY_LEFT = 'state'; -const STATE_KEY_RIGHT = 'stateRight'; - -export class Wrapper extends Component { - urlStates: { [key: string]: string }; +export class Wrapper extends Component { + // urlStates: { [key: string]: string }; constructor(props: WrapperProps) { super(props); - this.urlStates = props.urlStates; - this.state = { - split: Boolean(props.urlStates[STATE_KEY_RIGHT]), - splitState: undefined, - }; + // this.urlStates = props.urlStates; } - onChangeSplit = (split: boolean, splitState: ExploreState) => { - this.setState({ split, splitState }); - // When closing split, remove URL state for split part - if (!split) { - delete this.urlStates[STATE_KEY_RIGHT]; - this.props.updateLocation({ - query: this.urlStates, - }); - } - }; - - onSaveState = (key: string, state: ExploreState) => { - const urlState = serializeStateToUrlParam(state, true); - this.urlStates[key] = urlState; - this.props.updateLocation({ - query: this.urlStates, - }); - }; + // onSaveState = (key: string, state: ExploreState) => { + // const urlState = serializeStateToUrlParam(state, true); + // this.urlStates[key] = urlState; + // this.props.updateLocation({ + // query: this.urlStates, + // }); + // }; render() { - const { datasourceSrv } = this.props; + const { split } = this.props; // State overrides for props from first Explore - const { split, splitState } = this.state; - const urlStateLeft = parseUrlState(this.urlStates[STATE_KEY_LEFT]); - const urlStateRight = parseUrlState(this.urlStates[STATE_KEY_RIGHT]); + // const urlStateLeft = parseUrlState(this.urlStates[STATE_KEY_LEFT]); + // const urlStateRight = parseUrlState(this.urlStates[STATE_KEY_RIGHT]); return (
- + {split && ( - + )}
@@ -95,9 +55,11 @@ export class Wrapper extends Component { } } -const mapStateToProps = (state: StoreState) => ({ - urlStates: state.location.query, -}); +const mapStateToProps = (state: StoreState) => { + // urlStates: state.location.query, + const { split } = state.explore; + return { split }; +}; const mapDispatchToProps = { updateLocation, diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index d70a458059e..145b7506e79 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -17,6 +17,7 @@ import { DataSourceSelectItem } from 'app/types/datasources'; import { DataQuery, StoreState } from 'app/types'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { + ExploreId, HistoryItem, RangeScanner, ResultType, @@ -26,7 +27,7 @@ import { QueryHintGetter, } from 'app/types/explore'; import { Emitter } from 'app/core/core'; -import { dispatch } from 'rxjs/internal/observable/pairs'; +import { ExploreItemState } from './reducers'; export enum ActionTypes { AddQueryRow = 'ADD_QUERY_ROW', @@ -35,9 +36,11 @@ export enum ActionTypes { ChangeSize = 'CHANGE_SIZE', ChangeTime = 'CHANGE_TIME', ClickClear = 'CLICK_CLEAR', + ClickCloseSplit = 'CLICK_CLOSE_SPLIT', ClickExample = 'CLICK_EXAMPLE', ClickGraphButton = 'CLICK_GRAPH_BUTTON', ClickLogsButton = 'CLICK_LOGS_BUTTON', + ClickSplit = 'CLICK_SPLIT', ClickTableButton = 'CLICK_TABLE_BUTTON', HighlightLogsExpression = 'HIGHLIGHT_LOGS_EXPRESSION', InitializeExplore = 'INITIALIZE_EXPLORE', @@ -59,12 +62,14 @@ export enum ActionTypes { export interface AddQueryRowAction { type: ActionTypes.AddQueryRow; + exploreId: ExploreId; index: number; query: DataQuery; } export interface ChangeQueryAction { type: ActionTypes.ChangeQuery; + exploreId: ExploreId; query: DataQuery; index: number; override: boolean; @@ -72,38 +77,55 @@ export interface ChangeQueryAction { export interface ChangeSizeAction { type: ActionTypes.ChangeSize; + exploreId: ExploreId; width: number; height: number; } export interface ChangeTimeAction { type: ActionTypes.ChangeTime; + exploreId: ExploreId; range: TimeRange; } export interface ClickClearAction { type: ActionTypes.ClickClear; + exploreId: ExploreId; +} + +export interface ClickCloseSplitAction { + type: ActionTypes.ClickCloseSplit; } export interface ClickExampleAction { type: ActionTypes.ClickExample; + exploreId: ExploreId; query: DataQuery; } export interface ClickGraphButtonAction { type: ActionTypes.ClickGraphButton; + exploreId: ExploreId; } export interface ClickLogsButtonAction { type: ActionTypes.ClickLogsButton; + exploreId: ExploreId; +} + +export interface ClickSplitAction { + type: ActionTypes.ClickSplit; + itemState: ExploreItemState; } export interface ClickTableButtonAction { type: ActionTypes.ClickTableButton; + exploreId: ExploreId; } export interface InitializeExploreAction { type: ActionTypes.InitializeExplore; + exploreId: ExploreId; containerWidth: number; datasource: string; eventBridge: Emitter; @@ -114,25 +136,30 @@ export interface InitializeExploreAction { export interface HighlightLogsExpressionAction { type: ActionTypes.HighlightLogsExpression; + exploreId: ExploreId; expressions: string[]; } export interface LoadDatasourceFailureAction { type: ActionTypes.LoadDatasourceFailure; + exploreId: ExploreId; error: string; } export interface LoadDatasourcePendingAction { type: ActionTypes.LoadDatasourcePending; + exploreId: ExploreId; datasourceId: number; } export interface LoadDatasourceMissingAction { type: ActionTypes.LoadDatasourceMissing; + exploreId: ExploreId; } export interface LoadDatasourceSuccessAction { type: ActionTypes.LoadDatasourceSuccess; + exploreId: ExploreId; StartPage?: any; datasourceInstance: any; history: HistoryItem[]; @@ -147,6 +174,7 @@ export interface LoadDatasourceSuccessAction { export interface ModifyQueriesAction { type: ActionTypes.ModifyQueries; + exploreId: ExploreId; modification: any; index: number; modifier: (queries: DataQuery[], modification: any) => DataQuery[]; @@ -154,11 +182,13 @@ export interface ModifyQueriesAction { export interface QueryTransactionFailureAction { type: ActionTypes.QueryTransactionFailure; + exploreId: ExploreId; queryTransactions: QueryTransaction[]; } export interface QueryTransactionStartAction { type: ActionTypes.QueryTransactionStart; + exploreId: ExploreId; resultType: ResultType; rowIndex: number; transaction: QueryTransaction; @@ -166,27 +196,32 @@ export interface QueryTransactionStartAction { export interface QueryTransactionSuccessAction { type: ActionTypes.QueryTransactionSuccess; + exploreId: ExploreId; history: HistoryItem[]; queryTransactions: QueryTransaction[]; } export interface RemoveQueryRowAction { type: ActionTypes.RemoveQueryRow; + exploreId: ExploreId; index: number; } export interface ScanStartAction { type: ActionTypes.ScanStart; + exploreId: ExploreId; scanner: RangeScanner; } export interface ScanRangeAction { type: ActionTypes.ScanRange; + exploreId: ExploreId; range: RawTimeRange; } export interface ScanStopAction { type: ActionTypes.ScanStop; + exploreId: ExploreId; } export type Action = @@ -195,9 +230,11 @@ export type Action = | ChangeSizeAction | ChangeTimeAction | ClickClearAction + | ClickCloseSplitAction | ClickExampleAction | ClickGraphButtonAction | ClickLogsButtonAction + | ClickSplitAction | ClickTableButtonAction | HighlightLogsExpressionAction | InitializeExploreAction @@ -215,94 +252,126 @@ export type Action = | ScanStopAction; type ThunkResult = ThunkAction; -export function addQueryRow(index: number): AddQueryRowAction { +export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction { const query = generateEmptyQuery(index + 1); - return { type: ActionTypes.AddQueryRow, index, query }; + return { type: ActionTypes.AddQueryRow, exploreId, index, query }; } -export function changeDatasource(datasource: string): ThunkResult { +export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult { return async dispatch => { const instance = await getDatasourceSrv().get(datasource); - dispatch(loadDatasource(instance)); + dispatch(loadDatasource(exploreId, instance)); }; } -export function changeQuery(query: DataQuery, index: number, override: boolean): ThunkResult { +export function changeQuery( + exploreId: ExploreId, + query: DataQuery, + index: number, + override: boolean +): ThunkResult { return dispatch => { // Null query means reset if (query === null) { query = { ...generateEmptyQuery(index) }; } - dispatch({ type: ActionTypes.ChangeQuery, query, index, override }); + dispatch({ type: ActionTypes.ChangeQuery, exploreId, query, index, override }); if (override) { - dispatch(runQueries()); + dispatch(runQueries(exploreId)); } }; } -export function changeSize({ height, width }: { height: number; width: number }): ChangeSizeAction { - return { type: ActionTypes.ChangeSize, height, width }; +export function changeSize( + exploreId: ExploreId, + { height, width }: { height: number; width: number } +): ChangeSizeAction { + return { type: ActionTypes.ChangeSize, exploreId, height, width }; } -export function changeTime(range: TimeRange): ThunkResult { +export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.ChangeTime, range }); - dispatch(runQueries()); + dispatch({ type: ActionTypes.ChangeTime, exploreId, range }); + dispatch(runQueries(exploreId)); }; } -export function clickExample(rawQuery: DataQuery): ThunkResult { +export function clickClear(exploreId: ExploreId): ThunkResult { return dispatch => { - const query = { ...rawQuery, ...generateEmptyQuery() }; - dispatch({ - type: ActionTypes.ClickExample, - query, - }); - dispatch(runQueries()); - }; -} - -export function clickClear(): ThunkResult { - return dispatch => { - dispatch(scanStop()); - dispatch({ type: ActionTypes.ClickClear }); + dispatch(scanStop(exploreId)); + dispatch({ type: ActionTypes.ClickClear, exploreId }); // TODO save state }; } -export function clickGraphButton(): ThunkResult { +export function clickCloseSplit(): ThunkResult { + return dispatch => { + dispatch({ type: ActionTypes.ClickCloseSplit }); + // When closing split, remove URL state for split part + // TODO save state + }; +} + +export function clickExample(exploreId: ExploreId, rawQuery: DataQuery): ThunkResult { + return dispatch => { + const query = { ...rawQuery, ...generateEmptyQuery() }; + dispatch({ + type: ActionTypes.ClickExample, + exploreId, + query, + }); + dispatch(runQueries(exploreId)); + }; +} + +export function clickGraphButton(exploreId: ExploreId): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ClickGraphButton }); - if (getState().explore.showingGraph) { - dispatch(runQueries()); + dispatch({ type: ActionTypes.ClickGraphButton, exploreId }); + if (getState().explore[exploreId].showingGraph) { + dispatch(runQueries(exploreId)); } }; } -export function clickLogsButton(): ThunkResult { +export function clickLogsButton(exploreId: ExploreId): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ClickLogsButton }); - if (getState().explore.showingLogs) { - dispatch(runQueries()); + dispatch({ type: ActionTypes.ClickLogsButton, exploreId }); + if (getState().explore[exploreId].showingLogs) { + dispatch(runQueries(exploreId)); } }; } -export function clickTableButton(): ThunkResult { +export function clickSplit(): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ClickTableButton }); - if (getState().explore.showingTable) { - dispatch(runQueries()); + // Clone left state to become the right state + const leftState = getState().explore.left; + const itemState = { + ...leftState, + queryTransactions: [], + initialQueries: leftState.modifiedQueries.slice(), + }; + dispatch({ type: ActionTypes.ClickSplit, itemState }); + // TODO save state + }; +} + +export function clickTableButton(exploreId: ExploreId): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: ActionTypes.ClickTableButton, exploreId }); + if (getState().explore[exploreId].showingTable) { + dispatch(runQueries(exploreId)); } }; } -export function highlightLogsExpression(expressions: string[]): HighlightLogsExpressionAction { - return { type: ActionTypes.HighlightLogsExpression, expressions }; +export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction { + return { type: ActionTypes.HighlightLogsExpression, exploreId, expressions }; } export function initializeExplore( + exploreId: ExploreId, datasource: string, queries: DataQuery[], range: RawTimeRange, @@ -320,6 +389,7 @@ export function initializeExplore( dispatch({ type: ActionTypes.InitializeExplore, + exploreId, containerWidth, datasource, eventBridge, @@ -335,26 +405,35 @@ export function initializeExplore( } else { instance = await getDatasourceSrv().get(); } - dispatch(loadDatasource(instance)); + dispatch(loadDatasource(exploreId, instance)); } else { - dispatch(loadDatasourceMissing); + dispatch(loadDatasourceMissing(exploreId)); } }; } -export const loadDatasourceFailure = (error: string): LoadDatasourceFailureAction => ({ +export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({ type: ActionTypes.LoadDatasourceFailure, + exploreId, error, }); -export const loadDatasourceMissing: LoadDatasourceMissingAction = { type: ActionTypes.LoadDatasourceMissing }; +export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({ + type: ActionTypes.LoadDatasourceMissing, + exploreId, +}); -export const loadDatasourcePending = (datasourceId: number): LoadDatasourcePendingAction => ({ +export const loadDatasourcePending = (exploreId: ExploreId, datasourceId: number): LoadDatasourcePendingAction => ({ type: ActionTypes.LoadDatasourcePending, + exploreId, datasourceId, }); -export const loadDatasourceSuccess = (instance: any, queries: DataQuery[]): LoadDatasourceSuccessAction => { +export const loadDatasourceSuccess = ( + exploreId: ExploreId, + instance: any, + queries: DataQuery[] +): LoadDatasourceSuccessAction => { // Capabilities const supportsGraph = instance.meta.metrics; const supportsLogs = instance.meta.logs; @@ -369,6 +448,7 @@ export const loadDatasourceSuccess = (instance: any, queries: DataQuery[]): Load return { type: ActionTypes.LoadDatasourceSuccess, + exploreId, StartPage, datasourceInstance: instance, history, @@ -381,12 +461,12 @@ export const loadDatasourceSuccess = (instance: any, queries: DataQuery[]): Load }; }; -export function loadDatasource(instance: any): ThunkResult { +export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult { return async (dispatch, getState) => { const datasourceId = instance.meta.id; // Keep ID to track selection - dispatch(loadDatasourcePending(datasourceId)); + dispatch(loadDatasourcePending(exploreId, datasourceId)); let datasourceError = null; try { @@ -396,11 +476,11 @@ export function loadDatasource(instance: any): ThunkResult { datasourceError = (error && error.statusText) || 'Network error'; } if (datasourceError) { - dispatch(loadDatasourceFailure(datasourceError)); + dispatch(loadDatasourceFailure(exploreId, datasourceError)); return; } - if (datasourceId !== getState().explore.requestedDatasourceId) { + if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) { // User already changed datasource again, discard results return; } @@ -410,9 +490,9 @@ export function loadDatasource(instance: any): ThunkResult { } // Check if queries can be imported from previously selected datasource - const queries = getState().explore.modifiedQueries; + const queries = getState().explore[exploreId].modifiedQueries; let importedQueries = queries; - const origin = getState().explore.datasourceInstance; + const origin = getState().explore[exploreId].datasourceInstance; if (origin) { if (origin.meta.id === instance.meta.id) { // Keep same queries if same type of datasource @@ -426,7 +506,7 @@ export function loadDatasource(instance: any): ThunkResult { } } - if (datasourceId !== getState().explore.requestedDatasourceId) { + if (datasourceId !== getState().explore[exploreId].requestedDatasourceId) { // User already changed datasource again, discard results return; } @@ -437,23 +517,33 @@ export function loadDatasource(instance: any): ThunkResult { ...generateEmptyQuery(i), })); - dispatch(loadDatasourceSuccess(instance, nextQueries)); - dispatch(runQueries()); + dispatch(loadDatasourceSuccess(exploreId, instance, nextQueries)); + dispatch(runQueries(exploreId)); }; } -export function modifyQueries(modification: any, index: number, modifier: any): ThunkResult { +export function modifyQueries( + exploreId: ExploreId, + modification: any, + index: number, + modifier: any +): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.ModifyQueries, modification, index, modifier }); + dispatch({ type: ActionTypes.ModifyQueries, exploreId, modification, index, modifier }); if (!modification.preventSubmit) { - dispatch(runQueries()); + dispatch(runQueries(exploreId)); } }; } -export function queryTransactionFailure(transactionId: string, response: any, datasourceId: string): ThunkResult { +export function queryTransactionFailure( + exploreId: ExploreId, + transactionId: string, + response: any, + datasourceId: string +): ThunkResult { return (dispatch, getState) => { - const { datasourceInstance, queryTransactions } = getState().explore; + const { datasourceInstance, queryTransactions } = getState().explore[exploreId]; if (datasourceInstance.meta.id !== datasourceId || response.cancelled) { // Navigated away, queries did not matter return; @@ -500,19 +590,21 @@ export function queryTransactionFailure(transactionId: string, response: any, da return qt; }); - dispatch({ type: ActionTypes.QueryTransactionFailure, queryTransactions: nextQueryTransactions }); + dispatch({ type: ActionTypes.QueryTransactionFailure, exploreId, queryTransactions: nextQueryTransactions }); }; } export function queryTransactionStart( + exploreId: ExploreId, transaction: QueryTransaction, resultType: ResultType, rowIndex: number ): QueryTransactionStartAction { - return { type: ActionTypes.QueryTransactionStart, resultType, rowIndex, transaction }; + return { type: ActionTypes.QueryTransactionStart, exploreId, resultType, rowIndex, transaction }; } export function queryTransactionSuccess( + exploreId: ExploreId, transactionId: string, result: any, latency: number, @@ -520,7 +612,7 @@ export function queryTransactionSuccess( datasourceId: string ): ThunkResult { return (dispatch, getState) => { - const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore; + const { datasourceInstance, history, queryTransactions, scanner, scanning } = getState().explore[exploreId]; // If datasource already changed, results do not matter if (datasourceInstance.meta.id !== datasourceId) { @@ -558,6 +650,7 @@ export function queryTransactionSuccess( dispatch({ type: ActionTypes.QueryTransactionSuccess, + exploreId, history: nextHistory, queryTransactions: nextQueryTransactions, }); @@ -568,24 +661,24 @@ export function queryTransactionSuccess( const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done); if (!other) { const range = scanner(); - dispatch({ type: ActionTypes.ScanRange, range }); + dispatch({ type: ActionTypes.ScanRange, exploreId, range }); } } else { // We can stop scanning if we have a result - dispatch(scanStop()); + dispatch(scanStop(exploreId)); } } }; } -export function removeQueryRow(index: number): ThunkResult { +export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.RemoveQueryRow, index }); - dispatch(runQueries()); + dispatch({ type: ActionTypes.RemoveQueryRow, exploreId, index }); + dispatch(runQueries(exploreId)); }; } -export function runQueries() { +export function runQueries(exploreId: ExploreId) { return (dispatch, getState) => { const { datasourceInstance, @@ -596,10 +689,10 @@ export function runQueries() { supportsGraph, supportsLogs, supportsTable, - } = getState().explore; + } = getState().explore[exploreId]; if (!hasNonEmptyQuery(modifiedQueries)) { - dispatch({ type: ActionTypes.RunQueriesEmpty }); + dispatch({ type: ActionTypes.RunQueriesEmpty, exploreId }); return; } @@ -611,6 +704,7 @@ export function runQueries() { if (showingTable && supportsTable) { dispatch( runQueriesForType( + exploreId, 'Table', { interval, @@ -625,6 +719,7 @@ export function runQueries() { if (showingGraph && supportsGraph) { dispatch( runQueriesForType( + exploreId, 'Graph', { interval, @@ -636,13 +731,18 @@ export function runQueries() { ); } if (showingLogs && supportsLogs) { - dispatch(runQueriesForType('Logs', { interval, format: 'logs' })); + dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' })); } // TODO save state }; } -function runQueriesForType(resultType: ResultType, queryOptions: QueryOptions, resultGetter?: any) { +function runQueriesForType( + exploreId: ExploreId, + resultType: ResultType, + queryOptions: QueryOptions, + resultGetter?: any +) { return async (dispatch, getState) => { const { datasourceInstance, @@ -651,7 +751,7 @@ function runQueriesForType(resultType: ResultType, queryOptions: QueryOptions, r queryIntervals, range, scanning, - } = getState().explore; + } = getState().explore[exploreId]; const datasourceId = datasourceInstance.meta.id; // Run all queries concurrently @@ -665,30 +765,30 @@ function runQueriesForType(resultType: ResultType, queryOptions: QueryOptions, r queryIntervals, scanning ); - dispatch(queryTransactionStart(transaction, resultType, rowIndex)); + dispatch(queryTransactionStart(exploreId, transaction, resultType, rowIndex)); try { const now = Date.now(); const res = await datasourceInstance.query(transaction.options); eventBridge.emit('data-received', res.data || []); const latency = Date.now() - now; const results = resultGetter ? resultGetter(res.data) : res.data; - dispatch(queryTransactionSuccess(transaction.id, results, latency, queries, datasourceId)); + dispatch(queryTransactionSuccess(exploreId, transaction.id, results, latency, queries, datasourceId)); } catch (response) { eventBridge.emit('data-error', response); - dispatch(queryTransactionFailure(transaction.id, response, datasourceId)); + dispatch(queryTransactionFailure(exploreId, transaction.id, response, datasourceId)); } }); }; } -export function scanStart(scanner: RangeScanner): ThunkResult { +export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.ScanStart, scanner }); + dispatch({ type: ActionTypes.ScanStart, exploreId, scanner }); const range = scanner(); - dispatch({ type: ActionTypes.ScanRange, range }); + dispatch({ type: ActionTypes.ScanRange, exploreId, range }); }; } -export function scanStop(): ScanStopAction { - return { type: ActionTypes.ScanStop }; +export function scanStop(exploreId: ExploreId): ScanStopAction { + return { type: ActionTypes.ScanStop, exploreId }; } diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index b49d54405d1..cea4d766c58 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -16,7 +16,14 @@ import { LogsModel } from 'app/core/logs_model'; import TableModel from 'app/core/table_model'; // TODO move to types + export interface ExploreState { + split: boolean; + left: ExploreItemState; + right: ExploreItemState; +} + +export interface ExploreItemState { StartPage?: any; containerWidth: number; datasourceInstance: any; @@ -57,7 +64,7 @@ export const DEFAULT_RANGE = { // Millies step for helper bar charts const DEFAULT_GRAPH_INTERVAL = 15 * 1000; -const initialExploreState: ExploreState = { +const makeExploreItemState = (): ExploreItemState => ({ StartPage: undefined, containerWidth: 0, datasourceInstance: null, @@ -79,9 +86,15 @@ const initialExploreState: ExploreState = { supportsGraph: null, supportsLogs: null, supportsTable: null, +}); + +const initialExploreState: ExploreState = { + split: false, + left: makeExploreItemState(), + right: makeExploreItemState(), }; -export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => { +const itemReducer = (state, action: Action): ExploreItemState => { switch (action.type) { case ActionTypes.AddQueryRow: { const { initialQueries, modifiedQueries, queryTransactions } = state; @@ -407,6 +420,36 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp return state; }; +export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => { + switch (action.type) { + case ActionTypes.ClickCloseSplit: { + return { + ...state, + split: false, + }; + } + + case ActionTypes.ClickSplit: { + return { + ...state, + split: true, + right: action.itemState, + }; + } + } + + const { exploreId } = action as any; + if (exploreId !== undefined) { + const exploreItemState = state[exploreId]; + return { + ...state, + [exploreId]: itemReducer(exploreItemState, action), + }; + } + + return state; +}; + export default { explore: exploreReducer, }; diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index c64ce6133cf..525c8f74c81 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -75,6 +75,11 @@ export interface CompletionItemGroup { skipSort?: boolean; } +export enum ExploreId { + left = 'left', + right = 'right', +} + export interface HistoryItem { ts: number; query: DataQuery; diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 098dae1a4a2..abd13a10368 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -1,5 +1,5 @@ .explore { - width: 100%; + flex: 1 1 auto; &-container { padding: $dashboard-padding;