You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by fj...@apache.org on 2019/06/17 20:14:51 UTC

[incubator-druid] branch master updated: Web console: added Query View metadata pane, more query details (#7905)

This is an automated email from the ASF dual-hosted git repository.

fjy pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-druid.git


The following commit(s) were added to refs/heads/master by this push:
     new df9cdcf  Web console: added Query View metadata pane, more query details (#7905)
df9cdcf is described below

commit df9cdcf13b089e445c764e86a8b277ecec24d0ba
Author: Vadim Ogievetsky <va...@gmail.com>
AuthorDate: Mon Jun 17 13:14:42 2019 -0700

    Web console: added Query View metadata pane, more query details (#7905)
    
    * refactor
    
    * adding metadata hookup
    
    * add scroll
    
    * standerdize icons
    
    * better loading state
    
    * update snapshots
    
    * fix lint
    
    * do not mix up queryId and sqlQueryId
---
 web-console/package-lock.json                      |  10 +
 web-console/package.json                           |   2 +
 web-console/src/console-application.tsx            |  45 ++-
 .../src/dialogs/snitch-dialog/snitch-dialog.tsx    |   2 +-
 .../{views/index.ts => utils/column-metadata.ts}   |  15 +-
 web-console/src/utils/druid-query.ts               |   2 +-
 web-console/src/utils/general.tsx                  |  43 ++-
 web-console/src/utils/query-context.tsx            |  79 ++++
 web-console/src/utils/query-manager.tsx            |   2 +-
 .../views/datasource-view/datasource-view.spec.tsx |   2 +-
 .../src/views/datasource-view/datasource-view.tsx  |  10 +-
 web-console/src/views/index.ts                     |   2 +-
 .../src/views/load-data-view/load-data-view.tsx    |  13 +-
 .../__snapshots__/query-view.spec.tsx.snap         |  52 +++
 .../__snapshots__/column-tree.spec.tsx.snap        | 115 ++++++
 .../column-tree/column-tree.scss}                  |  38 +-
 .../column-tree/column-tree.spec.tsx}              |  22 +-
 .../views/query-view/column-tree/column-tree.tsx   | 203 +++++++++++
 .../__snapshots__/query-extra-info.spec.tsx.snap   |  58 +++
 .../query-extra-info/query-extra-info.scss}        |  20 +-
 .../query-extra-info/query-extra-info.spec.tsx}    |  22 +-
 .../query-extra-info/query-extra-info.tsx          | 105 ++++++
 .../__snapshots__/query-input.spec.tsx.snap        | 100 ++++++
 .../query-input/query-input.scss}                  |  27 +-
 .../query-input/query-input.spec.tsx}              |  15 +-
 .../views/query-view/query-input/query-input.tsx   | 194 ++++++++++
 .../__snapshots__/query-output.spec.tsx.snap       | 320 +++++++++++++++++
 .../query-output/query-output.scss}                |  17 +-
 .../query-output/query-output.spec.tsx}            |  16 +-
 .../views/query-view/query-output/query-output.tsx |  56 +++
 .../sql-view.scss => query-view/query-view.scss}   |  71 ++--
 .../query-view.spec.tsx}                           |  12 +-
 web-console/src/views/query-view/query-view.tsx    | 397 +++++++++++++++++++++
 .../__snapshots__/run-button.spec.tsx.snap         |  78 ++++
 .../run-button/run-button.scss}                    |  12 +-
 .../run-button/run-button.spec.tsx}                |  18 +-
 .../src/views/query-view/run-button/run-button.tsx | 154 ++++++++
 .../src/views/segments-view/segments-view.spec.tsx |   2 +-
 .../src/views/segments-view/segments-view.tsx      |   6 +-
 .../src/views/servers-view/servers-view.spec.tsx   |   2 +-
 .../src/views/servers-view/servers-view.tsx        |   6 +-
 .../sql-view/__snapshots__/sql-view.spec.tsx.snap  | 170 ---------
 .../__snapshots__/sql-control.spec.tsx.snap        | 206 -----------
 .../src/views/sql-view/sql-control/sql-control.tsx | 385 --------------------
 web-console/src/views/sql-view/sql-view.tsx        | 275 --------------
 .../__snapshots__/tasks-view.spec.tsx.snap         |  57 ++-
 .../src/views/task-view/tasks-view.spec.tsx        |   2 +-
 web-console/src/views/task-view/tasks-view.tsx     |  36 +-
 48 files changed, 2228 insertions(+), 1268 deletions(-)

diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index ce1e783..d422113 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -634,6 +634,11 @@
         "@types/lodash": "*"
       }
     },
+    "@types/memoize-one": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/@types/memoize-one/-/memoize-one-4.1.1.tgz",
+      "integrity": "sha512-+9djKUUn8hOyktLCfCy4hLaIPgDNovaU36fsnZe9trFHr6ddlbIn2q0SEsnkCkNR+pBWEU440Molz/+Mpyf+gQ=="
+    },
     "@types/minimatch": {
       "version": "3.0.3",
       "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@@ -6995,6 +7000,11 @@
         "p-is-promise": "^2.0.0"
       }
     },
+    "memoize-one": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.0.4.tgz",
+      "integrity": "sha512-P0z5IeAH6qHHGkJIXWw0xC2HNEgkx/9uWWBQw64FJj3/ol14VYdfVGWWr0fXfjhhv3TKVIqUq65os6O4GUNksA=="
+    },
     "memory-fs": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
diff --git a/web-console/package.json b/web-console/package.json
index e6e6b8c..f55ebe9 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -43,6 +43,7 @@
   },
   "dependencies": {
     "@blueprintjs/core": "^3.15.1",
+    "@types/memoize-one": "^4.1.1",
     "axios": "^0.19.0",
     "brace": "^0.11.1",
     "classnames": "^2.2.6",
@@ -54,6 +55,7 @@
     "file-saver": "^2.0.2",
     "hjson": "^3.1.2",
     "lodash.debounce": "^4.0.8",
+    "memoize-one": "^5.0.4",
     "numeral": "^2.0.6",
     "react": "^16.8.6",
     "react-ace": "^7.0.1",
diff --git a/web-console/src/console-application.tsx b/web-console/src/console-application.tsx
index 084fd44..6e7f204 100644
--- a/web-console/src/console-application.tsx
+++ b/web-console/src/console-application.tsx
@@ -23,21 +23,21 @@ import classNames from 'classnames';
 import React from 'react';
 import { HashRouter, Route, Switch } from 'react-router-dom';
 
-import { ExternalLink } from './components/external-link/external-link';
-import { HeaderActiveTab, HeaderBar } from './components/header-bar/header-bar';
-import { Loader } from './components/loader/loader';
+import { ExternalLink, HeaderActiveTab, HeaderBar, Loader } from './components';
 import { AppToaster } from './singletons/toaster';
 import { UrlBaser } from './singletons/url-baser';
 import { QueryManager } from './utils';
 import { DRUID_DOCS_API, DRUID_DOCS_SQL } from './variables';
-import { DatasourcesView } from './views/datasource-view/datasource-view';
-import { HomeView } from './views/home-view/home-view';
-import { LoadDataView } from './views/load-data-view/load-data-view';
-import { LookupsView } from './views/lookups-view/lookups-view';
-import { SegmentsView } from './views/segments-view/segments-view';
-import { ServersView } from './views/servers-view/servers-view';
-import { SqlView } from './views/sql-view/sql-view';
-import { TasksView } from './views/task-view/tasks-view';
+import {
+  DatasourcesView,
+  HomeView,
+  LoadDataView,
+  LookupsView,
+  QueryView,
+  SegmentsView,
+  ServersView,
+  TasksView
+} from './views';
 
 import './console-application.scss';
 
@@ -105,7 +105,7 @@ export class ConsoleApplication extends React.PureComponent<ConsoleApplicationPr
   private openDialog: string | null;
   private datasource: string | null;
   private onlyUnavailable: boolean | null;
-  private initSql: string | null;
+  private initQuery: string | null;
   private middleManager: string | null;
 
   constructor(props: ConsoleApplicationProps, context: any) {
@@ -156,7 +156,7 @@ export class ConsoleApplication extends React.PureComponent<ConsoleApplicationPr
       this.openDialog = null;
       this.datasource = null;
       this.onlyUnavailable = null;
-      this.initSql = null;
+      this.initQuery = null;
       this.middleManager = null;
     }, 50);
   }
@@ -188,8 +188,8 @@ export class ConsoleApplication extends React.PureComponent<ConsoleApplicationPr
     this.resetInitialsWithDelay();
   }
 
-  private goToSql = (initSql: string) => {
-    this.initSql = initSql;
+  private goToQuery = (initQuery: string) => {
+    this.initQuery = initQuery;
     window.location.hash = 'query';
     this.resetInitialsWithDelay();
   }
@@ -213,28 +213,28 @@ export class ConsoleApplication extends React.PureComponent<ConsoleApplicationPr
     return this.wrapInViewContainer('load-data', <LoadDataView initSupervisorId={this.supervisorId} initTaskId={this.taskId} goToTask={this.goToTask}/>, 'narrow-pad');
   }
 
-  private wrappedSqlView = () => {
-    return this.wrapInViewContainer('query', <SqlView initSql={this.initSql}/>);
+  private wrappedQueryView = () => {
+    return this.wrapInViewContainer('query', <QueryView initQuery={this.initQuery}/>);
   }
 
   private wrappedDatasourcesView = () => {
     const { noSqlMode } = this.state;
-    return this.wrapInViewContainer('datasources', <DatasourcesView goToSql={this.goToSql} goToSegments={this.goToSegments} noSqlMode={noSqlMode}/>);
+    return this.wrapInViewContainer('datasources', <DatasourcesView goToQuery={this.goToQuery} goToSegments={this.goToSegments} noSqlMode={noSqlMode}/>);
   }
 
   private wrappedSegmentsView = () => {
     const { noSqlMode } = this.state;
-    return this.wrapInViewContainer('segments', <SegmentsView datasource={this.datasource} onlyUnavailable={this.onlyUnavailable} goToSql={this.goToSql} noSqlMode={noSqlMode}/>);
+    return this.wrapInViewContainer('segments', <SegmentsView datasource={this.datasource} onlyUnavailable={this.onlyUnavailable} goToQuery={this.goToQuery} noSqlMode={noSqlMode}/>);
   }
 
   private wrappedTasksView = () => {
     const { noSqlMode } = this.state;
-    return this.wrapInViewContainer('tasks', <TasksView taskId={this.taskId} openDialog={this.openDialog} goToSql={this.goToSql} goToMiddleManager={this.goToMiddleManager} goToLoadDataView={this.goToLoadDataView} noSqlMode={noSqlMode}/>);
+    return this.wrapInViewContainer('tasks', <TasksView taskId={this.taskId} openDialog={this.openDialog} goToQuery={this.goToQuery} goToMiddleManager={this.goToMiddleManager} goToLoadDataView={this.goToLoadDataView} noSqlMode={noSqlMode}/>);
   }
 
   private wrappedServersView = () => {
     const { noSqlMode } = this.state;
-    return this.wrapInViewContainer('servers', <ServersView middleManager={this.middleManager} goToSql={this.goToSql} goToTask={this.goToTask} noSqlMode={noSqlMode}/>);
+    return this.wrapInViewContainer('servers', <ServersView middleManager={this.middleManager} goToQuery={this.goToQuery} goToTask={this.goToTask} noSqlMode={noSqlMode}/>);
   }
 
   private wrappedLookupsView = () => {
@@ -257,8 +257,7 @@ export class ConsoleApplication extends React.PureComponent<ConsoleApplicationPr
       <div className="console-application">
         <Switch>
           <Route path="/load-data" component={this.wrappedLoadDataView}/>
-          <Route path="/query" component={this.wrappedSqlView}/>
-          <Route path="/sql" component={this.wrappedSqlView}/>
+          <Route path="/query" component={this.wrappedQueryView}/>
 
           <Route path="/datasources" component={this.wrappedDatasourcesView}/>
           <Route path="/segments" component={this.wrappedSegmentsView}/>
diff --git a/web-console/src/dialogs/snitch-dialog/snitch-dialog.tsx b/web-console/src/dialogs/snitch-dialog/snitch-dialog.tsx
index cf6240a..5451ec0 100644
--- a/web-console/src/dialogs/snitch-dialog/snitch-dialog.tsx
+++ b/web-console/src/dialogs/snitch-dialog/snitch-dialog.tsx
@@ -26,7 +26,7 @@ import {
   Intent
 } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
-import classNames = require('classnames');
+import classNames from 'classnames';
 import React from 'react';
 
 import { HistoryDialog } from '../history-dialog/history-dialog';
diff --git a/web-console/src/views/index.ts b/web-console/src/utils/column-metadata.ts
similarity index 69%
copy from web-console/src/views/index.ts
copy to web-console/src/utils/column-metadata.ts
index 7a753bb..2902796 100644
--- a/web-console/src/views/index.ts
+++ b/web-console/src/utils/column-metadata.ts
@@ -15,11 +15,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-export * from './datasource-view/datasource-view';
-export * from './home-view/home-view';
-export * from './load-data-view/load-data-view';
-export * from './lookups-view/lookups-view';
-export * from './segments-view/segments-view';
-export * from './servers-view/servers-view';
-export * from './sql-view/sql-view';
-export * from './task-view/tasks-view';
+
+export interface ColumnMetadata {
+  TABLE_SCHEMA: string;
+  TABLE_NAME: string;
+  COLUMN_NAME: string;
+  DATA_TYPE: string;
+}
diff --git a/web-console/src/utils/druid-query.ts b/web-console/src/utils/druid-query.ts
index 9230d1f..2a780e7 100644
--- a/web-console/src/utils/druid-query.ts
+++ b/web-console/src/utils/druid-query.ts
@@ -61,7 +61,7 @@ export async function queryDruidRune(runeQuery: Record<string, any>): Promise<an
   return runeResultResp.data;
 }
 
-export async function queryDruidSql(sqlQuery: Record<string, any>): Promise<any[]> {
+export async function queryDruidSql<T = any>(sqlQuery: Record<string, any>): Promise<T[]> {
   let sqlResultResp: AxiosResponse<any>;
   try {
     sqlResultResp = await axios.post('/druid/v2/sql', sqlQuery);
diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx
index 94c3427..3a80686 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -135,13 +135,39 @@ function identity(x: any): any {
 
 export function lookupBy<T, Q>(array: T[], keyFn: (x: T, index: number) => string = String, valueFn: (x: T, index: number) => Q = identity): Record<string, Q> {
   const lookup: Record<string, Q> = {};
-  for (let i = 0; i < array.length; i++) {
+  const n = array.length;
+  for (let i = 0; i < n; i++) {
     const a = array[i];
     lookup[keyFn(a, i)] = valueFn(a, i);
   }
   return lookup;
 }
 
+export function mapRecord<T, Q>(record: Record<string, T>, fn: (value: T, key: string) => Q): Record<string, Q> {
+  const newRecord: Record<string, Q> = {};
+  const keys = Object.keys(record);
+  for (const key of keys) {
+    newRecord[key] = fn(record[key], key);
+  }
+  return newRecord;
+}
+
+export function groupBy<T, Q>(array: T[], keyFn: (x: T, index: number) => string, aggregateFn: (xs: T[], key: string) => Q): Q[] {
+  const buckets: Record<string, T[]> = {};
+  const n = array.length;
+  for (let i = 0; i < n; i++) {
+    const value = array[i];
+    const key = keyFn(value, i);
+    buckets[key] = buckets[key] || [];
+    buckets[key].push(value);
+  }
+  return Object.keys(buckets).map(key => aggregateFn(buckets[key], key));
+}
+
+export function uniq(array: string[]): string[] {
+  return Object.keys(lookupBy(array));
+}
+
 export function parseList(list: string): string[] {
   if (!list) return [];
   return list.split(',');
@@ -184,17 +210,6 @@ export function getHeadProp(results: Record<string, any>[], prop: string): any {
 
 // ----------------------------
 
-export function memoize<T, U>(fn: (x: T) => U): (x: T) => U {
-  let lastInput: T;
-  let lastOutput: U;
-  return (x: T) => {
-    if (x === lastInput) return lastOutput;
-    lastInput = x;
-    lastOutput = fn(lastInput);
-    return lastOutput;
-  };
-}
-
 export function parseJson(json: string): any {
   try {
     return JSON.parse(json);
@@ -247,7 +262,7 @@ export function sortWithPrefixSuffix(things: string[], prefix: string[], suffix:
 
 // ----------------------------
 
-export function downloadFile(text: string, type: string, fileName: string): void {
+export function downloadFile(text: string, type: string, filename: string): void {
   let blobType: string = '';
   switch (type) {
     case 'json':
@@ -262,5 +277,5 @@ export function downloadFile(text: string, type: string, fileName: string): void
   const blob = new Blob([text], {
     type: blobType
   });
-  FileSaver.saveAs(blob, fileName);
+  FileSaver.saveAs(blob, filename);
 }
diff --git a/web-console/src/utils/query-context.tsx b/web-console/src/utils/query-context.tsx
new file mode 100644
index 0000000..4d864b4
--- /dev/null
+++ b/web-console/src/utils/query-context.tsx
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { deepDelete, deepSet } from './object-change';
+
+export interface QueryContext {
+  useCache?: boolean | undefined;
+  populateCache?: boolean  | undefined;
+  useApproximateCountDistinct?: boolean | undefined;
+  useApproximateTopN?: boolean | undefined;
+}
+
+export function isEmptyContext(context: QueryContext): boolean {
+  return Object.keys(context).length === 0;
+}
+
+// -----------------------------
+
+export function getUseCache(context: QueryContext): boolean {
+  const { useCache } = context;
+  return typeof useCache === 'boolean' ? useCache : true;
+}
+
+export function setUseCache(context: QueryContext, useCache: boolean): QueryContext {
+  let newContext = context;
+  if (useCache) {
+    newContext = deepDelete(newContext, 'useCache');
+    newContext = deepDelete(newContext, 'populateCache');
+  } else {
+    newContext = deepSet(newContext, 'useCache', false);
+    newContext = deepSet(newContext, 'populateCache', false);
+  }
+  return newContext;
+}
+
+// -----------------------------
+
+export function getUseApproximateCountDistinct(context: QueryContext): boolean {
+  const { useApproximateCountDistinct } = context;
+  return typeof useApproximateCountDistinct === 'boolean' ? useApproximateCountDistinct : true;
+}
+
+export function setUseApproximateCountDistinct(context: QueryContext, useApproximateCountDistinct: boolean): QueryContext {
+  if (useApproximateCountDistinct) {
+    return deepDelete(context, 'useApproximateCountDistinct');
+  } else {
+    return deepSet(context, 'useApproximateCountDistinct', false);
+  }
+}
+
+// -----------------------------
+
+export function getUseApproximateTopN(context: QueryContext): boolean {
+  const { useApproximateTopN } = context;
+  return typeof useApproximateTopN === 'boolean' ? useApproximateTopN : true;
+}
+
+export function setUseApproximateTopN(context: QueryContext, useApproximateTopN: boolean): QueryContext {
+  if (useApproximateTopN) {
+    return deepDelete(context, 'useApproximateTopN');
+  } else {
+    return deepSet(context, 'useApproximateTopN', false);
+  }
+}
diff --git a/web-console/src/utils/query-manager.tsx b/web-console/src/utils/query-manager.tsx
index 7dd347d..d545bd5 100644
--- a/web-console/src/utils/query-manager.tsx
+++ b/web-console/src/utils/query-manager.tsx
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-import debounce = require('lodash.debounce');
+import debounce from 'lodash.debounce';
 
 export interface QueryStateInt<R> {
   result: R | null;
diff --git a/web-console/src/views/datasource-view/datasource-view.spec.tsx b/web-console/src/views/datasource-view/datasource-view.spec.tsx
index 0b87da3..ed645b0 100644
--- a/web-console/src/views/datasource-view/datasource-view.spec.tsx
+++ b/web-console/src/views/datasource-view/datasource-view.spec.tsx
@@ -25,7 +25,7 @@ describe('data source view', () => {
   it('matches snapshot', () => {
     const dataSourceView = shallow(
       <DatasourcesView
-        goToSql={(initSql: string) => {}}
+        goToQuery={(initSql: string) => {}}
         goToSegments={(datasource: string, onlyUnavailable?: boolean) => {}}
         noSqlMode={false}
       />);
diff --git a/web-console/src/views/datasource-view/datasource-view.tsx b/web-console/src/views/datasource-view/datasource-view.tsx
index dc96eeb..21b7803 100644
--- a/web-console/src/views/datasource-view/datasource-view.tsx
+++ b/web-console/src/views/datasource-view/datasource-view.tsx
@@ -45,7 +45,7 @@ const tableColumns: string[] = ['Datasource', 'Availability', 'Retention', 'Comp
 const tableColumnsNoSql: string[] = ['Datasource', 'Availability', 'Retention', 'Compaction', 'Size', ActionCell.COLUMN_LABEL];
 
 export interface DatasourcesViewProps extends React.Props<any> {
-  goToSql: (initSql: string) => void;
+  goToQuery: (initSql: string) => void;
   goToSegments: (datasource: string, onlyUnavailable?: boolean) => void;
   noSqlMode: boolean;
 }
@@ -407,7 +407,7 @@ GROUP BY 1`);
   }
 
   getDatasourceActions(datasource: string, disabled: boolean): BasicAction[] {
-    const { goToSql } = this.props;
+    const { goToQuery } = this.props;
 
     if (disabled) {
       return [
@@ -428,7 +428,7 @@ GROUP BY 1`);
         {
           icon: IconNames.APPLICATION,
           title: 'Query with SQL',
-          onAction: () => goToSql(`SELECT * FROM "${datasource}"`)
+          onAction: () => goToQuery(`SELECT * FROM "${datasource}"`)
         },
         {
           icon: IconNames.EXPORT,
@@ -646,7 +646,7 @@ GROUP BY 1`);
   }
 
   render() {
-    const { goToSql, noSqlMode } = this.props;
+    const { goToQuery, noSqlMode } = this.props;
     const { showDisabled } = this.state;
     const { tableColumnSelectionHandler } = this;
 
@@ -662,7 +662,7 @@ GROUP BY 1`);
           <Button
             icon={IconNames.APPLICATION}
             text="Go to SQL"
-            onClick={() => goToSql(this.datasourceQueryManager.getLastQuery())}
+            onClick={() => goToQuery(this.datasourceQueryManager.getLastQuery())}
           />
         }
         <Switch
diff --git a/web-console/src/views/index.ts b/web-console/src/views/index.ts
index 7a753bb..0a4a0bb 100644
--- a/web-console/src/views/index.ts
+++ b/web-console/src/views/index.ts
@@ -21,5 +21,5 @@ export * from './load-data-view/load-data-view';
 export * from './lookups-view/lookups-view';
 export * from './segments-view/segments-view';
 export * from './servers-view/servers-view';
-export * from './sql-view/sql-view';
+export * from './query-view/query-view';
 export * from './task-view/tasks-view';
diff --git a/web-console/src/views/load-data-view/load-data-view.tsx b/web-console/src/views/load-data-view/load-data-view.tsx
index b9aee95..68fdf89 100644
--- a/web-console/src/views/load-data-view/load-data-view.tsx
+++ b/web-console/src/views/load-data-view/load-data-view.tsx
@@ -28,9 +28,10 @@ import {
 import { IconNames } from '@blueprintjs/icons';
 import axios from 'axios';
 import classNames from 'classnames';
+import memoize from 'memoize-one';
 import React from 'react';
 
-import { AutoForm, CenterMessage, ClearableInput, ExternalLink, JSONInput, Loader, TableCell } from '../../components';
+import { AutoForm, CenterMessage, ClearableInput, ExternalLink, JSONInput, Loader } from '../../components';
 import { AsyncActionDialog } from '../../dialogs';
 import { AppToaster } from '../../singletons/toaster';
 import {
@@ -38,7 +39,7 @@ import {
   getDruidErrorMessage,
   localStorageGet,
   LocalStorageKeys,
-  localStorageSet, memoize, parseJson,
+  localStorageSet, parseJson,
   QueryState
 } from '../../utils';
 import { possibleDruidFormatForValues } from '../../utils/druid-time';
@@ -365,6 +366,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
       {this.renderResetConfirm()}
     </div>;
   }
+
   renderStepNav() {
     const { stage } = this.state;
 
@@ -381,7 +383,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
                 key={s}
                 active={s === stage}
                 onClick={() => this.updateStage(s)}
-                icon={s === 'json-spec' && IconNames.EYE_OPEN}
+                icon={s === 'json-spec' && IconNames.MANUALLY_ENTERED_DATA}
                 text={VIEW_TITLE[s]}
               />
             ))}
@@ -401,13 +403,14 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
         onPrevStage &&
         <Button
           className="prev"
-          icon={IconNames.ARROW_LEFT}
+          icon={IconNames.UNDO}
           text={prevLabel}
           onClick={onPrevStage}
         />
       }
       <Button
         text={`Next: ${VIEW_TITLE[nextStage]}`}
+        rightIcon={IconNames.ARROW_RIGHT}
         intent={Intent.PRIMARY}
         disabled={disabled}
         onClick={() => {
@@ -1294,7 +1297,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
     });
   }
 
-  private getMemoizedDimensionFiltersFromSpec = memoize<IngestionSpec, DruidFilter[]>((spec) => {
+  private getMemoizedDimensionFiltersFromSpec = memoize((spec) => {
     const { dimensionFilters } = splitFilter(deepGet(spec, 'dataSchema.transformSpec.filter'));
     return dimensionFilters;
   });
diff --git a/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap b/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap
new file mode 100644
index 0000000..3e4cfff
--- /dev/null
+++ b/web-console/src/views/query-view/__snapshots__/query-view.spec.tsx.snap
@@ -0,0 +1,52 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`sql view matches snapshot 1`] = `
+<div
+  className="query-view app-view"
+>
+  <ColumnTree
+    columnMetadata={null}
+    columnMetadataLoading={true}
+    onQueryStringChange={[Function]}
+  />
+  <t
+    customClassName=""
+    onDragEnd={null}
+    onDragStart={null}
+    onSecondaryPaneSizeChange={[Function]}
+    percentage={true}
+    primaryIndex={0}
+    primaryMinSize={30}
+    secondaryInitialSize={60}
+    secondaryMinSize={30}
+    vertical={true}
+  >
+    <div
+      className="control-pane"
+    >
+      <QueryInput
+        columnMetadata={null}
+        onQueryStringChange={[Function]}
+        queryString="test"
+        runeMode={false}
+      />
+      <div
+        className="control-bar"
+      >
+        <HotkeysTarget(RunButton)
+          onExplain={[Function]}
+          onQueryContextChange={[Function]}
+          onRun={[Function]}
+          queryContext={Object {}}
+          runeMode={false}
+        />
+      </div>
+    </div>
+    <QueryOutput
+      error={null}
+      loading={false}
+      result={null}
+    />
+  </t>
+</div>
+`;
diff --git a/web-console/src/views/query-view/column-tree/__snapshots__/column-tree.spec.tsx.snap b/web-console/src/views/query-view/column-tree/__snapshots__/column-tree.spec.tsx.snap
new file mode 100644
index 0000000..35754d9
--- /dev/null
+++ b/web-console/src/views/query-view/column-tree/__snapshots__/column-tree.spec.tsx.snap
@@ -0,0 +1,115 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`column tree matches snapshot 1`] = `
+<div
+  class="column-tree"
+>
+  <div
+    class="bp3-html-select bp3-fill bp3-large bp3-minimal schema-selector"
+  >
+    <select>
+      <option
+        value="0"
+      >
+        druid
+      </option>
+      <option
+        value="1"
+      >
+        sys
+      </option>
+    </select>
+    <span
+      class="bp3-icon bp3-icon-double-caret-vertical"
+      icon="double-caret-vertical"
+    >
+      <svg
+        data-icon="double-caret-vertical"
+        height="16"
+        viewBox="0 0 16 16"
+        width="16"
+      >
+        <desc>
+          double-caret-vertical
+        </desc>
+        <path
+          d="M5 7h6a1.003 1.003 0 0 0 .71-1.71l-3-3C8.53 2.11 8.28 2 8 2s-.53.11-.71.29l-3 3A1.003 1.003 0 0 0 5 7zm6 2H5a1.003 1.003 0 0 0-.71 1.71l3 3c.18.18.43.29.71.29s.53-.11.71-.29l3-3A1.003 1.003 0 0 0 11 9z"
+          fill-rule="evenodd"
+        />
+      </svg>
+    </span>
+  </div>
+  <div
+    class="tree-container"
+  >
+    <div
+      class="bp3-tree"
+    >
+      <ul
+        class="bp3-tree-node-list bp3-tree-root"
+      >
+        <li
+          class="bp3-tree-node"
+        >
+          <div
+            class="bp3-tree-node-content bp3-tree-node-content-0"
+          >
+            <span
+              class="bp3-icon bp3-icon-chevron-right bp3-tree-node-caret bp3-tree-node-caret-closed"
+              icon="chevron-right"
+            >
+              <svg
+                data-icon="chevron-right"
+                height="16"
+                viewBox="0 0 16 16"
+                width="16"
+              >
+                <desc>
+                  chevron-right
+                </desc>
+                <path
+                  d="M10.71 7.29l-4-4a1.003 1.003 0 0 0-1.42 1.42L8.59 8 5.3 11.29c-.19.18-.3.43-.3.71a1.003 1.003 0 0 0 1.71.71l4-4c.18-.18.29-.43.29-.71 0-.28-.11-.53-.29-.71z"
+                  fill-rule="evenodd"
+                />
+              </svg>
+            </span>
+            <span
+              class="bp3-icon bp3-icon-th bp3-tree-node-icon"
+              icon="th"
+            >
+              <svg
+                data-icon="th"
+                height="16"
+                viewBox="0 0 16 16"
+                width="16"
+              >
+                <desc>
+                  th
+                </desc>
+                <path
+                  d="M15 1H1c-.6 0-1 .5-1 1v12c0 .6.4 1 1 1h14c.6 0 1-.4 1-1V2c0-.5-.4-1-1-1zM6 13H2v-2h4v2zm0-3H2V8h4v2zm0-3H2V5h4v2zm8 6H7v-2h7v2zm0-3H7V8h7v2zm0-3H7V5h7v2z"
+                  fill-rule="evenodd"
+                />
+              </svg>
+            </span>
+            <span
+              class="bp3-tree-node-label"
+            >
+              deletion-tutorial
+            </span>
+          </div>
+          <div
+            class="bp3-collapse"
+          >
+            <div
+              aria-hidden="false"
+              class="bp3-collapse-body"
+              style="transform: translateY(-0px);"
+            />
+          </div>
+        </li>
+      </ul>
+    </div>
+  </div>
+</div>
+`;
diff --git a/web-console/src/views/sql-view/sql-view.scss b/web-console/src/views/query-view/column-tree/column-tree.scss
similarity index 67%
copy from web-console/src/views/sql-view/sql-view.scss
copy to web-console/src/views/query-view/column-tree/column-tree.scss
index f1ef90b..0b121a6 100644
--- a/web-console/src/views/sql-view/sql-view.scss
+++ b/web-console/src/views/query-view/column-tree/column-tree.scss
@@ -16,42 +16,28 @@
  * limitations under the License.
  */
 
-@import "../../variables";
+.column-tree {
+  background: #27313b;
 
-.sql-view {
-  height: 100%;
-  width: 100%;
-
-  .top-pane {
+  .schema-selector {
     position: absolute;
-    width: 100%;
     top: 0;
-    bottom: 10px;
 
-    .sql-control {
-      position: absolute;
-      width: 100%;
-      height: 100%;
+    select {
+      border-bottom-right-radius: 0;
+      border-bottom-left-radius: 0;
     }
   }
 
-  &.splitter-layout.splitter-layout-vertical > .layout-splitter {
-    height: 3px;
-    background-color: #6d8ea9;
-  }
-
-  .bottom-pane {
+  .tree-container {
     position: absolute;
-    width: 100%;
-    top: 10px;
+    top: 40px;
     bottom: 0;
+    width: 100%;
+    overflow: auto;
 
-    .ReactTable {
-      position: absolute;
-      top: 0;
-      bottom: 0;
-      width: 100%;
+    .bp3-tree-node-content-1 {
+      padding-left: 0;
     }
   }
 }
-
diff --git a/web-console/src/views/sql-view/sql-control/sql-control.spec.tsx b/web-console/src/views/query-view/column-tree/column-tree.spec.tsx
similarity index 58%
copy from web-console/src/views/sql-view/sql-control/sql-control.spec.tsx
copy to web-console/src/views/query-view/column-tree/column-tree.spec.tsx
index a044aa4..53505bd 100644
--- a/web-console/src/views/sql-view/sql-control/sql-control.spec.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree.spec.tsx
@@ -19,19 +19,23 @@
 import React from 'react';
 import { render } from 'react-testing-library';
 
-import { SqlControl } from './sql-control';
+import { ColumnMetadata } from '../../../utils/column-metadata';
 
-describe('sql control', () => {
+import { ColumnTree } from './column-tree';
+
+describe('column tree', () => {
   it('matches snapshot', () => {
-    const sqlControl = <SqlControl
-      initSql={'test'}
-      onRun={(query, context, wrapQuery) => {}}
-      onExplain={(sqlQuery, context) => {}}
-      queryElapsed={2}
-      onDownload={() => {}}
+    const columnTree = <ColumnTree
+      columnMetadataLoading={false}
+      columnMetadata={[
+        {'TABLE_SCHEMA': 'druid', 'TABLE_NAME': 'deletion-tutorial', 'COLUMN_NAME': '__time', 'DATA_TYPE': 'TIMESTAMP'},
+        {'TABLE_SCHEMA': 'druid', 'TABLE_NAME': 'deletion-tutorial', 'COLUMN_NAME': 'added', 'DATA_TYPE': 'BIGINT'},
+        {'TABLE_SCHEMA': 'sys', 'TABLE_NAME': 'tasks', 'COLUMN_NAME': 'error_msg', 'DATA_TYPE': 'VARCHAR'}
+      ] as ColumnMetadata[]}
+      onQueryStringChange={() => null}
     />;
 
-    const { container } = render(sqlControl);
+    const { container } = render(columnTree);
     expect(container.firstChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/views/query-view/column-tree/column-tree.tsx b/web-console/src/views/query-view/column-tree/column-tree.tsx
new file mode 100644
index 0000000..b3b2213
--- /dev/null
+++ b/web-console/src/views/query-view/column-tree/column-tree.tsx
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { HTMLSelect, IconName, ITreeNode, Tree } from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
+import React, { ChangeEvent } from 'react';
+
+import { Loader } from '../../../components';
+import { groupBy } from '../../../utils';
+import { ColumnMetadata } from '../../../utils/column-metadata';
+
+import './column-tree.scss';
+
+export interface ColumnTreeProps extends React.Props<any> {
+  columnMetadataLoading: boolean;
+  columnMetadata: ColumnMetadata[] | null;
+  onQueryStringChange: (queryString: string) => void;
+}
+
+export interface ColumnTreeState {
+  prevColumnMetadata: ColumnMetadata[] | null;
+  columnTree: ITreeNode[] | null;
+  selectedTreeIndex: number;
+}
+
+export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeState> {
+
+  static getDerivedStateFromProps(props: ColumnTreeProps, state: ColumnTreeState) {
+    const { columnMetadata } = props;
+
+    if (columnMetadata && columnMetadata !== state.prevColumnMetadata) {
+      return {
+        prevColumnMetadata: columnMetadata,
+        columnTree: groupBy(
+          columnMetadata,
+          (r) => r.TABLE_SCHEMA,
+          (metadata, schema): ITreeNode => ({
+            id: schema,
+            label: schema,
+            childNodes: groupBy(
+              metadata,
+              (r) => r.TABLE_NAME,
+              (metadata, table) => ({
+                id: table,
+                icon: IconNames.TH,
+                label: table,
+                childNodes: metadata.map(columnData => ({
+                  id: columnData.COLUMN_NAME,
+                  icon: ColumnTree.dataTypeToIcon(columnData.DATA_TYPE),
+                  label: columnData.COLUMN_NAME
+                }))
+              })
+            )
+          })
+        )
+      };
+    }
+    return null;
+  }
+
+  static dataTypeToIcon(dataType: string): IconName {
+    switch (dataType) {
+      case 'TIMESTAMP': return IconNames.TIME;
+      case 'VARCHAR': return IconNames.FONT;
+      case 'BIGINT': return IconNames.NUMERICAL;
+      default: return IconNames.HELP;
+    }
+  }
+
+  constructor(props: ColumnTreeProps, context: any) {
+    super(props, context);
+    this.state = {
+      prevColumnMetadata: null,
+      columnTree: null,
+      selectedTreeIndex: 0
+    };
+  }
+
+  renderSchemaSelector() {
+    const { columnTree, selectedTreeIndex } = this.state;
+    if (!columnTree) return null;
+
+    return <HTMLSelect
+      className="schema-selector"
+      value={selectedTreeIndex}
+      onChange={this.handleSchemaSelectorChange}
+      fill
+      minimal
+      large
+    >
+      {columnTree.map((treeNode, i) => (
+        <option key={i} value={i}>
+          {treeNode.label}
+        </option>
+      ))}
+    </HTMLSelect>;
+  }
+
+  private handleSchemaSelectorChange = (e: ChangeEvent<HTMLSelectElement>) => {
+    this.setState({ selectedTreeIndex: Number(e.target.value) });
+  }
+
+  render() {
+    const { columnMetadataLoading } = this.props;
+    if (columnMetadataLoading) {
+      return <div className="column-tree">
+        <Loader loading/>
+      </div>;
+    }
+
+    const { columnTree, selectedTreeIndex } = this.state;
+    if (!columnTree) return null;
+    const currentSchemaSubtree = columnTree[selectedTreeIndex].childNodes;
+    if (!currentSchemaSubtree) return null;
+
+    return <div className="column-tree">
+      {this.renderSchemaSelector()}
+      <div className="tree-container">
+        <Tree
+          contents={currentSchemaSubtree}
+          onNodeClick={this.handleNodeClick}
+          onNodeCollapse={this.handleNodeCollapse}
+          onNodeExpand={this.handleNodeExpand}
+        />
+      </div>
+    </div>;
+  }
+
+  private handleNodeClick = (nodeData: ITreeNode, nodePath: number[], e: React.MouseEvent<HTMLElement>) => {
+    const { onQueryStringChange } = this.props;
+    const { columnTree, selectedTreeIndex } = this.state;
+    if (!columnTree) return;
+
+    switch (nodePath.length) {
+      case 1: // Datasource
+        const tableSchema = columnTree[selectedTreeIndex].label;
+        if (tableSchema === 'druid') {
+          onQueryStringChange(`SELECT * FROM "${nodeData.label}"
+WHERE "__time" >= CURRENT_TIMESTAMP - INTERVAL '1' DAY`);
+        } else {
+          onQueryStringChange(`SELECT * FROM ${tableSchema}.${nodeData.label}`);
+        }
+        break;
+
+      case 2: // Column
+        const schemaNode = columnTree[selectedTreeIndex];
+        const columnSchema = schemaNode.label;
+        const columnTable = schemaNode.childNodes ? schemaNode.childNodes[nodePath[0]].label : '?';
+        if (columnSchema === 'druid') {
+          if (nodeData.icon === IconNames.TIME) {
+            onQueryStringChange(`SELECT TIME_FLOOR("${nodeData.label}", 'PT1H') AS "Time", COUNT(*) AS "Count"
+FROM "${columnTable}"
+WHERE "__time" >= CURRENT_TIMESTAMP - INTERVAL '1' DAY
+GROUP BY 1
+ORDER BY "Time" ASC`);
+          } else {
+            onQueryStringChange(`SELECT "${nodeData.label}", COUNT(*) AS "Count"
+FROM "${columnTable}"
+WHERE "__time" >= CURRENT_TIMESTAMP - INTERVAL '1' DAY
+GROUP BY 1
+ORDER BY "Count" DESC`);
+          }
+        } else {
+          onQueryStringChange(`SELECT "${nodeData.label}", COUNT(*) AS "Count"
+FROM ${columnSchema}.${columnTable}
+GROUP BY 1
+ORDER BY "Count" DESC`);
+        }
+        break;
+    }
+  }
+
+  private handleNodeCollapse = (nodeData: ITreeNode) => {
+    nodeData.isExpanded = false;
+    this.bounceState();
+  }
+
+  private handleNodeExpand = (nodeData: ITreeNode) => {
+    nodeData.isExpanded = true;
+    this.bounceState();
+  }
+
+  bounceState() {
+    const { columnTree } = this.state;
+    if (!columnTree) return;
+    this.setState({ columnTree: columnTree.slice() });
+  }
+}
diff --git a/web-console/src/views/query-view/query-extra-info/__snapshots__/query-extra-info.spec.tsx.snap b/web-console/src/views/query-view/query-extra-info/__snapshots__/query-extra-info.spec.tsx.snap
new file mode 100644
index 0000000..9aacc34
--- /dev/null
+++ b/web-console/src/views/query-view/query-extra-info/__snapshots__/query-extra-info.spec.tsx.snap
@@ -0,0 +1,58 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`query extra info matches snapshot 1`] = `
+<div
+  class="query-extra-info"
+>
+  <div
+    class="query-info"
+  >
+    <span
+      class="bp3-popover-wrapper"
+    >
+      <span
+        class="bp3-popover-target"
+      >
+        <span
+          class=""
+          tabindex="0"
+        >
+          1999+ results in 8.00s
+        </span>
+      </span>
+    </span>
+  </div>
+  <span
+    class="bp3-popover-wrapper download-button"
+  >
+    <span
+      class="bp3-popover-target"
+    >
+      <button
+        class="bp3-button bp3-minimal"
+        type="button"
+      >
+        <span
+          class="bp3-icon bp3-icon-download"
+          icon="download"
+        >
+          <svg
+            data-icon="download"
+            height="16"
+            viewBox="0 0 16 16"
+            width="16"
+          >
+            <desc>
+              download
+            </desc>
+            <path
+              d="M7.99-.01c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zM11.7 9.7l-3 3c-.18.18-.43.29-.71.29s-.53-.11-.71-.29l-3-3A1.003 1.003 0 0 1 5.7 8.28l1.29 1.29V3.99c0-.55.45-1 1-1s1 .45 1 1v5.59l1.29-1.29a1.003 1.003 0 0 1 1.71.71c0 .27-.11.52-.29.7z"
+              fill-rule="evenodd"
+            />
+          </svg>
+        </span>
+      </button>
+    </span>
+  </span>
+</div>
+`;
diff --git a/web-console/src/views/index.ts b/web-console/src/views/query-view/query-extra-info/query-extra-info.scss
similarity index 69%
copy from web-console/src/views/index.ts
copy to web-console/src/views/query-view/query-extra-info/query-extra-info.scss
index 7a753bb..3b16a5d 100644
--- a/web-console/src/views/index.ts
+++ b/web-console/src/views/query-view/query-extra-info/query-extra-info.scss
@@ -15,11 +15,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-export * from './datasource-view/datasource-view';
-export * from './home-view/home-view';
-export * from './load-data-view/load-data-view';
-export * from './lookups-view/lookups-view';
-export * from './segments-view/segments-view';
-export * from './servers-view/servers-view';
-export * from './sql-view/sql-view';
-export * from './task-view/tasks-view';
+
+.query-extra-info {
+  & > * {
+    display: inline-block;
+  }
+
+  .query-info {
+    line-height: 30px;
+    white-space: nowrap;
+    cursor: pointer;
+  }
+}
diff --git a/web-console/src/views/sql-view/sql-control/sql-control.spec.tsx b/web-console/src/views/query-view/query-extra-info/query-extra-info.spec.tsx
similarity index 66%
copy from web-console/src/views/sql-view/sql-control/sql-control.spec.tsx
copy to web-console/src/views/query-view/query-extra-info/query-extra-info.spec.tsx
index a044aa4..8fa03a9 100644
--- a/web-console/src/views/sql-view/sql-control/sql-control.spec.tsx
+++ b/web-console/src/views/query-view/query-extra-info/query-extra-info.spec.tsx
@@ -19,19 +19,23 @@
 import React from 'react';
 import { render } from 'react-testing-library';
 
-import { SqlControl } from './sql-control';
+import { QueryExtraInfo } from './query-extra-info';
 
-describe('sql control', () => {
+describe('query extra info', () => {
   it('matches snapshot', () => {
-    const sqlControl = <SqlControl
-      initSql={'test'}
-      onRun={(query, context, wrapQuery) => {}}
-      onExplain={(sqlQuery, context) => {}}
-      queryElapsed={2}
-      onDownload={() => {}}
+    const queryExtraInfo = <QueryExtraInfo
+      queryExtraInfo={{
+        queryId: 'e3ee781b-c0b6-4385-9d99-a8a1994bebac',
+        sqlQueryId: null,
+        startTime: new Date('1986-04-26T01:23:40+03:00'),
+        endTime: new Date('1986-04-26T01:23:48+03:00'),
+        numResults: 2000,
+        wrappedLimit: 2000
+      }}
+      onDownload={() => null}
     />;
 
-    const { container } = render(sqlControl);
+    const { container } = render(queryExtraInfo);
     expect(container.firstChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/views/query-view/query-extra-info/query-extra-info.tsx b/web-console/src/views/query-view/query-extra-info/query-extra-info.tsx
new file mode 100644
index 0000000..0246b04
--- /dev/null
+++ b/web-console/src/views/query-view/query-extra-info/query-extra-info.tsx
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Button, Intent, Menu, MenuDivider, MenuItem, Popover, Position, Tooltip } from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
+import copy from 'copy-to-clipboard';
+import React from 'react';
+
+import { AppToaster } from '../../../singletons/toaster';
+import { pluralIfNeeded } from '../../../utils';
+
+import './query-extra-info.scss';
+
+export interface QueryExtraInfoData {
+  queryId: string | null;
+  sqlQueryId: string | null;
+  startTime: Date;
+  endTime: Date;
+  numResults: number;
+  wrappedLimit?: number;
+}
+
+export interface QueryExtraInfoProps extends React.Props<any> {
+  queryExtraInfo: QueryExtraInfoData;
+  onDownload: (filename: string, format: string) => void;
+}
+
+export class QueryExtraInfo extends React.PureComponent<QueryExtraInfoProps> {
+
+  render() {
+    const { queryExtraInfo } = this.props;
+
+    const downloadMenu = <Menu className="download-format-menu">
+      <MenuDivider title="Download as:"/>
+      <MenuItem text="CSV" onClick={() => this.handleDownload('csv')} />
+      <MenuItem text="TSV" onClick={() => this.handleDownload('tsv')} />
+      <MenuItem text="JSON (new line delimited)" onClick={() => this.handleDownload('json')}/>
+    </Menu>;
+
+    let resultCount: string;
+    if (queryExtraInfo.wrappedLimit && queryExtraInfo.numResults === queryExtraInfo.wrappedLimit) {
+      resultCount = `${queryExtraInfo.numResults - 1}+ results`;
+    } else {
+      resultCount = pluralIfNeeded(queryExtraInfo.numResults, 'result');
+    }
+
+    const elapsed = queryExtraInfo.endTime.valueOf() - queryExtraInfo.startTime.valueOf();
+
+    let tooltipContent: JSX.Element | undefined;
+    if (queryExtraInfo.queryId) {
+      tooltipContent = <>Query ID: <strong>{queryExtraInfo.queryId}</strong> (click to copy)</>;
+    } else if (queryExtraInfo.sqlQueryId) {
+      tooltipContent = <>SQL query ID: <strong>{queryExtraInfo.sqlQueryId}</strong> (click to copy)</>;
+    }
+
+    return <div className="query-extra-info">
+      <div className="query-info" onClick={this.handleQueryInfoClick}>
+        <Tooltip content={tooltipContent} hoverOpenDelay={500}>
+          {`${resultCount} in ${(elapsed / 1000).toFixed(2)}s`}
+        </Tooltip>
+      </div>
+      <Popover className="download-button" content={downloadMenu} position={Position.BOTTOM_RIGHT}>
+        <Button
+          icon={IconNames.DOWNLOAD}
+          minimal
+        />
+      </Popover>
+    </div>;
+  }
+
+  private handleQueryInfoClick = () => {
+    const { queryExtraInfo } = this.props;
+    const id = queryExtraInfo.queryId || queryExtraInfo.sqlQueryId;
+    if (!id) return;
+
+    copy(id, { format: 'text/plain' });
+    AppToaster.show({
+      message: 'Query ID copied to clipboard',
+      intent: Intent.SUCCESS
+    });
+  }
+
+  private handleDownload = (format: string) => {
+    const { queryExtraInfo, onDownload } = this.props;
+    const id = queryExtraInfo.queryId || queryExtraInfo.sqlQueryId;
+    if (!id) return;
+
+    onDownload(`query-${id}.${format}`, format);
+  }
+}
diff --git a/web-console/src/views/query-view/query-input/__snapshots__/query-input.spec.tsx.snap b/web-console/src/views/query-view/query-input/__snapshots__/query-input.spec.tsx.snap
new file mode 100644
index 0000000..e2d965d
--- /dev/null
+++ b/web-console/src/views/query-view/query-input/__snapshots__/query-input.spec.tsx.snap
@@ -0,0 +1,100 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`query input matches snapshot 1`] = `
+<div
+  class="query-input"
+>
+  <div
+    class="ace-container"
+  >
+    <div
+      class=" ace_editor ace-tm ace_focus"
+      id="ace-editor"
+      style="width: 100%; height: 200px; font-size: 14px;"
+    >
+      <textarea
+        autocapitalize="off"
+        autocorrect="off"
+        class="ace_text-input"
+        spellcheck="false"
+        style="opacity: 0; position: fixed; top: 0px;"
+        wrap="off"
+      />
+      <div
+        aria-hidden="true"
+        class="ace_gutter"
+      >
+        <div
+          class="ace_layer ace_gutter-layer ace_folding-enabled"
+        />
+        <div
+          class="ace_gutter-active-line"
+        />
+      </div>
+      <div
+        class="ace_scroller"
+      >
+        <div
+          class="ace_content"
+        >
+          <div
+            class="ace_layer ace_print-margin-layer"
+          >
+            <div
+              class="ace_print-margin"
+              style="left: 4px; visibility: hidden;"
+            />
+          </div>
+          <div
+            class="ace_layer ace_marker-layer"
+          />
+          <div
+            class="ace_layer ace_text-layer"
+            style="padding: 0px 4px;"
+          />
+          <div
+            class="ace_layer ace_marker-layer"
+          />
+          <div
+            class="ace_layer ace_cursor-layer"
+          >
+            <div
+              class="ace_cursor"
+            />
+          </div>
+        </div>
+      </div>
+      <div
+        class="ace_scrollbar ace_scrollbar-v"
+        style="display: none; width: 20px;"
+      >
+        <div
+          class="ace_scrollbar-inner"
+          style="width: 20px;"
+        />
+      </div>
+      <div
+        class="ace_scrollbar ace_scrollbar-h"
+        style="display: none; height: 20px;"
+      >
+        <div
+          class="ace_scrollbar-inner"
+          style="height: 20px;"
+        />
+      </div>
+      <div
+        style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: hidden;"
+      >
+        <div
+          style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
+        />
+        <div
+          style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
+        >
+          XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
diff --git a/web-console/src/views/sql-view/sql-control/sql-control.scss b/web-console/src/views/query-view/query-input/query-input.scss
similarity index 76%
rename from web-console/src/views/sql-view/sql-control/sql-control.scss
rename to web-console/src/views/query-view/query-input/query-input.scss
index 7b54424..ec7c3f3 100644
--- a/web-console/src/views/sql-view/sql-control/sql-control.scss
+++ b/web-console/src/views/query-view/query-input/query-input.scss
@@ -18,12 +18,11 @@
 
 @import "../../../variables";
 
-.sql-control {
+.query-input {
   .ace-container {
     position: absolute;
     width: 100%;
-    top: 0;
-    bottom: 30px + $standard-padding;
+    height: 100%;
 
     .ace_scroller {
       background-color: #232C35;
@@ -33,24 +32,6 @@
       background-color: #27313c;
     }
   }
-
-  .buttons {
-    position: absolute;
-    width: 100%;
-    bottom: 0;
-    height: 30px;
-
-    .query-elapsed {
-      padding: 5px;
-      position: absolute;
-      right: 25px;
-    }
-
-    .download-button {
-      position: absolute;
-      right: 0px;
-    }
-  }
 }
 
 .ace_tooltip {
@@ -70,7 +51,3 @@
     font-size: 18px;
   }
 }
-
-.bp3-menu.download-format-menu {
-  min-width: 80px;
-}
diff --git a/web-console/src/views/sql-view/sql-control/sql-control.spec.tsx b/web-console/src/views/query-view/query-input/query-input.spec.tsx
similarity index 78%
copy from web-console/src/views/sql-view/sql-control/sql-control.spec.tsx
copy to web-console/src/views/query-view/query-input/query-input.spec.tsx
index a044aa4..55606d3 100644
--- a/web-console/src/views/sql-view/sql-control/sql-control.spec.tsx
+++ b/web-console/src/views/query-view/query-input/query-input.spec.tsx
@@ -19,16 +19,15 @@
 import React from 'react';
 import { render } from 'react-testing-library';
 
-import { SqlControl } from './sql-control';
+import { QueryInput } from './query-input';
 
-describe('sql control', () => {
+describe('query input', () => {
   it('matches snapshot', () => {
-    const sqlControl = <SqlControl
-      initSql={'test'}
-      onRun={(query, context, wrapQuery) => {}}
-      onExplain={(sqlQuery, context) => {}}
-      queryElapsed={2}
-      onDownload={() => {}}
+    const sqlControl = <QueryInput
+      queryString="hello world"
+      onQueryStringChange={() => null}
+      runeMode={false}
+      columnMetadata={null}
     />;
 
     const { container } = render(sqlControl);
diff --git a/web-console/src/views/query-view/query-input/query-input.tsx b/web-console/src/views/query-view/query-input/query-input.tsx
new file mode 100644
index 0000000..ac7ad65
--- /dev/null
+++ b/web-console/src/views/query-view/query-input/query-input.tsx
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { IResizeEntry, ITreeNode, ResizeSensor } from '@blueprintjs/core';
+import ace from 'brace';
+import React from 'react';
+import AceEditor from 'react-ace';
+import ReactDOMServer from 'react-dom/server';
+
+import { SQLFunctionDoc } from '../../../../lib/sql-function-doc';
+import { uniq } from '../../../utils';
+import { ColumnMetadata } from '../../../utils/column-metadata';
+import { ColumnTreeProps, ColumnTreeState } from '../column-tree/column-tree';
+
+import './query-input.scss';
+
+const langTools = ace.acequire('ace/ext/language_tools');
+
+export interface QueryInputProps extends React.Props<any> {
+  queryString: string;
+  onQueryStringChange: (newQueryString: string) => void;
+  runeMode: boolean;
+  columnMetadata: ColumnMetadata[] | null;
+}
+
+export interface QueryInputState {
+  // For reasons (https://github.com/securingsincity/react-ace/issues/415) react ace editor needs an explicit height
+  // Since this component will grown and shrink dynamically we will measure its height and then set it.
+  editorHeight: number;
+  prevColumnMetadata: ColumnMetadata[] | null;
+}
+
+export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputState> {
+
+  static getDerivedStateFromProps(props: ColumnTreeProps, state: ColumnTreeState) {
+    const { columnMetadata } = props;
+
+    if (columnMetadata && columnMetadata !== state.prevColumnMetadata) {
+      const completions = ([] as any[]).concat(
+        uniq(columnMetadata.map(d => d.TABLE_SCHEMA)).map(v => ({ value: v, score: 10, meta: 'schema' })),
+        uniq(columnMetadata.map(d => d.TABLE_NAME)).map(v => ({ value: v, score: 49, meta: 'datasource' })),
+        uniq(columnMetadata.map(d => d.COLUMN_NAME)).map(v => ({ value: v, score: 50, meta: 'column' }))
+      );
+
+      langTools.addCompleter({
+        getCompletions: (editor: any, session: any, pos: any, prefix: any, callback: any) => {
+          callback(null, completions);
+        }
+      });
+
+      return {
+        prevColumnMetadata: columnMetadata
+      };
+    }
+    return null;
+  }
+
+  constructor(props: QueryInputProps, context: any) {
+    super(props, context);
+    this.state = {
+      editorHeight: 200,
+      prevColumnMetadata: null
+    };
+  }
+
+  private replaceDefaultAutoCompleter = () => {
+    if (!langTools) return;
+    /*
+     Please refer to the source code @
+     https://github.com/ajaxorg/ace/blob/9b5b63d1dc7c1b81b58d30c87d14b5905d030ca5/lib/ace/ext/language_tools.js#L41
+     for the implementation of keyword completer
+    */
+    const keywordCompleter = {
+      getCompletions: (editor: any, session: any, pos: any, prefix: any, callback: any) => {
+        if (session.$mode.completer) {
+          return session.$mode.completer.getCompletions(editor, session, pos, prefix, callback);
+        }
+        const state = editor.session.getState(pos.row);
+        let keywordCompletions = session.$mode.getCompletions(state, session, pos, prefix);
+        keywordCompletions = keywordCompletions.map((d: any) => {
+          return Object.assign(d, {name: d.name.toUpperCase(), value: d.value.toUpperCase()});
+        });
+        return callback(null, keywordCompletions);
+      }
+    };
+    langTools.setCompleters([langTools.snippetCompleter, langTools.textCompleter, keywordCompleter]);
+  }
+
+  private addFunctionAutoCompleter = (): void => {
+    if (!langTools) return;
+
+    const functionList: any[] = SQLFunctionDoc.map((entry: any) => {
+      let funcName: string = entry.syntax.replace(/\(.*\)/, '()');
+      if (!funcName.includes('(')) funcName = funcName.substr(0, 10);
+      return {
+        value: funcName,
+        score: 80,
+        meta: 'function',
+        syntax: entry.syntax,
+        description: entry.description,
+        completer: {
+          insertMatch: (editor: any, data: any) => {
+            editor.completer.insertMatch({value: data.caption});
+            const pos = editor.getCursorPosition();
+            editor.gotoLine(pos.row + 1, pos.column - 1);
+          }
+        }
+      };
+    });
+
+    langTools.addCompleter({
+      getCompletions: (editor: any, session: any, pos: any, prefix: any, callback: any) => {
+        callback(null, functionList);
+      },
+      getDocTooltip: (item: any) => {
+        if (item.meta === 'function') {
+          const functionName = item.caption.slice(0, -2);
+          item.docHTML = ReactDOMServer.renderToStaticMarkup((
+            <div className="function-doc">
+              <div className="function-doc-name"><b>{functionName}</b></div>
+              <hr/>
+              <div><b>Syntax:</b></div>
+              <div>{item.syntax}</div>
+              <br/>
+              <div><b>Description:</b></div>
+              <div>{item.description}</div>
+            </div>
+          ));
+        }
+      }
+    });
+  }
+
+  componentDidMount(): void {
+    this.replaceDefaultAutoCompleter();
+    this.addFunctionAutoCompleter();
+  }
+
+  private handleAceContainerResize = (entries: IResizeEntry[]) => {
+    if (entries.length !== 1) return;
+    this.setState({ editorHeight: entries[0].contentRect.height });
+  }
+
+  render() {
+    const { queryString, runeMode, onQueryStringChange } = this.props;
+    const { editorHeight } = this.state;
+
+    // Set the key in the AceEditor to force a rebind and prevent an error that happens otherwise
+    return <div className="query-input">
+      <ResizeSensor onResize={this.handleAceContainerResize}>
+        <div className="ace-container">
+          <AceEditor
+            key={runeMode ? 'hjson' : 'sql'}
+            mode={runeMode ? 'hjson' : 'sql'}
+            theme="solarized_dark"
+            name="ace-editor"
+            onChange={onQueryStringChange}
+            focus
+            fontSize={14}
+            width="100%"
+            height={`${editorHeight}px`}
+            showPrintMargin={false}
+            value={queryString}
+            editorProps={{
+              $blockScrolling: Infinity
+            }}
+            setOptions={{
+              enableBasicAutocompletion: !runeMode,
+              enableLiveAutocompletion: !runeMode,
+              showLineNumbers: true,
+              tabSize: 2
+            }}
+            style={{}}
+          />
+        </div>
+      </ResizeSensor>
+    </div>;
+  }
+}
diff --git a/web-console/src/views/query-view/query-output/__snapshots__/query-output.spec.tsx.snap b/web-console/src/views/query-view/query-output/__snapshots__/query-output.spec.tsx.snap
new file mode 100644
index 0000000..65e36a8
--- /dev/null
+++ b/web-console/src/views/query-view/query-output/__snapshots__/query-output.spec.tsx.snap
@@ -0,0 +1,320 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`query output matches snapshot 1`] = `
+<div
+  class="query-output"
+>
+  <div
+    class="ReactTable"
+  >
+    <div
+      class="rt-table"
+      role="grid"
+    >
+      <div
+        class="rt-thead -header"
+        style="min-width: 0px;"
+      >
+        <div
+          class="rt-tr"
+          role="row"
+        />
+      </div>
+      <div
+        class="rt-tbody"
+        style="min-width: 0px;"
+      >
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -odd"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -even"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -odd"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -even"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -odd"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -even"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -odd"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -even"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -odd"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -even"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -odd"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -even"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -odd"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -even"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -odd"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -even"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -odd"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -even"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -odd"
+            role="row"
+          />
+        </div>
+        <div
+          class="rt-tr-group"
+          role="rowgroup"
+        >
+          <div
+            class="rt-tr -padRow -even"
+            role="row"
+          />
+        </div>
+      </div>
+    </div>
+    <div
+      class="pagination-bottom"
+    >
+      <div
+        class="-pagination"
+      >
+        <div
+          class="-previous"
+        >
+          <button
+            class="-btn"
+            disabled=""
+            type="button"
+          >
+            Previous
+          </button>
+        </div>
+        <div
+          class="-center"
+        >
+          <span
+            class="-pageInfo"
+          >
+            Page
+             
+            <div
+              class="-pageJump"
+            >
+              <input
+                aria-label="jump to page"
+                type="number"
+                value="1"
+              />
+            </div>
+             
+            of
+             
+            <span
+              class="-totalPages"
+            >
+              1
+            </span>
+          </span>
+          <span
+            class="select-wrap -pageSizeOptions"
+          >
+            <select
+              aria-label="rows per page"
+            >
+              <option
+                value="5"
+              >
+                5 rows
+              </option>
+              <option
+                value="10"
+              >
+                10 rows
+              </option>
+              <option
+                value="20"
+              >
+                20 rows
+              </option>
+              <option
+                value="25"
+              >
+                25 rows
+              </option>
+              <option
+                value="50"
+              >
+                50 rows
+              </option>
+              <option
+                value="100"
+              >
+                100 rows
+              </option>
+            </select>
+          </span>
+        </div>
+        <div
+          class="-next"
+        >
+          <button
+            class="-btn"
+            disabled=""
+            type="button"
+          >
+            Next
+          </button>
+        </div>
+      </div>
+    </div>
+    <div
+      class="rt-noData"
+    >
+      lol
+    </div>
+    <div
+      class="-loading"
+    >
+      <div
+        class="-loading-inner"
+      >
+        Loading...
+      </div>
+    </div>
+  </div>
+</div>
+`;
diff --git a/web-console/src/views/index.ts b/web-console/src/views/query-view/query-output/query-output.scss
similarity index 69%
copy from web-console/src/views/index.ts
copy to web-console/src/views/query-view/query-output/query-output.scss
index 7a753bb..f5dd473 100644
--- a/web-console/src/views/index.ts
+++ b/web-console/src/views/query-view/query-output/query-output.scss
@@ -15,11 +15,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-export * from './datasource-view/datasource-view';
-export * from './home-view/home-view';
-export * from './load-data-view/load-data-view';
-export * from './lookups-view/lookups-view';
-export * from './segments-view/segments-view';
-export * from './servers-view/servers-view';
-export * from './sql-view/sql-view';
-export * from './task-view/tasks-view';
+
+.query-output {
+  .ReactTable {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    width: 100%;
+  }
+}
diff --git a/web-console/src/views/sql-view/sql-control/sql-control.spec.tsx b/web-console/src/views/query-view/query-output/query-output.spec.tsx
similarity index 75%
copy from web-console/src/views/sql-view/sql-control/sql-control.spec.tsx
copy to web-console/src/views/query-view/query-output/query-output.spec.tsx
index a044aa4..1a9f04e 100644
--- a/web-console/src/views/sql-view/sql-control/sql-control.spec.tsx
+++ b/web-console/src/views/query-view/query-output/query-output.spec.tsx
@@ -19,19 +19,17 @@
 import React from 'react';
 import { render } from 'react-testing-library';
 
-import { SqlControl } from './sql-control';
+import { QueryOutput } from './query-output';
 
-describe('sql control', () => {
+describe('query output', () => {
   it('matches snapshot', () => {
-    const sqlControl = <SqlControl
-      initSql={'test'}
-      onRun={(query, context, wrapQuery) => {}}
-      onExplain={(sqlQuery, context) => {}}
-      queryElapsed={2}
-      onDownload={() => {}}
+    const queryOutput = <QueryOutput
+      loading={false}
+      result={null}
+      error="lol"
     />;
 
-    const { container } = render(sqlControl);
+    const { container } = render(queryOutput);
     expect(container.firstChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/views/query-view/query-output/query-output.tsx b/web-console/src/views/query-view/query-output/query-output.tsx
new file mode 100644
index 0000000..96e4779
--- /dev/null
+++ b/web-console/src/views/query-view/query-output/query-output.tsx
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import ReactTable from 'react-table';
+
+import { TableCell } from '../../../components';
+import { HeaderRows } from '../../../utils';
+
+import './query-output.scss';
+
+export interface QueryOutputProps extends React.Props<any> {
+  loading: boolean;
+  result: HeaderRows | null;
+  error: string | null;
+}
+
+export class QueryOutput extends React.PureComponent<QueryOutputProps> {
+
+  render() {
+    const { result, loading, error } = this.props;
+
+    return <div className="query-output">
+      <ReactTable
+        data={result ? result.rows : []}
+        loading={loading}
+        noDataText={!loading && result && !result.rows.length ? 'No results' : (error || '')}
+        sortable={false}
+        columns={
+          (result ? result.header : []).map((h: any, i) => {
+            return {
+              Header: h,
+              accessor: String(i),
+              Cell: row => <TableCell value={row.value}/>
+            };
+          })
+        }
+      />
+    </div>;
+  }
+}
diff --git a/web-console/src/views/sql-view/sql-view.scss b/web-console/src/views/query-view/query-view.scss
similarity index 56%
rename from web-console/src/views/sql-view/sql-view.scss
rename to web-console/src/views/query-view/query-view.scss
index f1ef90b..ec179eb 100644
--- a/web-console/src/views/sql-view/sql-view.scss
+++ b/web-console/src/views/query-view/query-view.scss
@@ -18,40 +18,71 @@
 
 @import "../../variables";
 
-.sql-view {
+$nav-width: 250px;
+
+.query-view {
   height: 100%;
   width: 100%;
 
-  .top-pane {
+  .column-tree {
+    position: absolute;
+    top: 0;
+    left: 0;
+    height: 100%;
+    width: $nav-width;
+  }
+
+  .splitter-layout {
     position: absolute;
-    width: 100%;
     top: 0;
-    bottom: 10px;
+    height: 100%;
+    left: $nav-width + $standard-padding;
+    right: 0;
+    width: auto;
 
-    .sql-control {
+    .control-pane {
       position: absolute;
       width: 100%;
-      height: 100%;
-    }
-  }
+      top: 0;
+      bottom: 10px;
 
-  &.splitter-layout.splitter-layout-vertical > .layout-splitter {
-    height: 3px;
-    background-color: #6d8ea9;
-  }
+      .query-input {
+        position: absolute;
+        top: 0;
+        width: 100%;
+        bottom: 30px + $standard-padding;
+      }
 
-  .bottom-pane {
-    position: absolute;
-    width: 100%;
-    top: 10px;
-    bottom: 0;
+      .control-bar {
+        position: absolute;
+        bottom: 0;
+        width: 100%;
 
-    .ReactTable {
+        .query-extra-info {
+          position: absolute;
+          right: 0;
+          top: 0;
+        }
+      }
+    }
+
+    &.splitter-layout-vertical > .layout-splitter {
+      height: 3px;
+      background-color: #6d8ea9;
+    }
+
+    .query-output {
       position: absolute;
-      top: 0;
-      bottom: 0;
       width: 100%;
+      top: 10px;
+      bottom: 0;
+
+
     }
   }
+
+  &.hide-column-tree .splitter-layout {
+    left: 0;
+  }
 }
 
diff --git a/web-console/src/views/sql-view/sql-view.spec.tsx b/web-console/src/views/query-view/query-view.spec.tsx
similarity index 73%
rename from web-console/src/views/sql-view/sql-view.spec.tsx
rename to web-console/src/views/query-view/query-view.spec.tsx
index e5809a8..a34cf0a 100644
--- a/web-console/src/views/sql-view/sql-view.spec.tsx
+++ b/web-console/src/views/query-view/query-view.spec.tsx
@@ -19,20 +19,20 @@
 import { shallow } from 'enzyme';
 import React from 'react';
 
-import { SqlView } from './sql-view';
+import { QueryView } from './query-view';
 
 describe('sql view', () => {
   it('matches snapshot', () => {
     const sqlView = shallow(
-      <SqlView
-        initSql={'test'}
+      <QueryView
+        initQuery={'test'}
       />);
     expect(sqlView).toMatchSnapshot();
   });
 
   it('trimSemicolon', () => {
-    expect(SqlView.trimSemicolon('SELECT * FROM tbl;')).toEqual('SELECT * FROM tbl');
-    expect(SqlView.trimSemicolon('SELECT * FROM tbl;   ')).toEqual('SELECT * FROM tbl   ');
-    expect(SqlView.trimSemicolon('SELECT * FROM tbl; --hello  ')).toEqual('SELECT * FROM tbl --hello  ');
+    expect(QueryView.trimSemicolon('SELECT * FROM tbl;')).toEqual('SELECT * FROM tbl');
+    expect(QueryView.trimSemicolon('SELECT * FROM tbl;   ')).toEqual('SELECT * FROM tbl   ');
+    expect(QueryView.trimSemicolon('SELECT * FROM tbl; --hello  ')).toEqual('SELECT * FROM tbl --hello  ');
   });
 });
diff --git a/web-console/src/views/query-view/query-view.tsx b/web-console/src/views/query-view/query-view.tsx
new file mode 100644
index 0000000..cffc9e9
--- /dev/null
+++ b/web-console/src/views/query-view/query-view.tsx
@@ -0,0 +1,397 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Intent } from '@blueprintjs/core';
+import axios from 'axios';
+import classNames from 'classnames';
+import Hjson from 'hjson';
+import React from 'react';
+import SplitterLayout from 'react-splitter-layout';
+
+import { QueryPlanDialog } from '../../dialogs';
+import { AppToaster } from '../../singletons/toaster';
+import {
+  BasicQueryExplanation,
+  decodeRune,
+  downloadFile, getDruidErrorMessage,
+  HeaderRows,
+  localStorageGet, LocalStorageKeys,
+  localStorageSet, parseQueryPlan,
+  queryDruidSql, QueryManager,
+  SemiJoinQueryExplanation
+} from '../../utils';
+import { ColumnMetadata } from '../../utils/column-metadata';
+import { isEmptyContext, QueryContext } from '../../utils/query-context';
+
+import { ColumnTree } from './column-tree/column-tree';
+import { QueryExtraInfo, QueryExtraInfoData } from './query-extra-info/query-extra-info';
+import { QueryInput } from './query-input/query-input';
+import { QueryOutput } from './query-output/query-output';
+import { RunButton } from './run-button/run-button';
+
+import './query-view.scss';
+
+interface QueryWithContext {
+  queryString: string;
+  queryContext: QueryContext;
+  wrapQuery?: boolean;
+}
+
+export interface QueryViewProps extends React.Props<any> {
+  initQuery: string | null;
+}
+
+export interface QueryViewState {
+  queryString: string;
+  queryContext: QueryContext;
+
+  columnMetadataLoading: boolean;
+  columnMetadata: ColumnMetadata[] | null;
+  columnMetadataError: string | null;
+
+  loading: boolean;
+  result: HeaderRows | null;
+  queryExtraInfo: QueryExtraInfoData | null;
+  error: string | null;
+
+  explainDialogOpen: boolean;
+  explainResult: BasicQueryExplanation | SemiJoinQueryExplanation | string | null;
+  loadingExplain: boolean;
+  explainError: Error | null;
+}
+
+interface QueryResult {
+  queryResult: HeaderRows;
+  queryExtraInfo: QueryExtraInfoData;
+}
+
+export class QueryView extends React.PureComponent<QueryViewProps, QueryViewState> {
+  static trimSemicolon(query: string): string {
+    // Trims out a trailing semicolon while preserving space (https://bit.ly/1n1yfkJ)
+    return query.replace(/;+((?:\s*--[^\n]*)?\s*)$/, '$1');
+  }
+
+  static isRune(queryString: string): boolean {
+    return queryString.trim().startsWith('{');
+  }
+
+  static validRune(queryString: string): boolean {
+    try {
+      Hjson.parse(queryString);
+      return true;
+    } catch {
+      return false;
+    }
+  }
+
+  static formatStr(s: string | number, format: 'csv' | 'tsv') {
+    if (format === 'csv') {
+      // remove line break, single quote => double quote, handle ','
+      return `"${String(s).replace(/(?:\r\n|\r|\n)/g, ' ').replace(/"/g, '""')}"`;
+    } else { // tsv
+      // remove line break, single quote => double quote, \t => ''
+      return String(s).replace(/(?:\r\n|\r|\n)/g, ' ').replace(/\t/g, '').replace(/"/g, '""');
+    }
+  }
+
+  private metadataQueryManager: QueryManager<string, ColumnMetadata[]>;
+  private sqlQueryManager: QueryManager<QueryWithContext, QueryResult>;
+  private explainQueryManager: QueryManager<QueryWithContext, BasicQueryExplanation | SemiJoinQueryExplanation | string>;
+
+  constructor(props: QueryViewProps, context: any) {
+    super(props, context);
+    this.state = {
+      queryString: props.initQuery || localStorageGet(LocalStorageKeys.QUERY_KEY) || '',
+      queryContext: {},
+
+      columnMetadataLoading: false,
+      columnMetadata: null,
+      columnMetadataError: null,
+
+      loading: false,
+      result: null,
+      queryExtraInfo: null,
+      error: null,
+
+      explainDialogOpen: false,
+      loadingExplain: false,
+      explainResult: null,
+      explainError: null
+    };
+  }
+
+  componentDidMount(): void {
+    this.metadataQueryManager = new QueryManager({
+      processQuery: async (query: string) => {
+        return await queryDruidSql<ColumnMetadata>({
+          query: `SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS`
+        });
+      },
+      onStateChange: ({ result, loading, error }) => {
+        if (error) {
+          AppToaster.show({
+            message: 'Could not load SQL metadata',
+            intent: Intent.DANGER
+          });
+        }
+        this.setState({
+          columnMetadataLoading: loading,
+          columnMetadata: result,
+          columnMetadataError: error
+        });
+      }
+    });
+
+    this.metadataQueryManager.runQuery('dummy');
+
+    this.sqlQueryManager = new QueryManager({
+      processQuery: async (queryWithContext: QueryWithContext) => {
+        const { queryString, queryContext, wrapQuery } = queryWithContext;
+        let queryId: string | null = null;
+        let sqlQueryId: string | null = null;
+        let wrappedLimit: number | undefined;
+
+        let queryResult: HeaderRows;
+        const startTime = new Date();
+        let endTime: Date;
+
+        if (QueryView.isRune(queryString)) {
+          // Secret way to issue a native JSON "rune" query
+          const runeQuery = Hjson.parse(queryString);
+
+          if (!isEmptyContext(queryContext)) runeQuery.context = queryContext;
+          let runeResult: any[];
+          try {
+            const runeResultResp = await axios.post('/druid/v2', runeQuery);
+            endTime = new Date();
+            runeResult = runeResultResp.data;
+            queryId = runeResultResp.headers['x-druid-query-id'];
+          } catch (e) {
+            throw new Error(getDruidErrorMessage(e));
+          }
+
+          queryResult = decodeRune(runeQuery, runeResult);
+
+        } else {
+          const actualQuery = wrapQuery ?
+            `SELECT * FROM (${QueryView.trimSemicolon(queryString)}\n) LIMIT 1000` :
+            queryString;
+
+          if (wrapQuery) wrappedLimit = 1000;
+
+          const queryPayload: Record<string, any> = {
+            query: actualQuery,
+            resultFormat: 'array',
+            header: true
+          };
+
+          if (!isEmptyContext(queryContext)) queryPayload.context = queryContext;
+          let sqlResult: any[];
+          try {
+            const sqlResultResp = await axios.post('/druid/v2/sql', queryPayload);
+            endTime = new Date();
+            sqlResult = sqlResultResp.data;
+            sqlQueryId = sqlResultResp.headers['x-druid-sql-query-id'];
+          } catch (e) {
+            throw new Error(getDruidErrorMessage(e));
+          }
+
+          queryResult = {
+            header: (sqlResult && sqlResult.length) ? sqlResult[0] : [],
+            rows: (sqlResult && sqlResult.length) ? sqlResult.slice(1) : []
+          };
+        }
+
+        return {
+          queryResult,
+          queryExtraInfo: {
+            queryId,
+            sqlQueryId,
+            startTime,
+            endTime,
+            numResults: queryResult.rows.length,
+            wrappedLimit
+          }
+        };
+      },
+      onStateChange: ({ result, loading, error }) => {
+        this.setState({
+          result: result ? result.queryResult : null,
+          queryExtraInfo: result ? result.queryExtraInfo : null,
+          loading,
+          error
+        });
+      }
+    });
+
+    this.explainQueryManager = new QueryManager({
+      processQuery: async (queryWithContext: QueryWithContext) => {
+        const { queryString, queryContext } = queryWithContext;
+        const explainPayload: Record<string, any> = {
+          query: `EXPLAIN PLAN FOR (${QueryView.trimSemicolon(queryString)}\n)`,
+          resultFormat: 'object'
+        };
+
+        if (!isEmptyContext(queryContext)) explainPayload.context = queryContext;
+        const result = await queryDruidSql(explainPayload);
+
+        return parseQueryPlan(result[0]['PLAN']);
+      },
+      onStateChange: ({ result, loading, error }) => {
+        this.setState({
+          explainResult: result,
+          loadingExplain: loading,
+          explainError: error !== null ? new Error(error) : null
+        });
+      }
+    });
+  }
+
+  componentWillUnmount(): void {
+    this.metadataQueryManager.terminate();
+    this.sqlQueryManager.terminate();
+    this.explainQueryManager.terminate();
+  }
+
+  handleDownload = (filename: string, format: string) => {
+    const { result } = this.state;
+    if (!result) return;
+    let lines: string[] = [];
+    let separator: string = '';
+
+    if (format === 'csv' || format === 'tsv') {
+      separator = format === 'csv' ? ',' : '\t';
+      lines.push(result.header.map(str => QueryView.formatStr(str, format)).join(separator));
+      lines = lines.concat(result.rows.map(r => r.map(cell => QueryView.formatStr(cell, format)).join(separator)));
+    } else { // json
+      lines = result.rows.map(r => {
+        const outputObject: Record<string, any> = {};
+        for (let k = 0; k < r.length; k++) {
+          const newName = result.header[k];
+          if (newName) {
+            outputObject[newName] = r[k];
+          }
+        }
+        return JSON.stringify(outputObject);
+      });
+    }
+
+    const lineBreak = '\n';
+    downloadFile(lines.join(lineBreak), format, filename);
+  }
+
+  renderExplainDialog() {
+    const {explainDialogOpen, explainResult, loadingExplain, explainError} = this.state;
+    if (!loadingExplain && explainDialogOpen) {
+      return <QueryPlanDialog
+        explainResult={explainResult}
+        explainError={explainError}
+        onClose={() => this.setState({explainDialogOpen: false})}
+      />;
+    }
+    return null;
+  }
+
+  renderMainArea() {
+    const { queryString, queryContext, loading, result, queryExtraInfo, error, columnMetadata } = this.state;
+    const runeMode = QueryView.isRune(queryString);
+
+    return <SplitterLayout
+      vertical
+      percentage
+      secondaryInitialSize={Number(localStorageGet(LocalStorageKeys.QUERY_VIEW_PANE_SIZE) as string) || 60}
+      primaryMinSize={30}
+      secondaryMinSize={30}
+      onSecondaryPaneSizeChange={this.handleSecondaryPaneSizeChange}
+    >
+      <div className="control-pane">
+        <QueryInput
+          queryString={queryString}
+          onQueryStringChange={this.handleQueryStringChange}
+          runeMode={runeMode}
+          columnMetadata={columnMetadata}
+        />
+        <div className="control-bar">
+          <RunButton
+            runeMode={runeMode}
+            queryContext={queryContext}
+            onQueryContextChange={this.handleQueryContextChange}
+            onRun={this.handleRun}
+            onExplain={this.handleExplain}
+          />
+          {
+            queryExtraInfo &&
+            <QueryExtraInfo
+              queryExtraInfo={queryExtraInfo}
+              onDownload={this.handleDownload}
+            />
+          }
+        </div>
+      </div>
+      <QueryOutput
+        loading={loading}
+        result={result}
+        error={error}
+      />
+    </SplitterLayout>;
+  }
+
+  private handleQueryStringChange = (queryString: string): void => {
+    this.setState({ queryString });
+  }
+
+  private handleQueryContextChange = (queryContext: QueryContext) => {
+    this.setState({ queryContext });
+  }
+
+  private handleRun = (wrapQuery: boolean) => {
+    const { queryString, queryContext } = this.state;
+
+    if (QueryView.isRune(queryString) && !QueryView.validRune(queryString)) return;
+
+    localStorageSet(LocalStorageKeys.QUERY_KEY, queryString);
+    this.sqlQueryManager.runQuery({ queryString, queryContext, wrapQuery });
+  }
+
+  private handleExplain = () => {
+    const { queryString, queryContext } = this.state;
+    this.setState({ explainDialogOpen: true });
+    this.explainQueryManager.runQuery({ queryString, queryContext });
+  }
+
+  private handleSecondaryPaneSizeChange = (secondaryPaneSize: number) => {
+    localStorageSet(LocalStorageKeys.QUERY_VIEW_PANE_SIZE, String(secondaryPaneSize));
+  }
+
+  render() {
+    const { columnMetadata, columnMetadataLoading, columnMetadataError } = this.state;
+
+    return <div className={classNames('query-view app-view', { 'hide-column-tree': columnMetadataError })}>
+      {
+        !columnMetadataError &&
+        <ColumnTree
+          columnMetadataLoading={columnMetadataLoading}
+          columnMetadata={columnMetadata}
+          onQueryStringChange={this.handleQueryStringChange}
+        />
+      }
+      {this.renderMainArea()}
+      {this.renderExplainDialog()}
+    </div>;
+  }
+}
diff --git a/web-console/src/views/query-view/run-button/__snapshots__/run-button.spec.tsx.snap b/web-console/src/views/query-view/run-button/__snapshots__/run-button.spec.tsx.snap
new file mode 100644
index 0000000..034d79b
--- /dev/null
+++ b/web-console/src/views/query-view/run-button/__snapshots__/run-button.spec.tsx.snap
@@ -0,0 +1,78 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`run button matches snapshot 1`] = `
+<div
+  class="bp3-button-group run-button"
+>
+  <span
+    class="bp3-popover-wrapper"
+  >
+    <span
+      class="bp3-popover-target"
+    >
+      <button
+        class="bp3-button"
+        tabindex="0"
+        type="button"
+      >
+        <span
+          class="bp3-icon bp3-icon-caret-right"
+          icon="caret-right"
+        >
+          <svg
+            data-icon="caret-right"
+            height="16"
+            viewBox="0 0 16 16"
+            width="16"
+          >
+            <desc>
+              caret-right
+            </desc>
+            <path
+              d="M11 8c0-.15-.07-.28-.17-.37l-4-3.5A.495.495 0 0 0 6 4.5v7a.495.495 0 0 0 .83.37l4-3.5c.1-.09.17-.22.17-.37z"
+              fill-rule="evenodd"
+            />
+          </svg>
+        </span>
+        <span
+          class="bp3-button-text"
+        >
+          Run with limit
+        </span>
+      </button>
+    </span>
+  </span>
+  <span
+    class="bp3-popover-wrapper"
+  >
+    <span
+      class="bp3-popover-target"
+    >
+      <button
+        class="bp3-button"
+        type="button"
+      >
+        <span
+          class="bp3-icon bp3-icon-more"
+          icon="more"
+        >
+          <svg
+            data-icon="more"
+            height="16"
+            viewBox="0 0 16 16"
+            width="16"
+          >
+            <desc>
+              more
+            </desc>
+            <path
+              d="M2 6.03a2 2 0 1 0 0 4 2 2 0 1 0 0-4zM14 6.03a2 2 0 1 0 0 4 2 2 0 1 0 0-4zM8 6.03a2 2 0 1 0 0 4 2 2 0 1 0 0-4z"
+              fill-rule="evenodd"
+            />
+          </svg>
+        </span>
+      </button>
+    </span>
+  </span>
+</div>
+`;
diff --git a/web-console/src/views/index.ts b/web-console/src/views/query-view/run-button/run-button.scss
similarity index 69%
copy from web-console/src/views/index.ts
copy to web-console/src/views/query-view/run-button/run-button.scss
index 7a753bb..4bd20b2 100644
--- a/web-console/src/views/index.ts
+++ b/web-console/src/views/query-view/run-button/run-button.scss
@@ -15,11 +15,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-export * from './datasource-view/datasource-view';
-export * from './home-view/home-view';
-export * from './load-data-view/load-data-view';
-export * from './lookups-view/lookups-view';
-export * from './segments-view/segments-view';
-export * from './servers-view/servers-view';
-export * from './sql-view/sql-view';
-export * from './task-view/tasks-view';
+
+.run-button {
+
+}
diff --git a/web-console/src/views/sql-view/sql-control/sql-control.spec.tsx b/web-console/src/views/query-view/run-button/run-button.spec.tsx
similarity index 75%
rename from web-console/src/views/sql-view/sql-control/sql-control.spec.tsx
rename to web-console/src/views/query-view/run-button/run-button.spec.tsx
index a044aa4..18b6f1c 100644
--- a/web-console/src/views/sql-view/sql-control/sql-control.spec.tsx
+++ b/web-console/src/views/query-view/run-button/run-button.spec.tsx
@@ -19,19 +19,19 @@
 import React from 'react';
 import { render } from 'react-testing-library';
 
-import { SqlControl } from './sql-control';
+import { RunButton } from './run-button';
 
-describe('sql control', () => {
+describe('run button', () => {
   it('matches snapshot', () => {
-    const sqlControl = <SqlControl
-      initSql={'test'}
-      onRun={(query, context, wrapQuery) => {}}
-      onExplain={(sqlQuery, context) => {}}
-      queryElapsed={2}
-      onDownload={() => {}}
+    const runButton = <RunButton
+      runeMode={false}
+      queryContext={{}}
+      onQueryContextChange={() => null}
+      onRun={() => null}
+      onExplain={() => null}
     />;
 
-    const { container } = render(sqlControl);
+    const { container } = render(runButton);
     expect(container.firstChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/views/query-view/run-button/run-button.tsx b/web-console/src/views/query-view/run-button/run-button.tsx
new file mode 100644
index 0000000..8b8e3de
--- /dev/null
+++ b/web-console/src/views/query-view/run-button/run-button.tsx
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+  Button, ButtonGroup,
+  Hotkey,
+  Hotkeys,
+  HotkeysTarget,
+  Menu,
+  MenuItem,
+  Popover,
+  Position, Tooltip
+} from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
+import React from 'react';
+
+import { MenuCheckbox } from '../../../components';
+import {
+  getUseApproximateCountDistinct,
+  getUseApproximateTopN, getUseCache,
+  QueryContext,
+  setUseApproximateCountDistinct, setUseApproximateTopN, setUseCache
+} from '../../../utils/query-context';
+import { DRUID_DOCS_RUNE, DRUID_DOCS_SQL } from '../../../variables';
+
+import './run-button.scss';
+
+export interface RunButtonProps extends React.Props<any> {
+  runeMode: boolean;
+  queryContext: QueryContext;
+  onQueryContextChange: (newQueryContext: QueryContext) => void;
+  onRun: ((wrapQuery: boolean) => void) | null;
+  onExplain: () => void;
+}
+
+interface RunButtonState {
+  wrapQuery: boolean;
+}
+
+@HotkeysTarget
+export class RunButton extends React.PureComponent<RunButtonProps, RunButtonState> {
+  constructor(props: RunButtonProps, context: any) {
+    super(props, context);
+    this.state = {
+      wrapQuery: true
+    };
+  }
+
+  public renderHotkeys() {
+    return <Hotkeys>
+      <Hotkey
+        allowInInput
+        global
+        combo="ctrl + enter"
+        label="run on click"
+        onKeyDown={this.handleRun}
+      />
+    </Hotkeys>;
+  }
+
+  private handleRun = () => {
+    const { onRun } = this.props;
+    if (!onRun) return;
+    onRun(this.state.wrapQuery);
+  }
+
+  renderExtraMenu() {
+    const { runeMode, onExplain, queryContext, onQueryContextChange } = this.props;
+    const { wrapQuery } = this.state;
+
+    const useCache = getUseCache(queryContext);
+    const useApproximateCountDistinct = getUseApproximateCountDistinct(queryContext);
+    const useApproximateTopN = getUseApproximateTopN(queryContext);
+
+    return <Menu>
+      <MenuItem
+        icon={IconNames.HELP}
+        text="Docs"
+        href={runeMode ? DRUID_DOCS_RUNE : DRUID_DOCS_SQL}
+        target="_blank"
+      />
+      {
+        !runeMode &&
+        <>
+          <MenuItem
+            icon={IconNames.CLEAN}
+            text="Explain"
+            onClick={onExplain}
+          />
+          <MenuCheckbox
+            checked={wrapQuery}
+            label="Wrap query with limit"
+            onChange={() => this.setState({wrapQuery: !wrapQuery})}
+          />
+          <MenuCheckbox
+            checked={useApproximateCountDistinct}
+            label="Use approximate COUNT(DISTINCT)"
+            onChange={() => {
+              onQueryContextChange(setUseApproximateCountDistinct(queryContext, !useApproximateCountDistinct));
+            }}
+          />
+          <MenuCheckbox
+            checked={useApproximateTopN}
+            label="Use approximate TopN"
+            onChange={() => {
+              onQueryContextChange(setUseApproximateTopN(queryContext, !useApproximateTopN));
+            }}
+          />
+        </>
+      }
+      <MenuCheckbox
+        checked={useCache}
+        label="Use cache"
+        onChange={() => {
+          onQueryContextChange(setUseCache(queryContext, !useCache));
+        }}
+      />
+    </Menu>;
+  }
+
+  render() {
+    const { runeMode, onRun } = this.props;
+    const { wrapQuery } = this.state;
+
+    return <ButtonGroup className="run-button">
+      <Tooltip content="Control + Enter" hoverOpenDelay={900}>
+        <Button
+          icon={IconNames.CARET_RIGHT}
+          onClick={this.handleRun}
+          text={runeMode ? 'Rune' : (wrapQuery ? 'Run with limit' : 'Run as is')}
+          disabled={!onRun}
+        />
+      </Tooltip>
+      <Popover position={Position.BOTTOM_LEFT} content={this.renderExtraMenu()}>
+        <Button icon={IconNames.MORE}/>
+      </Popover>
+    </ButtonGroup>;
+  }
+}
diff --git a/web-console/src/views/segments-view/segments-view.spec.tsx b/web-console/src/views/segments-view/segments-view.spec.tsx
index b835cc6..789e376 100644
--- a/web-console/src/views/segments-view/segments-view.spec.tsx
+++ b/web-console/src/views/segments-view/segments-view.spec.tsx
@@ -27,7 +27,7 @@ describe('segments-view', () => {
       <SegmentsView
         datasource={'test'}
         onlyUnavailable={false}
-        goToSql={(initSql: string) => {}}
+        goToQuery={(initSql: string) => {}}
         noSqlMode={false}
       />);
     expect(segmentsView).toMatchSnapshot();
diff --git a/web-console/src/views/segments-view/segments-view.tsx b/web-console/src/views/segments-view/segments-view.tsx
index 9e3f895..eb4eb3e 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -44,7 +44,7 @@ const tableColumns: string[] = ['Segment ID', 'Datasource', 'Start', 'End', 'Ver
 const tableColumnsNoSql: string[] = ['Segment ID', 'Datasource', 'Start', 'End', 'Version', 'Partition', 'Size'];
 
 export interface SegmentsViewProps extends React.Props<any> {
-  goToSql: (initSql: string) => void;
+  goToQuery: (initSql: string) => void;
   datasource: string | null;
   onlyUnavailable: boolean | null;
   noSqlMode: boolean;
@@ -381,7 +381,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
   }
 
   render() {
-    const { goToSql, noSqlMode } = this.props;
+    const { goToQuery, noSqlMode } = this.props;
     const { tableColumnSelectionHandler } = this;
 
     return <div className="segments-view app-view">
@@ -397,7 +397,7 @@ export class SegmentsView extends React.PureComponent<SegmentsViewProps, Segment
             icon={IconNames.APPLICATION}
             text="Go to SQL"
             hidden={noSqlMode}
-            onClick={() => goToSql(this.segmentsSqlQueryManager.getLastQuery().query)}
+            onClick={() => goToQuery(this.segmentsSqlQueryManager.getLastQuery().query)}
           />
         }
         <TableColumnSelector
diff --git a/web-console/src/views/servers-view/servers-view.spec.tsx b/web-console/src/views/servers-view/servers-view.spec.tsx
index a8f01a7..8fdcf06 100644
--- a/web-console/src/views/servers-view/servers-view.spec.tsx
+++ b/web-console/src/views/servers-view/servers-view.spec.tsx
@@ -26,7 +26,7 @@ describe('servers view', () => {
     const serversView = shallow(
       <ServersView
         middleManager={'test'}
-        goToSql={(initSql: string) => {}}
+        goToQuery={(initSql: string) => {}}
         goToTask={(taskId: string) => {}}
         noSqlMode={false}
       />);
diff --git a/web-console/src/views/servers-view/servers-view.tsx b/web-console/src/views/servers-view/servers-view.tsx
index aa0789a..a3185ef 100644
--- a/web-console/src/views/servers-view/servers-view.tsx
+++ b/web-console/src/views/servers-view/servers-view.tsx
@@ -57,7 +57,7 @@ function formatQueues(segmentsToLoad: number, segmentsToLoadSize: number, segmen
 
 export interface ServersViewProps extends React.Props<any> {
   middleManager: string | null;
-  goToSql: (initSql: string) => void;
+  goToQuery: (initSql: string) => void;
   goToTask: (taskId: string) => void;
   noSqlMode: boolean;
 }
@@ -537,7 +537,7 @@ ORDER BY "rank" DESC, "server" DESC`);
   }
 
   render() {
-    const { goToSql, noSqlMode } = this.props;
+    const { goToQuery, noSqlMode } = this.props;
     const { groupServersBy } = this.state;
     const { serverTableColumnSelectionHandler } = this;
 
@@ -559,7 +559,7 @@ ORDER BY "rank" DESC, "server" DESC`);
           <Button
             icon={IconNames.APPLICATION}
             text="Go to SQL"
-            onClick={() => goToSql(this.serverQueryManager.getLastQuery())}
+            onClick={() => goToQuery(this.serverQueryManager.getLastQuery())}
           />
         }
         <TableColumnSelector
diff --git a/web-console/src/views/sql-view/__snapshots__/sql-view.spec.tsx.snap b/web-console/src/views/sql-view/__snapshots__/sql-view.spec.tsx.snap
deleted file mode 100644
index 737e993..0000000
--- a/web-console/src/views/sql-view/__snapshots__/sql-view.spec.tsx.snap
+++ /dev/null
@@ -1,170 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`sql view matches snapshot 1`] = `
-<t
-  customClassName="sql-view app-view"
-  onDragEnd={null}
-  onDragStart={null}
-  onSecondaryPaneSizeChange={[Function]}
-  percentage={true}
-  primaryIndex={0}
-  primaryMinSize={30}
-  secondaryInitialSize={60}
-  secondaryMinSize={30}
-  vertical={true}
->
-  <div
-    className="top-pane"
-  >
-    <HotkeysTarget(SqlControl)
-      initSql="test"
-      onDownload={[Function]}
-      onExplain={[Function]}
-      onRun={[Function]}
-      queryElapsed={null}
-    />
-  </div>
-  <div
-    className="bottom-pane"
-  >
-    <ReactTable
-      AggregatedComponent={[Function]}
-      ExpanderComponent={[Function]}
-      FilterComponent={[Function]}
-      LoadingComponent={[Function]}
-      NoDataComponent={[Function]}
-      PadRowComponent={[Function]}
-      PaginationComponent={[Function]}
-      PivotValueComponent={[Function]}
-      ResizerComponent={[Function]}
-      TableComponent={[Function]}
-      TbodyComponent={[Function]}
-      TdComponent={[Function]}
-      TfootComponent={[Function]}
-      ThComponent={[Function]}
-      TheadComponent={[Function]}
-      TrComponent={[Function]}
-      TrGroupComponent={[Function]}
-      aggregatedKey="_aggregated"
-      className=""
-      collapseOnDataChange={true}
-      collapseOnPageChange={true}
-      collapseOnSortingChange={true}
-      column={
-        Object {
-          "Aggregated": undefined,
-          "Cell": undefined,
-          "Expander": undefined,
-          "Filter": undefined,
-          "Footer": undefined,
-          "Header": undefined,
-          "Pivot": undefined,
-          "PivotValue": undefined,
-          "Placeholder": undefined,
-          "aggregate": undefined,
-          "className": "",
-          "filterAll": false,
-          "filterMethod": undefined,
-          "filterable": undefined,
-          "footerClassName": "",
-          "footerStyle": Object {},
-          "getFooterProps": [Function],
-          "getHeaderProps": [Function],
-          "getProps": [Function],
-          "headerClassName": "",
-          "headerStyle": Object {},
-          "minResizeWidth": 11,
-          "minWidth": 100,
-          "resizable": undefined,
-          "show": true,
-          "sortMethod": undefined,
-          "sortable": undefined,
-          "style": Object {},
-        }
-      }
-      columns={Array []}
-      data={Array []}
-      defaultExpanded={Object {}}
-      defaultFilterMethod={[Function]}
-      defaultFiltered={Array []}
-      defaultPage={0}
-      defaultPageSize={20}
-      defaultResized={Array []}
-      defaultSortDesc={false}
-      defaultSortMethod={[Function]}
-      defaultSorted={Array []}
-      expanderDefaults={
-        Object {
-          "filterable": false,
-          "resizable": false,
-          "sortable": false,
-          "width": 35,
-        }
-      }
-      filterable={false}
-      freezeWhenExpanded={false}
-      getLoadingProps={[Function]}
-      getNoDataProps={[Function]}
-      getPaginationProps={[Function]}
-      getProps={[Function]}
-      getResizerProps={[Function]}
-      getTableProps={[Function]}
-      getTbodyProps={[Function]}
-      getTdProps={[Function]}
-      getTfootProps={[Function]}
-      getTfootTdProps={[Function]}
-      getTfootTrProps={[Function]}
-      getTheadFilterProps={[Function]}
-      getTheadFilterThProps={[Function]}
-      getTheadFilterTrProps={[Function]}
-      getTheadGroupProps={[Function]}
-      getTheadGroupThProps={[Function]}
-      getTheadGroupTrProps={[Function]}
-      getTheadProps={[Function]}
-      getTheadThProps={[Function]}
-      getTheadTrProps={[Function]}
-      getTrGroupProps={[Function]}
-      getTrProps={[Function]}
-      groupedByPivotKey="_groupedByPivot"
-      indexKey="_index"
-      loading={false}
-      loadingText="Loading..."
-      multiSort={true}
-      nestingLevelKey="_nestingLevel"
-      nextText="Next"
-      noDataText=""
-      ofText="of"
-      onFetchData={[Function]}
-      originalKey="_original"
-      pageJumpText="jump to page"
-      pageSizeOptions={
-        Array [
-          5,
-          10,
-          20,
-          25,
-          50,
-          100,
-        ]
-      }
-      pageText="Page"
-      pivotDefaults={Object {}}
-      pivotIDKey="_pivotID"
-      pivotValKey="_pivotVal"
-      previousText="Previous"
-      resizable={true}
-      resolveData={[Function]}
-      rowsSelectorText="rows per page"
-      rowsText="rows"
-      showPageJump={true}
-      showPageSizeOptions={true}
-      showPagination={true}
-      showPaginationBottom={true}
-      showPaginationTop={false}
-      sortable={false}
-      style={Object {}}
-      subRowsKey="_subRows"
-    />
-  </div>
-</t>
-`;
diff --git a/web-console/src/views/sql-view/sql-control/__snapshots__/sql-control.spec.tsx.snap b/web-console/src/views/sql-view/sql-control/__snapshots__/sql-control.spec.tsx.snap
deleted file mode 100644
index 98e7d7c..0000000
--- a/web-console/src/views/sql-view/sql-control/__snapshots__/sql-control.spec.tsx.snap
+++ /dev/null
@@ -1,206 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`sql control matches snapshot 1`] = `
-<div
-  class="sql-control"
->
-  <div
-    class="ace-container"
-  >
-    <div
-      class=" ace_editor ace-tm ace_focus"
-      id="ace-editor"
-      style="width: 100%; height: 200px; font-size: 14px;"
-    >
-      <textarea
-        autocapitalize="off"
-        autocorrect="off"
-        class="ace_text-input"
-        spellcheck="false"
-        style="opacity: 0; position: fixed; top: 0px;"
-        wrap="off"
-      />
-      <div
-        aria-hidden="true"
-        class="ace_gutter"
-      >
-        <div
-          class="ace_layer ace_gutter-layer ace_folding-enabled"
-        />
-        <div
-          class="ace_gutter-active-line"
-        />
-      </div>
-      <div
-        class="ace_scroller"
-      >
-        <div
-          class="ace_content"
-        >
-          <div
-            class="ace_layer ace_print-margin-layer"
-          >
-            <div
-              class="ace_print-margin"
-              style="left: 4px; visibility: hidden;"
-            />
-          </div>
-          <div
-            class="ace_layer ace_marker-layer"
-          />
-          <div
-            class="ace_layer ace_text-layer"
-            style="padding: 0px 4px;"
-          />
-          <div
-            class="ace_layer ace_marker-layer"
-          />
-          <div
-            class="ace_layer ace_cursor-layer"
-          >
-            <div
-              class="ace_cursor"
-            />
-          </div>
-        </div>
-      </div>
-      <div
-        class="ace_scrollbar ace_scrollbar-v"
-        style="display: none; width: 20px;"
-      >
-        <div
-          class="ace_scrollbar-inner"
-          style="width: 20px;"
-        />
-      </div>
-      <div
-        class="ace_scrollbar ace_scrollbar-h"
-        style="display: none; height: 20px;"
-      >
-        <div
-          class="ace_scrollbar-inner"
-          style="height: 20px;"
-        />
-      </div>
-      <div
-        style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: hidden;"
-      >
-        <div
-          style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
-        />
-        <div
-          style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
-        >
-          XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
-        </div>
-      </div>
-    </div>
-  </div>
-  <div
-    class="buttons"
-  >
-    <div
-      class="bp3-button-group"
-    >
-      <button
-        class="bp3-button"
-        type="button"
-      >
-        <span
-          class="bp3-icon bp3-icon-caret-right"
-          icon="caret-right"
-        >
-          <svg
-            data-icon="caret-right"
-            height="16"
-            viewBox="0 0 16 16"
-            width="16"
-          >
-            <desc>
-              caret-right
-            </desc>
-            <path
-              d="M11 8c0-.15-.07-.28-.17-.37l-4-3.5A.495.495 0 0 0 6 4.5v7a.495.495 0 0 0 .83.37l4-3.5c.1-.09.17-.22.17-.37z"
-              fill-rule="evenodd"
-            />
-          </svg>
-        </span>
-        <span
-          class="bp3-button-text"
-        >
-          Run with limit
-        </span>
-      </button>
-      <span
-        class="bp3-popover-wrapper"
-      >
-        <span
-          class="bp3-popover-target"
-        >
-          <button
-            class="bp3-button"
-            type="button"
-          >
-            <span
-              class="bp3-icon bp3-icon-more"
-              icon="more"
-            >
-              <svg
-                data-icon="more"
-                height="16"
-                viewBox="0 0 16 16"
-                width="16"
-              >
-                <desc>
-                  more
-                </desc>
-                <path
-                  d="M2 6.03a2 2 0 1 0 0 4 2 2 0 1 0 0-4zM14 6.03a2 2 0 1 0 0 4 2 2 0 1 0 0-4zM8 6.03a2 2 0 1 0 0 4 2 2 0 1 0 0-4z"
-                  fill-rule="evenodd"
-                />
-              </svg>
-            </span>
-          </button>
-        </span>
-      </span>
-    </div>
-    <span
-      class="query-elapsed"
-    >
-      Last query took 0.00 seconds
-    </span>
-    <span
-      class="bp3-popover-wrapper download-button"
-    >
-      <span
-        class="bp3-popover-target"
-      >
-        <button
-          class="bp3-button bp3-minimal"
-          type="button"
-        >
-          <span
-            class="bp3-icon bp3-icon-download"
-            icon="download"
-          >
-            <svg
-              data-icon="download"
-              height="16"
-              viewBox="0 0 16 16"
-              width="16"
-            >
-              <desc>
-                download
-              </desc>
-              <path
-                d="M7.99-.01c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zM11.7 9.7l-3 3c-.18.18-.43.29-.71.29s-.53-.11-.71-.29l-3-3A1.003 1.003 0 0 1 5.7 8.28l1.29 1.29V3.99c0-.55.45-1 1-1s1 .45 1 1v5.59l1.29-1.29a1.003 1.003 0 0 1 1.71.71c0 .27-.11.52-.29.7z"
-                fill-rule="evenodd"
-              />
-            </svg>
-          </span>
-        </button>
-      </span>
-    </span>
-  </div>
-</div>
-`;
diff --git a/web-console/src/views/sql-view/sql-control/sql-control.tsx b/web-console/src/views/sql-view/sql-control/sql-control.tsx
deleted file mode 100644
index 58486a3..0000000
--- a/web-console/src/views/sql-view/sql-control/sql-control.tsx
+++ /dev/null
@@ -1,385 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import {
-  Button,
-  ButtonGroup,
-  Intent, IResizeEntry,
-  Menu,
-  MenuItem, NavbarGroup,
-  Popover,
-  Position, ResizeSensor
-} from '@blueprintjs/core';
-import { Hotkey, Hotkeys, HotkeysTarget } from '@blueprintjs/core';
-import { IconNames } from '@blueprintjs/icons';
-import axios from 'axios';
-import ace from 'brace';
-import Hjson from 'hjson';
-import React from 'react';
-import AceEditor from 'react-ace';
-import ReactDOMServer from 'react-dom/server';
-
-import { SQLFunctionDoc } from '../../../../lib/sql-function-doc';
-import { MenuCheckbox } from '../../../components';
-import { AppToaster } from '../../../singletons/toaster';
-import { DRUID_DOCS_RUNE, DRUID_DOCS_SQL } from '../../../variables';
-
-import './sql-control.scss';
-
-function validHjson(query: string) {
-  try {
-    Hjson.parse(query);
-    return true;
-  } catch {
-    return false;
-  }
-}
-
-const langTools = ace.acequire('ace/ext/language_tools');
-
-export interface SqlControlProps extends React.Props<any> {
-  initSql: string | null;
-  onRun: (query: string, context: Record<string, any>, wrapQuery: boolean) => void;
-  onExplain: (sqlQuery: string, context: Record<string, any>) => void;
-  queryElapsed: number | null;
-  onDownload: (format: string) => void;
-}
-
-export interface SqlControlState {
-  query: string;
-  autoComplete: boolean;
-  autoCompleteLoading: boolean;
-  wrapQuery: boolean;
-  useApproximateCountDistinct: boolean;
-  useApproximateTopN: boolean;
-  useCache: boolean;
-
-  // For reasons (https://github.com/securingsincity/react-ace/issues/415) react ace editor needs an explicit height
-  // Since this component will grown and shrink dynamically we will measure its height and then set it.
-  editorHeight: number;
-}
-
-@HotkeysTarget
-export class SqlControl extends React.PureComponent<SqlControlProps, SqlControlState> {
-  constructor(props: SqlControlProps, context: any) {
-    super(props, context);
-    this.state = {
-      query: props.initSql || '',
-      autoComplete: true,
-      autoCompleteLoading: false,
-      wrapQuery: true,
-      useApproximateCountDistinct: true,
-      useApproximateTopN: true,
-      useCache: true,
-
-      editorHeight: 200
-    };
-  }
-
-  private replaceDefaultAutoCompleter = () => {
-    /*
-     Please refer to the source code @
-     https://github.com/ajaxorg/ace/blob/9b5b63d1dc7c1b81b58d30c87d14b5905d030ca5/lib/ace/ext/language_tools.js#L41
-     for the implementation of keyword completer
-    */
-    const keywordCompleter = {
-      getCompletions: (editor: any, session: any, pos: any, prefix: any, callback: any) => {
-        if (session.$mode.completer) {
-          return session.$mode.completer.getCompletions(editor, session, pos, prefix, callback);
-        }
-        const state = editor.session.getState(pos.row);
-        let keywordCompletions = session.$mode.getCompletions(state, session, pos, prefix);
-        keywordCompletions = keywordCompletions.map((d: any) => {
-          return Object.assign(d, {name: d.name.toUpperCase(), value: d.value.toUpperCase()});
-        });
-        return callback(null, keywordCompletions);
-      }
-    };
-    langTools.setCompleters([langTools.snippetCompleter, langTools.textCompleter, keywordCompleter]);
-  }
-
-  private addDatasourceAutoCompleter = async (): Promise<any> => {
-    const datasourceResp = await axios.post('/druid/v2/sql', { query: `SELECT datasource FROM sys.segments GROUP BY 1`});
-    const datasourceList: any[] = datasourceResp.data.map((d: any) => {
-      const datasourceName: string = d.datasource;
-      return {
-        value: datasourceName,
-        score: 50,
-        meta: 'datasource'
-      };
-    });
-
-    const completer = {
-      getCompletions: (editor: any, session: any, pos: any, prefix: any, callback: any) => {
-        callback(null, datasourceList);
-      }
-    };
-
-    langTools.addCompleter(completer);
-  }
-
-  private addColumnNameAutoCompleter = async (): Promise<any> => {
-    const columnNameResp = await axios.post('/druid/v2/sql', {query: `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'druid'`});
-    const columnNameList: any[] = columnNameResp.data.map((d: any) => {
-      const columnName: string = d.COLUMN_NAME;
-      return {
-        value: columnName,
-        score: 50,
-        meta: 'column'
-      };
-    });
-
-    const completer = {
-      getCompletions: (editor: any, session: any, pos: any, prefix: any, callback: any) => {
-        callback(null, columnNameList);
-      }
-    };
-
-    langTools.addCompleter(completer);
-  }
-
-  private addFunctionAutoCompleter = (): void => {
-    const functionList: any[] = SQLFunctionDoc.map((entry: any) => {
-      let funcName: string = entry.syntax.replace(/\(.*\)/, '()');
-      if (!funcName.includes('(')) funcName = funcName.substr(0, 10);
-      return {
-        value: funcName,
-        score: 80,
-        meta: 'function',
-        syntax: entry.syntax,
-        description: entry.description,
-        completer: {
-          insertMatch: (editor: any, data: any) => {
-            editor.completer.insertMatch({value: data.caption});
-            const pos = editor.getCursorPosition();
-            editor.gotoLine(pos.row + 1, pos.column - 1);
-          }
-        }
-      };
-    });
-
-    const completer = {
-      getCompletions: (editor: any, session: any, pos: any, prefix: any, callback: any) => {
-        callback(null, functionList);
-      },
-      getDocTooltip: (item: any) => {
-        if (item.meta === 'function') {
-          const functionName = item.caption.slice(0, -2);
-          item.docHTML = ReactDOMServer.renderToStaticMarkup((
-            <div className="function-doc">
-              <div className="function-doc-name"><b>{functionName}</b></div>
-              <hr/>
-              <div><b>Syntax:</b></div>
-              <div>{item.syntax}</div>
-              <br/>
-              <div><b>Description:</b></div>
-              <div>{item.description}</div>
-            </div>
-          ));
-        }
-      }
-    };
-    langTools.addCompleter(completer);
-  }
-
-  private addCompleters = async () => {
-    try {
-      this.replaceDefaultAutoCompleter();
-      this.addFunctionAutoCompleter();
-      await this.addDatasourceAutoCompleter();
-      await this.addColumnNameAutoCompleter();
-    } catch (e) {
-      AppToaster.show({
-        message: 'Failed to load SQL auto completer',
-        intent: Intent.DANGER
-      });
-    }
-  }
-
-  componentDidMount(): void {
-    this.addCompleters();
-  }
-
-  getContext(): Record<string, any> {
-    const { useCache, useApproximateCountDistinct, useApproximateTopN } = this.state;
-    const context: Record<string, any> = {};
-
-    if (useCache === false) {
-      context.useCache = false;
-      context.populateCache = false;
-    }
-
-    if (useApproximateCountDistinct === false) {
-      context.useApproximateCountDistinct = false;
-    }
-
-    if (useApproximateTopN === false) {
-      context.useApproximateTopN = false;
-    }
-
-    return context;
-  }
-
-  private handleChange = (newValue: string): void => {
-    this.setState({
-      query: newValue
-    });
-  }
-
-  private onRunClick = () => {
-    const { onRun } = this.props;
-    const { query, wrapQuery } = this.state;
-    onRun(query, this.getContext(), wrapQuery);
-  }
-
-  private handleAceContainerResize = (entries: IResizeEntry[]) => {
-    if (entries.length !== 1) return;
-    this.setState({ editorHeight: entries[0].contentRect.height });
-  }
-
-  renderExtraMenu(isRune: boolean) {
-    const { onExplain } = this.props;
-    const { query, autoComplete, useCache, wrapQuery, useApproximateCountDistinct, useApproximateTopN } = this.state;
-
-    return <Menu>
-      <MenuItem
-        icon={IconNames.HELP}
-        text="Docs"
-        href={isRune ? DRUID_DOCS_RUNE : DRUID_DOCS_SQL}
-        target="_blank"
-      />
-      {
-        !isRune &&
-        <>
-          <MenuItem
-            icon={IconNames.CLEAN}
-            text="Explain"
-            onClick={() => onExplain(query, this.getContext())}
-          />
-          <MenuCheckbox
-            checked={wrapQuery}
-            label="Wrap query with limit"
-            onChange={() => this.setState({wrapQuery: !wrapQuery})}
-          />
-          <MenuCheckbox
-            checked={autoComplete}
-            label="Auto complete"
-            onChange={() => this.setState({autoComplete: !autoComplete})}
-          />
-          <MenuCheckbox
-            checked={useApproximateCountDistinct}
-            label="Use approximate COUNT(DISTINCT)"
-            onChange={() => this.setState({useApproximateCountDistinct: !useApproximateCountDistinct})}
-          />
-          <MenuCheckbox
-            checked={useApproximateTopN}
-            label="Use approximate TopN"
-            onChange={() => this.setState({useApproximateTopN: !useApproximateTopN})}
-          />
-        </>
-      }
-      <MenuCheckbox
-        checked={useCache}
-        label="Use cache"
-        onChange={() => this.setState({useCache: !useCache})}
-      />
-    </Menu>;
-  }
-
-  public renderHotkeys() {
-    return <Hotkeys>
-      <Hotkey
-        allowInInput
-        global
-        combo="ctrl + enter"
-        label="run on click"
-        onKeyDown={this.onRunClick}
-      />
-    </Hotkeys>;
-  }
-
-  render() {
-    const { queryElapsed, onDownload } = this.props;
-    const { query, autoComplete, wrapQuery, editorHeight } = this.state;
-    const isRune = query.trim().startsWith('{');
-    const downloadMenu = <Menu className="download-format-menu">
-      <MenuItem text="csv" onClick={() => onDownload('csv')} />
-      <MenuItem text="tsv" onClick={() => onDownload('tsv')} />
-      <MenuItem text="JSON" onClick={() => onDownload('json')}/>
-    </Menu>;
-
-    // Set the key in the AceEditor to force a rebind and prevent an error that happens otherwise
-    return <div className="sql-control">
-      <ResizeSensor onResize={this.handleAceContainerResize}>
-        <div className="ace-container">
-          <AceEditor
-            key={isRune ? 'hjson' : 'sql'}
-            mode={isRune ? 'hjson' : 'sql'}
-            theme="solarized_dark"
-            name="ace-editor"
-            onChange={this.handleChange}
-            focus
-            fontSize={14}
-            width="100%"
-            height={`${editorHeight}px`}
-            showPrintMargin={false}
-            value={query}
-            editorProps={{
-              $blockScrolling: Infinity
-            }}
-            setOptions={{
-              enableBasicAutocompletion: isRune ? false : autoComplete,
-              enableLiveAutocompletion: isRune ? false : autoComplete,
-              showLineNumbers: true,
-              tabSize: 2
-            }}
-            style={{}}
-          />
-        </div>
-      </ResizeSensor>
-      <div className="buttons">
-        <ButtonGroup>
-          <Button
-            icon={IconNames.CARET_RIGHT}
-            onClick={this.onRunClick}
-            text={isRune ? 'Rune' : (wrapQuery ? 'Run with limit' : 'Run as is')}
-            disabled={isRune && !validHjson(query)}
-          />
-          <Popover position={Position.BOTTOM_LEFT} content={this.renderExtraMenu(isRune)}>
-            <Button icon={IconNames.MORE}/>
-          </Popover>
-        </ButtonGroup>
-        {
-          queryElapsed &&
-          <span className="query-elapsed">
-            {`Last query took ${(queryElapsed / 1000).toFixed(2)} seconds`}
-          </span>
-        }
-        {
-          queryElapsed &&
-          <Popover className="download-button" content={downloadMenu} position={Position.BOTTOM_RIGHT}>
-            <Button
-              icon={IconNames.DOWNLOAD}
-              minimal
-            />
-          </Popover>
-        }
-      </div>
-    </div>;
-  }
-}
diff --git a/web-console/src/views/sql-view/sql-view.tsx b/web-console/src/views/sql-view/sql-view.tsx
deleted file mode 100644
index d132080..0000000
--- a/web-console/src/views/sql-view/sql-view.tsx
+++ /dev/null
@@ -1,275 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import Hjson from 'hjson';
-import React from 'react';
-import SplitterLayout from 'react-splitter-layout';
-import ReactTable from 'react-table';
-
-import { TableCell } from '../../components';
-import { QueryPlanDialog } from '../../dialogs';
-import {
-  BasicQueryExplanation,
-  decodeRune,
-  downloadFile,
-  HeaderRows,
-  localStorageGet, LocalStorageKeys,
-  localStorageSet, parseQueryPlan,
-  queryDruidRune,
-  queryDruidSql, QueryManager,
-  SemiJoinQueryExplanation
-} from '../../utils';
-
-import { SqlControl } from './sql-control/sql-control';
-
-import './sql-view.scss';
-
-interface QueryWithContext {
-  queryString: string;
-  context?: Record<string, any>;
-  wrapQuery?: boolean;
-}
-
-export interface SqlViewProps extends React.Props<any> {
-  initSql: string | null;
-}
-
-export interface SqlViewState {
-  loading: boolean;
-  result: HeaderRows | null;
-  error: string | null;
-  explainDialogOpen: boolean;
-  explainResult: BasicQueryExplanation | SemiJoinQueryExplanation | string | null;
-  loadingExplain: boolean;
-  explainError: Error | null;
-  queryElapsed: number | null;
-}
-
-interface SqlQueryResult {
-  queryResult: HeaderRows;
-  queryElapsed: number;
-}
-
-export class SqlView extends React.PureComponent<SqlViewProps, SqlViewState> {
-  static trimSemicolon(query: string): string {
-    // Trims out a trailing semicolon while preserving space (https://bit.ly/1n1yfkJ)
-    return query.replace(/;+((?:\s*--[^\n]*)?\s*)$/, '$1');
-  }
-
-  private sqlQueryManager: QueryManager<QueryWithContext, SqlQueryResult>;
-  private explainQueryManager: QueryManager<QueryWithContext, BasicQueryExplanation | SemiJoinQueryExplanation | string>;
-
-  constructor(props: SqlViewProps, context: any) {
-    super(props, context);
-    this.state = {
-      loading: false,
-      result: null,
-      error: null,
-      explainDialogOpen: false,
-      loadingExplain: false,
-      explainResult: null,
-      explainError: null,
-      queryElapsed: null
-    };
-  }
-
-  componentDidMount(): void {
-    this.sqlQueryManager = new QueryManager({
-      processQuery: async (queryWithContext: QueryWithContext) => {
-        const { queryString, context, wrapQuery } = queryWithContext;
-        const startTime = new Date();
-
-        if (queryString.trim().startsWith('{')) {
-          // Secret way to issue a native JSON "rune" query
-          const runeQuery = Hjson.parse(queryString);
-
-          if (context) runeQuery.context = context;
-          const result = await queryDruidRune(runeQuery);
-          return {
-            queryResult: decodeRune(runeQuery, result),
-            queryElapsed: new Date().valueOf() - startTime.valueOf()
-          };
-
-        } else {
-          const actualQuery = wrapQuery ?
-            `SELECT * FROM (${SqlView.trimSemicolon(queryString)}\n) LIMIT 2000` :
-            queryString;
-
-          const queryPayload: Record<string, any> = {
-            query: actualQuery,
-            resultFormat: 'array',
-            header: true
-          };
-
-          if (context) queryPayload.context = context;
-          const result = await queryDruidSql(queryPayload);
-
-          return {
-            queryResult: {
-              header: (result && result.length) ? result[0] : [],
-              rows: (result && result.length) ? result.slice(1) : []
-            },
-            queryElapsed: new Date().valueOf() - startTime.valueOf()
-          };
-        }
-      },
-      onStateChange: ({ result, loading, error }) => {
-        this.setState({
-          result: result ? result.queryResult : null,
-          queryElapsed: result ? result.queryElapsed : null,
-          loading,
-          error
-        });
-      }
-    });
-
-    this.explainQueryManager = new QueryManager({
-      processQuery: async (queryWithContext: QueryWithContext) => {
-        const { queryString, context } = queryWithContext;
-        const explainPayload: Record<string, any> = {
-          query: `EXPLAIN PLAN FOR (${SqlView.trimSemicolon(queryString)}\n)`,
-          resultFormat: 'object'
-        };
-
-        if (context) explainPayload.context = context;
-        const result = await queryDruidSql(explainPayload);
-
-        return parseQueryPlan(result[0]['PLAN']);
-      },
-      onStateChange: ({ result, loading, error }) => {
-        this.setState({
-          explainResult: result,
-          loadingExplain: loading,
-          explainError: error !== null ? new Error(error) : null
-        });
-      }
-    });
-  }
-
-  componentWillUnmount(): void {
-    this.sqlQueryManager.terminate();
-    this.explainQueryManager.terminate();
-  }
-
-  onSecondaryPaneSizeChange(secondaryPaneSize: number) {
-    localStorageSet(LocalStorageKeys.QUERY_VIEW_PANE_SIZE, String(secondaryPaneSize));
-  }
-
-  formatStr(s: string | number, format: 'csv' | 'tsv') {
-    if (format === 'csv') {
-      // remove line break, single quote => double quote, handle ','
-      return `"${s.toString().replace(/(?:\r\n|\r|\n)/g, ' ').replace(/"/g, '""')}"`;
-    } else { // tsv
-      // remove line break, single quote => double quote, \t => ''
-      return `${s.toString().replace(/(?:\r\n|\r|\n)/g, ' ').replace(/\t/g, '').replace(/"/g, '""')}`;
-    }
-  }
-
-  onDownload = (format: string) => {
-    const { result } = this.state;
-    if (!result) return;
-    let data: string = '';
-    let seperator: string = '';
-    const lineBreak = '\n';
-
-    if (format === 'csv' || format === 'tsv') {
-      seperator = format === 'csv' ? ',' : '\t';
-      data = result.header.map(str => this.formatStr(str, format)).join(seperator) + lineBreak;
-      data += result.rows.map(r => r.map(cell => this.formatStr(cell, format)).join(seperator)).join(lineBreak);
-    } else { // json
-      data = result.rows.map(r => {
-        const outputObject: Record<string, any> = {};
-        for (let k = 0; k < r.length; k++) {
-          const newName = result.header[k];
-          if (newName) {
-            outputObject[newName] = r[k];
-          }
-        }
-        return JSON.stringify(outputObject);
-      }).join(lineBreak);
-    }
-    downloadFile(data, format, 'query_result.' + format);
-  }
-
-  renderExplainDialog() {
-    const {explainDialogOpen, explainResult, loadingExplain, explainError} = this.state;
-    if (!loadingExplain && explainDialogOpen) {
-      return <QueryPlanDialog
-        explainResult={explainResult}
-        explainError={explainError}
-        onClose={() => this.setState({explainDialogOpen: false})}
-      />;
-    }
-    return null;
-  }
-
-  renderResultTable() {
-    const { result, loading, error } = this.state;
-
-    return <ReactTable
-      data={result ? result.rows : []}
-      loading={loading}
-      noDataText={!loading && result && !result.rows.length ? 'No results' : (error || '')}
-      sortable={false}
-      columns={
-        (result ? result.header : []).map((h: any, i) => {
-          return {
-            Header: h,
-            accessor: String(i),
-            Cell: row => <TableCell value={row.value}/>
-          };
-        })
-      }
-    />;
-  }
-
-  render() {
-    const { initSql } = this.props;
-    const { queryElapsed } = this.state;
-
-    return <SplitterLayout
-      customClassName="sql-view app-view"
-      vertical
-      percentage
-      secondaryInitialSize={Number(localStorageGet(LocalStorageKeys.QUERY_VIEW_PANE_SIZE) as string) || 60}
-      primaryMinSize={30}
-      secondaryMinSize={30}
-      onSecondaryPaneSizeChange={this.onSecondaryPaneSizeChange}
-    >
-      <div className="top-pane">
-        <SqlControl
-          initSql={initSql || localStorageGet(LocalStorageKeys.QUERY_KEY)}
-          onRun={(queryString, context, wrapQuery) => {
-            localStorageSet(LocalStorageKeys.QUERY_KEY, queryString);
-            this.sqlQueryManager.runQuery({ queryString, context, wrapQuery });
-          }}
-          onExplain={(queryString, context) => {
-            this.setState({ explainDialogOpen: true });
-            this.explainQueryManager.runQuery({ queryString, context });
-          }}
-          queryElapsed={queryElapsed}
-          onDownload={this.onDownload}
-        />
-      </div>
-      <div className="bottom-pane">
-        {this.renderResultTable()}
-        {this.renderExplainDialog()}
-      </div>
-    </SplitterLayout>;
-  }
-}
diff --git a/web-console/src/views/task-view/__snapshots__/tasks-view.spec.tsx.snap b/web-console/src/views/task-view/__snapshots__/tasks-view.spec.tsx.snap
index a01963f..8ba938c 100644
--- a/web-console/src/views/task-view/__snapshots__/tasks-view.spec.tsx.snap
+++ b/web-console/src/views/task-view/__snapshots__/tasks-view.spec.tsx.snap
@@ -25,11 +25,52 @@ exports[`tasks view matches snapshot 1`] = `
           onClick={[Function]}
           text="Refresh"
         />
-        <Blueprint3.Button
-          icon="plus"
-          onClick={[Function]}
-          text="Submit supervisor"
-        />
+        <Blueprint3.Popover
+          boundary="scrollParent"
+          captureDismiss={false}
+          content={
+            <Blueprint3.Menu>
+              <Blueprint3.MenuItem
+                disabled={false}
+                icon="cloud-upload"
+                multiline={false}
+                onClick={[Function]}
+                popoverProps={Object {}}
+                shouldDismissPopover={true}
+                text="Go to data loader"
+              />
+              <Blueprint3.MenuItem
+                disabled={false}
+                icon="manually-entered-data"
+                multiline={false}
+                onClick={[Function]}
+                popoverProps={Object {}}
+                shouldDismissPopover={true}
+                text="Submit JSON supervisor"
+              />
+            </Blueprint3.Menu>
+          }
+          defaultIsOpen={false}
+          disabled={false}
+          hasBackdrop={false}
+          hoverCloseDelay={300}
+          hoverOpenDelay={150}
+          inheritDarkTheme={true}
+          interactionKind="click"
+          minimal={false}
+          modifiers={Object {}}
+          openOnTargetFocus={true}
+          position="bottom-left"
+          targetTagName="span"
+          transitionDuration={300}
+          usePortal={true}
+          wrapperTagName="span"
+        >
+          <Blueprint3.Button
+            icon="plus"
+            text="Submit supervisor"
+          />
+        </Blueprint3.Popover>
         <TableColumnSelector
           columns={
             Array [
@@ -325,19 +366,21 @@ exports[`tasks view matches snapshot 1`] = `
             <Blueprint3.Menu>
               <Blueprint3.MenuItem
                 disabled={false}
+                icon="cloud-upload"
                 multiline={false}
                 onClick={[Function]}
                 popoverProps={Object {}}
                 shouldDismissPopover={true}
-                text="Raw JSON task"
+                text="Go to data loader"
               />
               <Blueprint3.MenuItem
                 disabled={false}
+                icon="manually-entered-data"
                 multiline={false}
                 onClick={[Function]}
                 popoverProps={Object {}}
                 shouldDismissPopover={true}
-                text="Go to data loader"
+                text="Submit JSON task"
               />
             </Blueprint3.Menu>
           }
diff --git a/web-console/src/views/task-view/tasks-view.spec.tsx b/web-console/src/views/task-view/tasks-view.spec.tsx
index 2e962f4..4736241 100644
--- a/web-console/src/views/task-view/tasks-view.spec.tsx
+++ b/web-console/src/views/task-view/tasks-view.spec.tsx
@@ -27,7 +27,7 @@ describe('tasks view', () => {
       <TasksView
         openDialog={'test'}
         taskId={'test'}
-        goToSql={(initSql: string) => null}
+        goToQuery={(initSql: string) => null}
         goToMiddleManager={(middleManager: string) => null}
         goToLoadDataView={() => null}
         noSqlMode={false}
diff --git a/web-console/src/views/task-view/tasks-view.tsx b/web-console/src/views/task-view/tasks-view.tsx
index b2441ca..7b1f532 100644
--- a/web-console/src/views/task-view/tasks-view.tsx
+++ b/web-console/src/views/task-view/tasks-view.tsx
@@ -46,7 +46,7 @@ const taskTableColumns: string[] = ['Task ID', 'Type', 'Datasource', 'Location',
 export interface TasksViewProps extends React.Props<any> {
   taskId: string | null;
   openDialog: string | null;
-  goToSql: (initSql: string) => void;
+  goToQuery: (initSql: string) => void;
   goToMiddleManager: (middleManager: string) => void;
   goToLoadDataView: (supervisorId?: string, taskId?: string) => void;
   noSqlMode: boolean;
@@ -709,18 +709,34 @@ ORDER BY "rank" DESC, "created_time" DESC`);
 
 
   render() {
-    const { goToSql, goToLoadDataView, noSqlMode } = this.props;
+    const { goToQuery, goToLoadDataView, noSqlMode } = this.props;
     const { groupTasksBy, supervisorSpecDialogOpen, taskSpecDialogOpen, alertErrorMsg, taskTableActionDialogId, taskTableActionDialogActions, supervisorTableActionDialogId, supervisorTableActionDialogActions, taskTableActionDialogStatus } = this.state;
     const { supervisorTableColumnSelectionHandler, taskTableColumnSelectionHandler } = this;
-    const submitTaskMenu = <Menu>
+
+    const submitSupervisorMenu = <Menu>
       <MenuItem
-        text="Raw JSON task"
-        onClick={() => this.setState({ taskSpecDialogOpen: true })}
+        icon={IconNames.CLOUD_UPLOAD}
+        text="Go to data loader"
+        onClick={() => goToLoadDataView()}
+      />
+      <MenuItem
+        icon={IconNames.MANUALLY_ENTERED_DATA}
+        text="Submit JSON supervisor"
+        onClick={() => this.setState({ supervisorSpecDialogOpen: true })}
       />
+    </Menu>;
+
+    const submitTaskMenu = <Menu>
       <MenuItem
+        icon={IconNames.CLOUD_UPLOAD}
         text="Go to data loader"
         onClick={() => goToLoadDataView()}
       />
+      <MenuItem
+        icon={IconNames.MANUALLY_ENTERED_DATA}
+        text="Submit JSON task"
+        onClick={() => this.setState({ taskSpecDialogOpen: true })}
+      />
     </Menu>;
 
     return <>
@@ -740,11 +756,9 @@ ORDER BY "rank" DESC, "created_time" DESC`);
               text="Refresh"
               onClick={() => this.supervisorQueryManager.rerunLastQuery()}
             />
-            <Button
-              icon={IconNames.PLUS}
-              text="Submit supervisor"
-              onClick={() => this.setState({ supervisorSpecDialogOpen: true })}
-            />
+            <Popover content={submitSupervisorMenu} position={Position.BOTTOM_LEFT}>
+              <Button icon={IconNames.PLUS} text="Submit supervisor"/>
+            </Popover>
             <TableColumnSelector
               columns={supervisorTableColumns}
               onChange={(column) => supervisorTableColumnSelectionHandler.changeTableColumnSelector(column)}
@@ -772,7 +786,7 @@ ORDER BY "rank" DESC, "created_time" DESC`);
               <Button
                 icon={IconNames.APPLICATION}
                 text="Go to SQL"
-                onClick={() => goToSql(this.taskQueryManager.getLastQuery())}
+                onClick={() => goToQuery(this.taskQueryManager.getLastQuery())}
               />
             }
             <Popover content={submitTaskMenu} position={Position.BOTTOM_LEFT}>


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@druid.apache.org
For additional commands, e-mail: commits-help@druid.apache.org