Browse Source

Allow multiple Explore items for split

David Kaltschmidt 7 years ago
parent
commit
68c039b289

+ 8 - 5
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
+    )
   );
 }
 

+ 144 - 128
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<ExploreProps, any> {
+export class Explore extends React.PureComponent<ExploreProps> {
   el: any;
   exploreEvents: Emitter;
   /**
@@ -147,13 +149,23 @@ export class Explore extends React.PureComponent<ExploreProps, any> {
   }
 
   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<ExploreProps, any> {
   };
 
   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<ExploreProps, any> {
     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<ExploreProps, any> {
     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<ExploreProps, any> {
   };
 
   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<ExploreProps, any> {
       datasourceLoading,
       datasourceMissing,
       exploreDatasources,
+      exploreId,
       graphResult,
       history,
       initialQueries,
       logsHighlighterExpressions,
       logsResult,
       queryTransactions,
-      position,
       range,
       scanning,
       scanRange,
@@ -323,7 +323,7 @@ export class Explore extends React.PureComponent<ExploreProps, any> {
     return (
       <div className={exploreClass} ref={this.getRef}>
         <div className="navbar">
-          {position === 'left' ? (
+          {exploreId === 'left' ? (
             <div>
               <a className="navbar-page-btn">
                 <i className="fa fa-rocket" />
@@ -347,7 +347,7 @@ export class Explore extends React.PureComponent<ExploreProps, any> {
             </div>
           ) : null}
           <div className="navbar__spacer" />
-          {position === 'left' && !split ? (
+          {exploreId === 'left' && !split ? (
             <div className="navbar-buttons">
               <button className="btn navbar-button" onClick={this.onClickSplit}>
                 Split
@@ -378,83 +378,96 @@ export class Explore extends React.PureComponent<ExploreProps, any> {
           </div>
         )}
 
-        {datasourceInstance && !datasourceError ? (
-          <div className="explore-container">
-            <QueryRows
-              datasource={datasourceInstance}
-              history={history}
-              initialQueries={initialQueries}
-              onAddQueryRow={this.onAddQueryRow}
-              onChangeQuery={this.onChangeQuery}
-              onClickHintFix={this.onModifyQueries}
-              onExecuteQuery={this.onSubmit}
-              onRemoveQueryRow={this.onRemoveQueryRow}
-              transactions={queryTransactions}
-              exploreEvents={this.exploreEvents}
-              range={range}
-            />
-            <main className="m-t-2">
-              <ErrorBoundary>
-                {showingStartPage && <StartPage onClickExample={this.onClickExample} />}
-                {!showingStartPage && (
-                  <>
-                    {supportsGraph && (
-                      <Panel
-                        label="Graph"
-                        isOpen={showingGraph}
-                        loading={graphLoading}
-                        onToggle={this.onClickGraphButton}
-                      >
-                        <Graph
-                          data={graphResult}
-                          height={graphHeight}
-                          id={`explore-graph-${position}`}
-                          onChangeTime={this.onChangeTime}
-                          range={range}
-                          split={split}
-                        />
-                      </Panel>
-                    )}
-                    {supportsTable && (
-                      <Panel
-                        label="Table"
-                        loading={tableLoading}
-                        isOpen={showingTable}
-                        onToggle={this.onClickTableButton}
-                      >
-                        <Table data={tableResult} loading={tableLoading} onClickCell={this.onClickLabel} />
-                      </Panel>
-                    )}
-                    {supportsLogs && (
-                      <Panel label="Logs" loading={logsLoading} isOpen={showingLogs} onToggle={this.onClickLogsButton}>
-                        <Logs
-                          data={logsResult}
-                          key={logsResult.id}
-                          highlighterExpressions={logsHighlighterExpressions}
-                          loading={logsLoading}
-                          position={position}
-                          onChangeTime={this.onChangeTime}
-                          onClickLabel={this.onClickLabel}
-                          onStartScanning={this.onStartScanning}
-                          onStopScanning={this.onStopScanning}
-                          range={range}
-                          scanning={scanning}
-                          scanRange={scanRange}
-                        />
-                      </Panel>
-                    )}
-                  </>
+        {datasourceInstance &&
+          !datasourceError && (
+            <div className="explore-container">
+              <QueryRows
+                datasource={datasourceInstance}
+                history={history}
+                initialQueries={initialQueries}
+                onAddQueryRow={this.onAddQueryRow}
+                onChangeQuery={this.onChangeQuery}
+                onClickHintFix={this.onModifyQueries}
+                onExecuteQuery={this.onSubmit}
+                onRemoveQueryRow={this.onRemoveQueryRow}
+                transactions={queryTransactions}
+                exploreEvents={this.exploreEvents}
+                range={range}
+              />
+              <AutoSizer onResize={this.onResize} disableHeight>
+                {({ width }) => (
+                  <main className="m-t-2" style={{ width }}>
+                    <ErrorBoundary>
+                      {showingStartPage && <StartPage onClickExample={this.onClickExample} />}
+                      {!showingStartPage && (
+                        <>
+                          {supportsGraph && (
+                            <Panel
+                              label="Graph"
+                              isOpen={showingGraph}
+                              loading={graphLoading}
+                              onToggle={this.onClickGraphButton}
+                            >
+                              <Graph
+                                data={graphResult}
+                                height={graphHeight}
+                                id={`explore-graph-${exploreId}`}
+                                onChangeTime={this.onChangeTime}
+                                range={range}
+                                split={split}
+                              />
+                            </Panel>
+                          )}
+                          {supportsTable && (
+                            <Panel
+                              label="Table"
+                              loading={tableLoading}
+                              isOpen={showingTable}
+                              onToggle={this.onClickTableButton}
+                            >
+                              <Table data={tableResult} loading={tableLoading} onClickCell={this.onClickLabel} />
+                            </Panel>
+                          )}
+                          {supportsLogs && (
+                            <Panel
+                              label="Logs"
+                              loading={logsLoading}
+                              isOpen={showingLogs}
+                              onToggle={this.onClickLogsButton}
+                            >
+                              <Logs
+                                data={logsResult}
+                                exploreId={exploreId}
+                                key={logsResult.id}
+                                highlighterExpressions={logsHighlighterExpressions}
+                                loading={logsLoading}
+                                onChangeTime={this.onChangeTime}
+                                onClickLabel={this.onClickLabel}
+                                onStartScanning={this.onStartScanning}
+                                onStopScanning={this.onStopScanning}
+                                range={range}
+                                scanning={scanning}
+                                scanRange={scanRange}
+                              />
+                            </Panel>
+                          )}
+                        </>
+                      )}
+                    </ErrorBoundary>
+                  </main>
                 )}
-              </ErrorBoundary>
-            </main>
-          </div>
-        ) : null}
+              </AutoSizer>
+            </div>
+          )}
       </div>
     );
   }
 }
 
-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));

+ 3 - 3
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<LogsProps, LogsState> {
   render() {
     const {
       data,
+      exploreId,
       highlighterExpressions,
       loading = false,
       onClickLabel,
-      position,
       range,
       scanning,
       scanRange,
@@ -400,7 +400,7 @@ export default class Logs extends PureComponent<LogsProps, LogsState> {
             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}

+ 24 - 62
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<WrapperProps, WrapperState> {
-  urlStates: { [key: string]: string };
+export class Wrapper extends Component<WrapperProps> {
+  // 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 (
       <div className="explore-wrapper">
         <ErrorBoundary>
-          <Explore
-            datasourceSrv={datasourceSrv}
-            onChangeSplit={this.onChangeSplit}
-            onSaveState={this.onSaveState}
-            position="left"
-            split={split}
-            stateKey={STATE_KEY_LEFT}
-            urlState={urlStateLeft}
-          />
+          <Explore exploreId={ExploreId.left} />
         </ErrorBoundary>
         {split && (
           <ErrorBoundary>
-            <Explore
-              datasourceSrv={datasourceSrv}
-              onChangeSplit={this.onChangeSplit}
-              onSaveState={this.onSaveState}
-              position="right"
-              split={split}
-              splitState={splitState}
-              stateKey={STATE_KEY_RIGHT}
-              urlState={urlStateRight}
-            />
+            <Explore exploreId={ExploreId.right} />
           </ErrorBoundary>
         )}
       </div>
@@ -95,9 +55,11 @@ export class Wrapper extends Component<WrapperProps, WrapperState> {
   }
 }
 
-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,

+ 177 - 77
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<R> = ThunkAction<R, StoreState, undefined, Action>;
 
-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<void> {
+export function changeDatasource(exploreId: ExploreId, datasource: string): ThunkResult<void> {
   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<void> {
+export function changeQuery(
+  exploreId: ExploreId,
+  query: DataQuery,
+  index: number,
+  override: boolean
+): ThunkResult<void> {
   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(exploreId: ExploreId, range: TimeRange): ThunkResult<void> {
+  return dispatch => {
+    dispatch({ type: ActionTypes.ChangeTime, exploreId, range });
+    dispatch(runQueries(exploreId));
+  };
 }
 
-export function changeTime(range: TimeRange): ThunkResult<void> {
+export function clickClear(exploreId: ExploreId): ThunkResult<void> {
   return dispatch => {
-    dispatch({ type: ActionTypes.ChangeTime, range });
-    dispatch(runQueries());
+    dispatch(scanStop(exploreId));
+    dispatch({ type: ActionTypes.ClickClear, exploreId });
+    // TODO save state
   };
 }
 
-export function clickExample(rawQuery: DataQuery): ThunkResult<void> {
+export function clickCloseSplit(): ThunkResult<void> {
+  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<void> {
   return dispatch => {
     const query = { ...rawQuery, ...generateEmptyQuery() };
     dispatch({
       type: ActionTypes.ClickExample,
+      exploreId,
       query,
     });
-    dispatch(runQueries());
+    dispatch(runQueries(exploreId));
   };
 }
 
-export function clickClear(): ThunkResult<void> {
-  return dispatch => {
-    dispatch(scanStop());
-    dispatch({ type: ActionTypes.ClickClear });
-    // TODO save state
+export function clickGraphButton(exploreId: ExploreId): ThunkResult<void> {
+  return (dispatch, getState) => {
+    dispatch({ type: ActionTypes.ClickGraphButton, exploreId });
+    if (getState().explore[exploreId].showingGraph) {
+      dispatch(runQueries(exploreId));
+    }
   };
 }
 
-export function clickGraphButton(): ThunkResult<void> {
+export function clickLogsButton(exploreId: ExploreId): ThunkResult<void> {
   return (dispatch, getState) => {
-    dispatch({ type: ActionTypes.ClickGraphButton });
-    if (getState().explore.showingGraph) {
-      dispatch(runQueries());
+    dispatch({ type: ActionTypes.ClickLogsButton, exploreId });
+    if (getState().explore[exploreId].showingLogs) {
+      dispatch(runQueries(exploreId));
     }
   };
 }
 
-export function clickLogsButton(): ThunkResult<void> {
+export function clickSplit(): ThunkResult<void> {
   return (dispatch, getState) => {
-    dispatch({ type: ActionTypes.ClickLogsButton });
-    if (getState().explore.showingLogs) {
-      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(): ThunkResult<void> {
+export function clickTableButton(exploreId: ExploreId): ThunkResult<void> {
   return (dispatch, getState) => {
-    dispatch({ type: ActionTypes.ClickTableButton });
-    if (getState().explore.showingTable) {
-      dispatch(runQueries());
+    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<void> {
+export function loadDatasource(exploreId: ExploreId, instance: any): ThunkResult<void> {
   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<void> {
       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<void> {
     }
 
     // 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<void> {
       }
     }
 
-    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<void> {
       ...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<void> {
+export function modifyQueries(
+  exploreId: ExploreId,
+  modification: any,
+  index: number,
+  modifier: any
+): ThunkResult<void> {
   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<void> {
+export function queryTransactionFailure(
+  exploreId: ExploreId,
+  transactionId: string,
+  response: any,
+  datasourceId: string
+): ThunkResult<void> {
   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<void> {
   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<void> {
+export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult<void> {
   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<void> {
+export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult<void> {
   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 };
 }

+ 45 - 2
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,
 };

+ 5 - 0
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;

+ 1 - 1
public/sass/pages/_explore.scss

@@ -1,5 +1,5 @@
 .explore {
-  width: 100%;
+  flex: 1 1 auto;
 
   &-container {
     padding: $dashboard-padding;