Explorar o código

Connect Explore child components to store

David Kaltschmidt %!s(int64=7) %!d(string=hai) anos
pai
achega
546a3a9d98

+ 20 - 181
public/app/features/explore/Explore.tsx

@@ -6,86 +6,58 @@ import { AutoSizer } from 'react-virtualized';
 import { RawTimeRange, TimeRange } from '@grafana/ui';
 
 import { DataSourceSelectItem } from 'app/types/datasources';
-import {
-  ExploreItemState,
-  ExploreUrlState,
-  HistoryItem,
-  QueryTransaction,
-  RangeScanner,
-  ExploreId,
-} from 'app/types/explore';
+import { ExploreItemState, ExploreUrlState, 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, DEFAULT_RANGE } from 'app/core/utils/explore';
 import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
 import { Emitter } from 'app/core/utils/emitter';
-import { LogsModel } from 'app/core/logs_model';
-import TableModel from 'app/core/table_model';
 
 import {
-  addQueryRow,
   changeDatasource,
-  changeQuery,
   changeSize,
   changeTime,
   clickClear,
   clickCloseSplit,
   clickExample,
-  clickGraphButton,
-  clickLogsButton,
   clickSplit,
-  clickTableButton,
-  highlightLogsExpression,
   initializeExplore,
   modifyQueries,
-  removeQueryRow,
   runQueries,
   scanStart,
   scanStop,
 } from './state/actions';
 
-import Panel from './Panel';
-import QueryRows from './QueryRows';
-import Graph from './Graph';
-import Logs from './Logs';
-import Table from './Table';
-import ErrorBoundary from './ErrorBoundary';
 import { Alert } from './Error';
+import ErrorBoundary from './ErrorBoundary';
+import GraphContainer from './GraphContainer';
+import LogsContainer from './LogsContainer';
+import QueryRows from './QueryRows';
+import TableContainer from './TableContainer';
 import TimePicker, { parseTime } from './TimePicker';
 
 interface ExploreProps {
   StartPage?: any;
-  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[];
   initialDatasource?: string;
   initialQueries: DataQuery[];
   initializeExplore: typeof initializeExplore;
   initialized: boolean;
-  logsHighlighterExpressions?: string[];
-  logsResult?: LogsModel;
+  loading: boolean;
   modifyQueries: typeof modifyQueries;
-  queryTransactions: QueryTransaction[];
-  removeQueryRow: typeof removeQueryRow;
   range: RawTimeRange;
   runQueries: typeof runQueries;
   scanner?: RangeScanner;
@@ -94,14 +66,10 @@ interface ExploreProps {
   scanStart: typeof scanStart;
   scanStop: typeof scanStop;
   split: boolean;
-  showingGraph: boolean;
-  showingLogs: boolean;
   showingStartPage?: boolean;
-  showingTable: boolean;
   supportsGraph: boolean | null;
   supportsLogs: boolean | null;
   supportsTable: boolean | null;
-  tableResult?: TableModel;
   urlState: ExploreUrlState;
 }
 
@@ -172,24 +140,10 @@ export class Explore extends React.PureComponent<ExploreProps> {
     this.el = el;
   };
 
-  onAddQueryRow = index => {
-    this.props.addQueryRow(this.props.exploreId, index);
-  };
-
   onChangeDatasource = async option => {
     this.props.changeDatasource(this.props.exploreId, option.value);
   };
 
-  onChangeQuery = (query: DataQuery, index: number, override?: boolean) => {
-    const { changeQuery, datasourceInstance, exploreId } = this.props;
-
-    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);
-    }
-  };
-
   onChangeTime = (range: TimeRange, changedByScanner?: boolean) => {
     if (this.props.scanning && !changedByScanner) {
       this.onStopScanning();
@@ -205,14 +159,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
     this.props.clickCloseSplit();
   };
 
-  onClickGraphButton = () => {
-    this.props.clickGraphButton(this.props.exploreId);
-  };
-
-  onClickLogsButton = () => {
-    this.props.clickLogsButton(this.props.exploreId);
-  };
-
   // Use this in help pages to set page to a single query
   onClickExample = (query: DataQuery) => {
     this.props.clickExample(this.props.exploreId, query);
@@ -222,10 +168,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
     this.props.clickSplit();
   };
 
-  onClickTableButton = () => {
-    this.props.clickTableButton(this.props.exploreId);
-  };
-
   onClickLabel = (key: string, value: string) => {
     this.onModifyQueries({ type: 'ADD_FILTER', key, value });
   };
@@ -238,10 +180,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
     }
   };
 
-  onRemoveQueryRow = index => {
-    this.props.removeQueryRow(this.props.exploreId, index);
-  };
-
   onResize = (size: { height: number; width: number }) => {
     this.props.changeSize(this.props.exploreId, size);
   };
@@ -265,14 +203,6 @@ export class Explore extends React.PureComponent<ExploreProps> {
     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(this.props.exploreId, expressions);
-    }
-  }, 500);
-
   render() {
     const {
       StartPage,
@@ -282,34 +212,19 @@ export class Explore extends React.PureComponent<ExploreProps> {
       datasourceMissing,
       exploreDatasources,
       exploreId,
-      graphResult,
-      history,
+      loading,
       initialQueries,
-      logsHighlighterExpressions,
-      logsResult,
-      queryTransactions,
       range,
-      scanning,
-      scanRange,
-      showingGraph,
-      showingLogs,
       showingStartPage,
-      showingTable,
       split,
       supportsGraph,
       supportsLogs,
       supportsTable,
-      tableResult,
     } = this.props;
-    const graphHeight = showingGraph && showingTable ? '200px' : '400px';
     const exploreClass = split ? 'explore explore-split' : 'explore';
     const selectedDatasource = datasourceInstance
       ? exploreDatasources.find(d => d.name === datasourceInstance.name)
       : undefined;
-    const graphLoading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done);
-    const tableLoading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done);
-    const logsLoading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done);
-    const loading = queryTransactions.some(qt => !qt.done);
 
     return (
       <div className={exploreClass} ref={this.getRef}>
@@ -372,19 +287,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
         {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}
-              />
+              <QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} initialQueries={initialQueries} />
               <AutoSizer onResize={this.onResize} disableHeight>
                 {({ width }) => (
                   <main className="m-t-2" style={{ width }}>
@@ -392,55 +295,16 @@ export class Explore extends React.PureComponent<ExploreProps> {
                       {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>
-                          )}
+                          {supportsGraph && <GraphContainer exploreId={exploreId} />}
+                          {supportsTable && <TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />}
                           {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>
+                            <LogsContainer
+                              exploreId={exploreId}
+                              onChangeTime={this.onChangeTime}
+                              onClickLabel={this.onClickLabel}
+                              onStartScanning={this.onStartScanning}
+                              onStopScanning={this.onStopScanning}
+                            />
                           )}
                         </>
                       )}
@@ -466,26 +330,17 @@ function mapStateToProps(state: StoreState, { exploreId }) {
     datasourceLoading,
     datasourceMissing,
     exploreDatasources,
-    graphResult,
     initialDatasource,
     initialQueries,
     initialized,
-    history,
-    logsHighlighterExpressions,
-    logsResult,
     queryTransactions,
     range,
-    scanning,
-    scanRange,
-    showingGraph,
-    showingLogs,
     showingStartPage,
-    showingTable,
     supportsGraph,
     supportsLogs,
     supportsTable,
-    tableResult,
   } = item;
+  const loading = queryTransactions.some(qt => !qt.done);
   return {
     StartPage,
     datasourceError,
@@ -493,46 +348,30 @@ function mapStateToProps(state: StoreState, { exploreId }) {
     datasourceLoading,
     datasourceMissing,
     exploreDatasources,
-    graphResult,
     initialDatasource,
     initialQueries,
     initialized,
-    history,
-    logsHighlighterExpressions,
-    logsResult,
+    loading,
     queryTransactions,
     range,
-    scanning,
-    scanRange,
-    showingGraph,
-    showingLogs,
     showingStartPage,
-    showingTable,
     split,
     supportsGraph,
     supportsLogs,
     supportsTable,
-    tableResult,
   };
 }
 
 const mapDispatchToProps = {
-  addQueryRow,
   changeDatasource,
-  changeQuery,
   changeSize,
   changeTime,
   clickClear,
   clickCloseSplit,
   clickExample,
-  clickGraphButton,
-  clickLogsButton,
   clickSplit,
-  clickTableButton,
-  highlightLogsExpression,
   initializeExplore,
   modifyQueries,
-  removeQueryRow,
   runQueries,
   scanStart,
   scanStop,

+ 61 - 0
public/app/features/explore/GraphContainer.tsx

@@ -0,0 +1,61 @@
+import React, { PureComponent } from 'react';
+import { hot } from 'react-hot-loader';
+import { connect } from 'react-redux';
+import { RawTimeRange, TimeRange } from '@grafana/ui';
+
+import { ExploreId, ExploreItemState } from 'app/types/explore';
+import { StoreState } from 'app/types';
+
+import { clickGraphButton } from './state/actions';
+import Graph from './Graph';
+import Panel from './Panel';
+
+interface GraphContainerProps {
+  onChangeTime: (range: TimeRange) => void;
+  clickGraphButton: typeof clickGraphButton;
+  exploreId: ExploreId;
+  graphResult?: any[];
+  loading: boolean;
+  range: RawTimeRange;
+  showingGraph: boolean;
+  showingTable: boolean;
+  split: boolean;
+}
+
+export class GraphContainer extends PureComponent<GraphContainerProps> {
+  onClickGraphButton = () => {
+    this.props.clickGraphButton(this.props.exploreId);
+  };
+
+  render() {
+    const { exploreId, graphResult, loading, onChangeTime, showingGraph, showingTable, range, split } = this.props;
+    const graphHeight = showingGraph && showingTable ? '200px' : '400px';
+    return (
+      <Panel label="Graph" isOpen={showingGraph} loading={loading} onToggle={this.onClickGraphButton}>
+        <Graph
+          data={graphResult}
+          height={graphHeight}
+          id={`explore-graph-${exploreId}`}
+          onChangeTime={onChangeTime}
+          range={range}
+          split={split}
+        />
+      </Panel>
+    );
+  }
+}
+
+function mapStateToProps(state: StoreState, { exploreId }) {
+  const explore = state.explore;
+  const { split } = explore;
+  const item: ExploreItemState = explore[exploreId];
+  const { graphResult, queryTransactions, range, showingGraph, showingTable } = item;
+  const loading = queryTransactions.some(qt => qt.resultType === 'Graph' && !qt.done);
+  return { graphResult, loading, range, showingGraph, showingTable, split };
+}
+
+const mapDispatchToProps = {
+  clickGraphButton,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(GraphContainer));

+ 91 - 0
public/app/features/explore/LogsContainer.tsx

@@ -0,0 +1,91 @@
+import React, { PureComponent } from 'react';
+import { hot } from 'react-hot-loader';
+import { connect } from 'react-redux';
+import { RawTimeRange, TimeRange } from '@grafana/ui';
+
+import { ExploreId, ExploreItemState } from 'app/types/explore';
+import { LogsModel } from 'app/core/logs_model';
+import { StoreState } from 'app/types';
+
+import { clickLogsButton } from './state/actions';
+import Logs from './Logs';
+import Panel from './Panel';
+
+interface LogsContainerProps {
+  clickLogsButton: typeof clickLogsButton;
+  exploreId: ExploreId;
+  loading: boolean;
+  logsHighlighterExpressions?: string[];
+  logsResult?: LogsModel;
+  onChangeTime: (range: TimeRange) => void;
+  onClickLabel: (key: string, value: string) => void;
+  onStartScanning: () => void;
+  onStopScanning: () => void;
+  range: RawTimeRange;
+  scanning?: boolean;
+  scanRange?: RawTimeRange;
+  showingLogs: boolean;
+}
+
+export class LogsContainer extends PureComponent<LogsContainerProps> {
+  onClickLogsButton = () => {
+    this.props.clickLogsButton(this.props.exploreId);
+  };
+
+  render() {
+    const {
+      exploreId,
+      loading,
+      logsHighlighterExpressions,
+      logsResult,
+      onChangeTime,
+      onClickLabel,
+      onStartScanning,
+      onStopScanning,
+      range,
+      showingLogs,
+      scanning,
+      scanRange,
+    } = this.props;
+    return (
+      <Panel label="Logs" loading={loading} isOpen={showingLogs} onToggle={this.onClickLogsButton}>
+        <Logs
+          data={logsResult}
+          exploreId={exploreId}
+          key={logsResult.id}
+          highlighterExpressions={logsHighlighterExpressions}
+          loading={loading}
+          onChangeTime={onChangeTime}
+          onClickLabel={onClickLabel}
+          onStartScanning={onStartScanning}
+          onStopScanning={onStopScanning}
+          range={range}
+          scanning={scanning}
+          scanRange={scanRange}
+        />
+      </Panel>
+    );
+  }
+}
+
+function mapStateToProps(state: StoreState, { exploreId }) {
+  const explore = state.explore;
+  const item: ExploreItemState = explore[exploreId];
+  const { logsHighlighterExpressions, logsResult, queryTransactions, scanning, scanRange, showingLogs, range } = item;
+  const loading = queryTransactions.some(qt => qt.resultType === 'Logs' && !qt.done);
+  return {
+    loading,
+    logsHighlighterExpressions,
+    logsResult,
+    scanning,
+    scanRange,
+    showingLogs,
+    range,
+  };
+}
+
+const mapDispatchToProps = {
+  clickLogsButton,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(LogsContainer));

+ 163 - 0
public/app/features/explore/QueryRow.tsx

@@ -0,0 +1,163 @@
+import React, { PureComponent } from 'react';
+import { hot } from 'react-hot-loader';
+import { connect } from 'react-redux';
+import { RawTimeRange } from '@grafana/ui';
+import _ from 'lodash';
+
+import { QueryTransaction, HistoryItem, QueryHint, ExploreItemState, ExploreId } from 'app/types/explore';
+import { Emitter } from 'app/core/utils/emitter';
+import { DataQuery, StoreState } from 'app/types';
+
+// import DefaultQueryField from './QueryField';
+import QueryEditor from './QueryEditor';
+import QueryTransactionStatus from './QueryTransactionStatus';
+import {
+  addQueryRow,
+  changeQuery,
+  highlightLogsExpression,
+  modifyQueries,
+  removeQueryRow,
+  runQueries,
+} from './state/actions';
+
+function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
+  const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
+  if (transaction) {
+    return transaction.hints[0];
+  }
+  return undefined;
+}
+
+interface QueryRowProps {
+  addQueryRow: typeof addQueryRow;
+  changeQuery: typeof changeQuery;
+  className?: string;
+  exploreId: ExploreId;
+  datasourceInstance: any;
+  highlightLogsExpression: typeof highlightLogsExpression;
+  history: HistoryItem[];
+  index: number;
+  initialQuery: DataQuery;
+  modifyQueries: typeof modifyQueries;
+  queryTransactions: QueryTransaction[];
+  exploreEvents: Emitter;
+  range: RawTimeRange;
+  removeQueryRow: typeof removeQueryRow;
+  runQueries: typeof runQueries;
+}
+
+export class QueryRow extends PureComponent<QueryRowProps> {
+  onExecuteQuery = () => {
+    const { exploreId } = this.props;
+    this.props.runQueries(exploreId);
+  };
+
+  onChangeQuery = (query: DataQuery, override?: boolean) => {
+    const { datasourceInstance, exploreId, index } = this.props;
+    this.props.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);
+    }
+  };
+
+  onClickAddButton = () => {
+    const { exploreId, index } = this.props;
+    this.props.addQueryRow(exploreId, index);
+  };
+
+  onClickClearButton = () => {
+    this.onChangeQuery(null, true);
+  };
+
+  onClickHintFix = action => {
+    const { datasourceInstance, exploreId, index } = this.props;
+    if (datasourceInstance && datasourceInstance.modifyQuery) {
+      const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action);
+      this.props.modifyQueries(exploreId, action, index, modifier);
+    }
+  };
+
+  onClickRemoveButton = () => {
+    const { exploreId, index } = this.props;
+    this.props.removeQueryRow(exploreId, index);
+  };
+
+  updateLogsHighlights = _.debounce((value: DataQuery) => {
+    const { datasourceInstance } = this.props;
+    if (datasourceInstance.getHighlighterExpression) {
+      const expressions = [datasourceInstance.getHighlighterExpression(value)];
+      this.props.highlightLogsExpression(this.props.exploreId, expressions);
+    }
+  }, 500);
+
+  render() {
+    const { datasourceInstance, history, index, initialQuery, queryTransactions, exploreEvents, range } = this.props;
+    const transactions = queryTransactions.filter(t => t.rowIndex === index);
+    const transactionWithError = transactions.find(t => t.error !== undefined);
+    const hint = getFirstHintFromTransactions(transactions);
+    const queryError = transactionWithError ? transactionWithError.error : null;
+    const QueryField = datasourceInstance.pluginExports.ExploreQueryField;
+    return (
+      <div className="query-row">
+        <div className="query-row-status">
+          <QueryTransactionStatus transactions={transactions} />
+        </div>
+        <div className="query-row-field">
+          {QueryField ? (
+            <QueryField
+              datasource={datasourceInstance}
+              error={queryError}
+              hint={hint}
+              initialQuery={initialQuery}
+              history={history}
+              onClickHintFix={this.onClickHintFix}
+              onPressEnter={this.onExecuteQuery}
+              onQueryChange={this.onChangeQuery}
+            />
+          ) : (
+            <QueryEditor
+              datasource={datasourceInstance}
+              error={queryError}
+              onQueryChange={this.onChangeQuery}
+              onExecuteQuery={this.onExecuteQuery}
+              initialQuery={initialQuery}
+              exploreEvents={exploreEvents}
+              range={range}
+            />
+          )}
+        </div>
+        <div className="query-row-tools">
+          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickClearButton}>
+            <i className="fa fa-times" />
+          </button>
+          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickAddButton}>
+            <i className="fa fa-plus" />
+          </button>
+          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickRemoveButton}>
+            <i className="fa fa-minus" />
+          </button>
+        </div>
+      </div>
+    );
+  }
+}
+
+function mapStateToProps(state: StoreState, { exploreId, index }) {
+  const explore = state.explore;
+  const item: ExploreItemState = explore[exploreId];
+  const { datasourceInstance, history, initialQueries, queryTransactions, range } = item;
+  const initialQuery = initialQueries[index];
+  return { datasourceInstance, history, initialQuery, queryTransactions, range };
+}
+
+const mapDispatchToProps = {
+  addQueryRow,
+  changeQuery,
+  highlightLogsExpression,
+  modifyQueries,
+  removeQueryRow,
+  runQueries,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(QueryRow));

+ 9 - 143
public/app/features/explore/QueryRows.tsx

@@ -1,159 +1,25 @@
 import React, { PureComponent } from 'react';
 
-import { QueryTransaction, HistoryItem, QueryHint } from 'app/types/explore';
 import { Emitter } from 'app/core/utils/emitter';
+import { DataQuery } from 'app/types';
+import { ExploreId } from 'app/types/explore';
 
-// import DefaultQueryField from './QueryField';
-import QueryEditor from './QueryEditor';
-import QueryTransactionStatus from './QueryTransactionStatus';
-import { DataSource, DataQuery } from 'app/types';
-import { RawTimeRange } from '@grafana/ui';
+import QueryRow from './QueryRow';
 
-function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
-  const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
-  if (transaction) {
-    return transaction.hints[0];
-  }
-  return undefined;
-}
-
-interface QueryRowEventHandlers {
-  onAddQueryRow: (index: number) => void;
-  onChangeQuery: (value: DataQuery, index: number, override?: boolean) => void;
-  onClickHintFix: (action: object, index?: number) => void;
-  onExecuteQuery: () => void;
-  onRemoveQueryRow: (index: number) => void;
-}
-
-interface QueryRowCommonProps {
+interface QueryRowsProps {
   className?: string;
-  datasource: DataSource;
-  history: HistoryItem[];
-  transactions: QueryTransaction[];
   exploreEvents: Emitter;
-  range: RawTimeRange;
-}
-
-type QueryRowProps = QueryRowCommonProps &
-  QueryRowEventHandlers & {
-    index: number;
-    initialQuery: DataQuery;
-  };
-
-class QueryRow extends PureComponent<QueryRowProps> {
-  onExecuteQuery = () => {
-    const { onExecuteQuery } = this.props;
-    onExecuteQuery();
-  };
-
-  onChangeQuery = (value: DataQuery, override?: boolean) => {
-    const { index, onChangeQuery } = this.props;
-    if (onChangeQuery) {
-      onChangeQuery(value, index, override);
-    }
-  };
-
-  onClickAddButton = () => {
-    const { index, onAddQueryRow } = this.props;
-    if (onAddQueryRow) {
-      onAddQueryRow(index);
-    }
-  };
-
-  onClickClearButton = () => {
-    this.onChangeQuery(null, true);
-  };
-
-  onClickHintFix = action => {
-    const { index, onClickHintFix } = this.props;
-    if (onClickHintFix) {
-      onClickHintFix(action, index);
-    }
-  };
-
-  onClickRemoveButton = () => {
-    const { index, onRemoveQueryRow } = this.props;
-    if (onRemoveQueryRow) {
-      onRemoveQueryRow(index);
-    }
-  };
-
-  onPressEnter = () => {
-    const { onExecuteQuery } = this.props;
-    if (onExecuteQuery) {
-      onExecuteQuery();
-    }
-  };
-
-  render() {
-    const { datasource, history, initialQuery, transactions, exploreEvents, range } = this.props;
-    const transactionWithError = transactions.find(t => t.error !== undefined);
-    const hint = getFirstHintFromTransactions(transactions);
-    const queryError = transactionWithError ? transactionWithError.error : null;
-    const QueryField = datasource.pluginExports.ExploreQueryField;
-    return (
-      <div className="query-row">
-        <div className="query-row-status">
-          <QueryTransactionStatus transactions={transactions} />
-        </div>
-        <div className="query-row-field">
-          {QueryField ? (
-            <QueryField
-              datasource={datasource}
-              error={queryError}
-              hint={hint}
-              initialQuery={initialQuery}
-              history={history}
-              onClickHintFix={this.onClickHintFix}
-              onPressEnter={this.onPressEnter}
-              onQueryChange={this.onChangeQuery}
-            />
-          ) : (
-            <QueryEditor
-              datasource={datasource}
-              error={queryError}
-              onQueryChange={this.onChangeQuery}
-              onExecuteQuery={this.onExecuteQuery}
-              initialQuery={initialQuery}
-              exploreEvents={exploreEvents}
-              range={range}
-            />
-          )}
-        </div>
-        <div className="query-row-tools">
-          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickClearButton}>
-            <i className="fa fa-times" />
-          </button>
-          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickAddButton}>
-            <i className="fa fa-plus" />
-          </button>
-          <button className="btn navbar-button navbar-button--tight" onClick={this.onClickRemoveButton}>
-            <i className="fa fa-minus" />
-          </button>
-        </div>
-      </div>
-    );
-  }
+  exploreId: ExploreId;
+  initialQueries: DataQuery[];
 }
-
-type QueryRowsProps = QueryRowCommonProps &
-  QueryRowEventHandlers & {
-    initialQueries: DataQuery[];
-  };
-
 export default class QueryRows extends PureComponent<QueryRowsProps> {
   render() {
-    const { className = '', initialQueries, transactions, ...handlers } = this.props;
+    const { className = '', exploreEvents, exploreId, initialQueries } = this.props;
     return (
       <div className={className}>
         {initialQueries.map((query, index) => (
-          <QueryRow
-            key={query.key}
-            index={index}
-            initialQuery={query}
-            transactions={transactions.filter(t => t.rowIndex === index)}
-            {...handlers}
-          />
+          // TODO instead of relying on initialQueries, move to react key list in redux
+          <QueryRow key={query.key} exploreEvents={exploreEvents} exploreId={exploreId} index={index} />
         ))}
       </div>
     );

+ 49 - 0
public/app/features/explore/TableContainer.tsx

@@ -0,0 +1,49 @@
+import React, { PureComponent } from 'react';
+import { hot } from 'react-hot-loader';
+import { connect } from 'react-redux';
+
+import { ExploreId, ExploreItemState } from 'app/types/explore';
+import { StoreState } from 'app/types';
+
+import { clickTableButton } from './state/actions';
+import Table from './Table';
+import Panel from './Panel';
+import TableModel from 'app/core/table_model';
+
+interface TableContainerProps {
+  clickTableButton: typeof clickTableButton;
+  exploreId: ExploreId;
+  loading: boolean;
+  onClickLabel: (key: string, value: string) => void;
+  showingTable: boolean;
+  tableResult?: TableModel;
+}
+
+export class TableContainer extends PureComponent<TableContainerProps> {
+  onClickTableButton = () => {
+    this.props.clickTableButton(this.props.exploreId);
+  };
+
+  render() {
+    const { loading, onClickLabel, showingTable, tableResult } = this.props;
+    return (
+      <Panel label="Table" loading={loading} isOpen={showingTable} onToggle={this.onClickTableButton}>
+        <Table data={tableResult} loading={loading} onClickCell={onClickLabel} />
+      </Panel>
+    );
+  }
+}
+
+function mapStateToProps(state: StoreState, { exploreId }) {
+  const explore = state.explore;
+  const item: ExploreItemState = explore[exploreId];
+  const { queryTransactions, showingTable, tableResult } = item;
+  const loading = queryTransactions.some(qt => qt.resultType === 'Table' && !qt.done);
+  return { loading, showingTable, tableResult };
+}
+
+const mapDispatchToProps = {
+  clickTableButton,
+};
+
+export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TableContainer));

+ 1 - 0
public/app/features/explore/state/reducers.ts

@@ -232,6 +232,7 @@ const itemReducer = (state, action: Action): ExploreItemState => {
         initialQueries: action.initialQueries,
         logsHighlighterExpressions: undefined,
         modifiedQueries: action.initialQueries.slice(),
+        queryTransactions: [],
         showingStartPage: action.showingStartPage,
         supportsGraph: action.supportsGraph,
         supportsLogs: action.supportsLogs,