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/08/25 23:56:37 UTC

[incubator-druid] branch master updated: Web console: Save query context also (#8395)

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 9254dc8  Web console: Save query context also (#8395)
9254dc8 is described below

commit 9254dc8b805ec8ac98e61e4e40f68626c91949d0
Author: Vadim Ogievetsky <va...@gmail.com>
AuthorDate: Sun Aug 25 16:56:27 2019 -0700

    Web console: Save query context also (#8395)
    
    * tidy up menus
    
    * fix query output API
    
    * save context also
    
    * pull out auto run into a switch
    
    * better copy
    
    * add group_id
    
    * support FLOAT also
    
    * use built in time logic
    
    * fix trunc direction
    
    * add skipCache
    
    * add manifest url
    
    * remove depricated props
---
 web-console/console-config.js                      |   5 +-
 web-console/package-lock.json                      |   6 +-
 web-console/package.json                           |   2 +-
 .../src/components/action-cell/action-cell.tsx     |   4 -
 web-console/src/components/deferred/deferred.tsx   |   4 -
 .../components/refresh-button/refresh-button.tsx   |   5 +-
 .../segment-timeline/segment-timeline.tsx          |  17 +-
 .../src/components/show-value/show-value.tsx       |   4 -
 .../edit-context-dialog/edit-context-dialog.tsx    |   8 +-
 web-console/src/dialogs/index.ts                   |   1 +
 .../query-history-dialog/query-history-dialog.tsx  |  18 +-
 .../__snapshots__/query-plan-dialog.spec.tsx.snap  |  14 +-
 .../query-plan-dialog/query-plan-dialog.scss       |   4 +
 .../query-plan-dialog/query-plan-dialog.tsx        |  39 +-
 .../show-value-dialog/show-value-dialog.tsx        |   1 +
 web-console/src/utils/local-storage-keys.tsx       |  15 +
 web-console/src/utils/query-context.tsx            |   1 +
 web-console/src/utils/sampler.ts                   |   2 +-
 web-console/src/views/index.ts                     |   1 +
 .../src/views/load-data-view/load-data-view.tsx    |   3 +-
 .../__snapshots__/query-view.spec.tsx.snap         |  29 +-
 .../__snapshots__/column-tree.spec.tsx.snap        | 184 +++++++++-
 .../column-tree/column-tree-menu/index.ts}         |   4 +-
 .../number-menu-items/number-menu-items.spec.tsx   |   9 +-
 .../number-menu-items/number-menu-items.tsx        | 141 ++++---
 .../string-menu-items/string-menu-items.spec.tsx   |   9 +-
 .../string-menu-items/string-menu-items.tsx        | 157 ++++----
 .../time-menu-items/time-menu-items.spec.tsx       |   9 +-
 .../time-menu-items/time-menu-items.tsx            | 403 ++++++++++-----------
 .../query-view/column-tree/column-tree.spec.tsx    |  25 +-
 .../views/query-view/column-tree/column-tree.tsx   | 291 +++++++--------
 .../query-view/query-output/query-output.spec.tsx  |   9 +-
 .../views/query-view/query-output/query-output.tsx |  43 ++-
 web-console/src/views/query-view/query-view.scss   |   1 +
 web-console/src/views/query-view/query-view.tsx    | 229 ++++--------
 .../query-view/run-button/run-button.spec.tsx      |   4 +-
 .../src/views/query-view/run-button/run-button.tsx |  32 +-
 .../__snapshots__/tasks-view.spec.tsx.snap         |  14 +
 web-console/src/views/task-view/tasks-view.tsx     |  18 +-
 web-console/src/visualization/bar-group.tsx        |   2 +-
 web-console/src/visualization/bar-unit.tsx         |   2 +-
 web-console/src/visualization/chart-axis.tsx       |   2 +-
 .../src/visualization/stacked-bar-chart.tsx        |   2 +-
 43 files changed, 908 insertions(+), 865 deletions(-)

diff --git a/web-console/console-config.js b/web-console/console-config.js
index 127d3a0..6fac8a2 100644
--- a/web-console/console-config.js
+++ b/web-console/console-config.js
@@ -16,4 +16,7 @@
  * limitations under the License.
  */
 
-window.consoleConfig = { /* future configs may go here */ };
+window.consoleConfig = {
+  "exampleManifestsUrl": "https://druid.apache.org/data/example-manifests.tsv"
+  /* future configs may go here */
+};
diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index 87bb391..a4ae431 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -4428,9 +4428,9 @@
       "integrity": "sha512-0sYnfUHHMoajaud/i5BHKA12bUxiWEHJ9rxGqVEppFxsEcxef0TZQ5J59lU+UniEBcz/sG5fTESRyS7cOm3tSQ=="
     },
     "druid-query-toolkit": {
-      "version": "0.3.24",
-      "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.3.24.tgz",
-      "integrity": "sha512-kFvEXAjjNuJYpeRsAzzO/cJ2rr4nHBGTSCAA4UPxyt4pKNZE/OUap7IQbsdnxYmhkHgfjUBGcFteufaVHSn7SA==",
+      "version": "0.3.26",
+      "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.3.26.tgz",
+      "integrity": "sha512-j9HcwHCx2YnFSefYc1oJDw8rPq5zSB0tpGkaMp2GkO9syKbdncKfUPugZ613c5XIOBe+j5Hqh/luqh4sLacHGQ==",
       "requires": {
         "tslib": "^1.10.0"
       }
diff --git a/web-console/package.json b/web-console/package.json
index a221cc8..553938f 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -61,7 +61,7 @@
     "d3": "^5.10.1",
     "d3-array": "^2.3.1",
     "druid-console": "0.0.2",
-    "druid-query-toolkit": "^0.3.24",
+    "druid-query-toolkit": "^0.3.26",
     "file-saver": "^2.0.2",
     "has-own-prop": "^2.0.0",
     "hjson": "^3.1.2",
diff --git a/web-console/src/components/action-cell/action-cell.tsx b/web-console/src/components/action-cell/action-cell.tsx
index 248c5ea..f48fc4c 100644
--- a/web-console/src/components/action-cell/action-cell.tsx
+++ b/web-console/src/components/action-cell/action-cell.tsx
@@ -35,10 +35,6 @@ export class ActionCell extends React.PureComponent<ActionCellProps> {
   static COLUMN_LABEL = 'Actions';
   static COLUMN_WIDTH = 70;
 
-  constructor(props: ActionCellProps, context: any) {
-    super(props, context);
-  }
-
   render(): JSX.Element {
     const { onDetail, actions } = this.props;
     const actionsMenu = actions ? basicActionsToMenu(actions) : null;
diff --git a/web-console/src/components/deferred/deferred.tsx b/web-console/src/components/deferred/deferred.tsx
index 6194979..7e6cde3 100644
--- a/web-console/src/components/deferred/deferred.tsx
+++ b/web-console/src/components/deferred/deferred.tsx
@@ -25,10 +25,6 @@ export interface DeferredProps {
 export interface DeferredState {}
 
 export class Deferred extends React.PureComponent<DeferredProps, DeferredState> {
-  constructor(props: DeferredProps, context: any) {
-    super(props, context);
-  }
-
   render(): JSX.Element {
     const { content } = this.props;
     return content();
diff --git a/web-console/src/components/refresh-button/refresh-button.tsx b/web-console/src/components/refresh-button/refresh-button.tsx
index 9393e50..cfe68e5 100644
--- a/web-console/src/components/refresh-button/refresh-button.tsx
+++ b/web-console/src/components/refresh-button/refresh-button.tsx
@@ -15,6 +15,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 import { IconNames } from '@blueprintjs/icons';
 import React from 'react';
 
@@ -27,10 +28,6 @@ export interface RefreshButtonProps {
 }
 
 export class RefreshButton extends React.PureComponent<RefreshButtonProps> {
-  constructor(props: RefreshButtonProps, context: any) {
-    super(props, context);
-  }
-
   render(): JSX.Element {
     const { onRefresh, localStorageKey } = this.props;
     const intervals = [
diff --git a/web-console/src/components/segment-timeline/segment-timeline.tsx b/web-console/src/components/segment-timeline/segment-timeline.tsx
index 34d3a88..dd9eea7 100644
--- a/web-console/src/components/segment-timeline/segment-timeline.tsx
+++ b/web-console/src/components/segment-timeline/segment-timeline.tsx
@@ -27,7 +27,7 @@ import { Loader } from '../loader/loader';
 
 import './segment-timeline.scss';
 
-interface SegmentTimelineProps extends React.Props<any> {
+interface SegmentTimelineProps {
   chartHeight: number;
   chartWidth: number;
 }
@@ -71,9 +71,7 @@ export interface BarChartMargin {
 }
 
 export class SegmentTimeline extends React.Component<SegmentTimelineProps, SegmentTimelineState> {
-  private dataQueryManager: QueryManager<null, any>;
-  private datasourceQueryManager: QueryManager<null, any>;
-  private colors = [
+  static COLORS = [
     '#b33040',
     '#d25c4d',
     '#f2b447',
@@ -91,6 +89,13 @@ export class SegmentTimeline extends React.Component<SegmentTimelineProps, Segme
     '#915412',
     '#87606c',
   ];
+
+  static getColor(index: number): string {
+    return SegmentTimeline.COLORS[index % SegmentTimeline.COLORS.length];
+  }
+
+  private dataQueryManager: QueryManager<null, any>;
+  private datasourceQueryManager: QueryManager<null, any>;
   private chartMargin = { top: 20, right: 10, bottom: 20, left: 10 };
 
   constructor(props: SegmentTimelineProps) {
@@ -249,7 +254,7 @@ export class SegmentTimeline extends React.Component<SegmentTimelineProps, Segme
             y: d[datasource] === undefined ? 0 : d[datasource],
             y0,
             datasource,
-            color: this.colors[i],
+            color: SegmentTimeline.getColor(i),
           };
           y0 += d[datasource] === undefined ? 0 : d[datasource];
           return barUnitData;
@@ -279,7 +284,7 @@ export class SegmentTimeline extends React.Component<SegmentTimelineProps, Segme
             x: d.day,
             y,
             datasource,
-            color: this.colors[i],
+            color: SegmentTimeline.getColor(i),
           };
         });
         if (!dataResult.every((d: any) => d.y === 0)) {
diff --git a/web-console/src/components/show-value/show-value.tsx b/web-console/src/components/show-value/show-value.tsx
index f30f4c7..d1e6ce4 100644
--- a/web-console/src/components/show-value/show-value.tsx
+++ b/web-console/src/components/show-value/show-value.tsx
@@ -31,10 +31,6 @@ export interface ShowValueProps {
 }
 
 export class ShowValue extends React.PureComponent<ShowValueProps> {
-  constructor(props: ShowValueProps, context: any) {
-    super(props, context);
-  }
-
   render(): JSX.Element {
     const { endpoint, downloadFilename, jsonValue } = this.props;
     return (
diff --git a/web-console/src/dialogs/edit-context-dialog/edit-context-dialog.tsx b/web-console/src/dialogs/edit-context-dialog/edit-context-dialog.tsx
index 5a6e320..89e6dca 100644
--- a/web-console/src/dialogs/edit-context-dialog/edit-context-dialog.tsx
+++ b/web-console/src/dialogs/edit-context-dialog/edit-context-dialog.tsx
@@ -15,6 +15,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 import { Button, Callout, Classes, Dialog, Intent, TextArea } from '@blueprintjs/core';
 import Hjson from 'hjson';
 import React from 'react';
@@ -42,6 +43,7 @@ export class EditContextDialog extends React.PureComponent<
   constructor(props: EditContextDialogProps) {
     super(props);
     this.state = {
+      queryContext: props.queryContext,
       queryContextString: Object.keys(props.queryContext).length
         ? JSON.stringify(props.queryContext, undefined, 2)
         : '{\n\n}',
@@ -72,10 +74,12 @@ export class EditContextDialog extends React.PureComponent<
   };
 
   private handleSave = () => {
-    const { onQueryContextChange } = this.props;
+    const { onQueryContextChange, onClose } = this.props;
     const { queryContext } = this.state;
     if (!queryContext) return;
+
     onQueryContextChange(queryContext);
+    onClose();
   };
 
   render(): JSX.Element {
@@ -94,9 +98,9 @@ export class EditContextDialog extends React.PureComponent<
           <div className={'edit-context-dialog-buttons'}>
             <Button text={'Close'} onClick={onClose} />
             <Button
-              disabled={Boolean(error)}
               text={'Save'}
               intent={Intent.PRIMARY}
+              disabled={Boolean(error)}
               onClick={this.handleSave}
             />
           </div>
diff --git a/web-console/src/dialogs/index.ts b/web-console/src/dialogs/index.ts
index 27bd358..0972cdc 100644
--- a/web-console/src/dialogs/index.ts
+++ b/web-console/src/dialogs/index.ts
@@ -15,6 +15,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 export * from './about-dialog/about-dialog';
 export * from './async-action-dialog/async-action-dialog';
 export * from './compaction-dialog/compaction-dialog';
diff --git a/web-console/src/dialogs/query-history-dialog/query-history-dialog.tsx b/web-console/src/dialogs/query-history-dialog/query-history-dialog.tsx
index 129ef82..813b3b6 100644
--- a/web-console/src/dialogs/query-history-dialog/query-history-dialog.tsx
+++ b/web-console/src/dialogs/query-history-dialog/query-history-dialog.tsx
@@ -26,9 +26,10 @@ import './query-history-dialog.scss';
 export interface QueryRecord {
   version: string;
   queryString: string;
+  queryContext?: Record<string, any>;
 }
 export interface QueryHistoryDialogProps {
-  setQueryString: (queryString: string) => void;
+  setQueryString: (queryString: string, queryContext: Record<string, any>) => void;
   onClose: () => void;
   queryRecords: readonly QueryRecord[];
 }
@@ -51,9 +52,14 @@ export class QueryHistoryDialog extends React.PureComponent<
   static addQueryToHistory(
     queryHistory: readonly QueryRecord[],
     queryString: string,
+    queryContext: Record<string, any>,
   ): readonly QueryRecord[] {
-    // Do not add to history if already the same as the last element
-    if (queryHistory.length && queryHistory[0].queryString === queryString) {
+    // Do not add to history if already the same as the last element in query and context
+    if (
+      queryHistory.length &&
+      queryHistory[0].queryString === queryString &&
+      JSON.stringify(queryHistory[0].queryContext) === JSON.stringify(queryContext)
+    ) {
       return queryHistory;
     }
 
@@ -61,7 +67,8 @@ export class QueryHistoryDialog extends React.PureComponent<
       {
         version: QueryHistoryDialog.getHistoryVersion(),
         queryString,
-      },
+        queryContext,
+      } as QueryRecord,
     ]
       .concat(queryHistory)
       .slice(0, 10);
@@ -77,8 +84,9 @@ export class QueryHistoryDialog extends React.PureComponent<
   private handleSelect = () => {
     const { queryRecords, setQueryString, onClose } = this.props;
     const { activeTab } = this.state;
+    const queryRecord = queryRecords[activeTab];
 
-    setQueryString(queryRecords[activeTab].queryString);
+    setQueryString(queryRecord.queryString, queryRecord.queryContext || {});
     onClose();
   };
 
diff --git a/web-console/src/dialogs/query-plan-dialog/__snapshots__/query-plan-dialog.spec.tsx.snap b/web-console/src/dialogs/query-plan-dialog/__snapshots__/query-plan-dialog.spec.tsx.snap
index e91d4b8..e5e442e 100644
--- a/web-console/src/dialogs/query-plan-dialog/__snapshots__/query-plan-dialog.spec.tsx.snap
+++ b/web-console/src/dialogs/query-plan-dialog/__snapshots__/query-plan-dialog.spec.tsx.snap
@@ -55,7 +55,9 @@ exports[`query plan dialog matches snapshot 1`] = `
         <div
           class="bp3-dialog-body"
         >
-          <div>
+          <div
+            class="generic-result"
+          >
             test
           </div>
         </div>
@@ -75,16 +77,6 @@ exports[`query plan dialog matches snapshot 1`] = `
                 Close
               </span>
             </button>
-            <button
-              class="bp3-button bp3-intent-primary"
-              type="button"
-            >
-              <span
-                class="bp3-button-text"
-              >
-                Open
-              </span>
-            </button>
           </div>
         </div>
       </div>
diff --git a/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.scss b/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.scss
index 2835b79..e7797a2 100644
--- a/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.scss
+++ b/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.scss
@@ -36,4 +36,8 @@
       height: 25vh !important;
     }
   }
+
+  .generic-result {
+    overflow: scroll;
+  }
 }
diff --git a/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.tsx b/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.tsx
index 6297e0c..8959cf7 100644
--- a/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.tsx
+++ b/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.tsx
@@ -38,23 +38,16 @@ export interface QueryPlanDialogProps {
   setQueryString: (queryString: string) => void;
 }
 
-export interface QueryPlanDialogState {}
-
-export class QueryPlanDialog extends React.PureComponent<
-  QueryPlanDialogProps,
-  QueryPlanDialogState
-> {
+export class QueryPlanDialog extends React.PureComponent<QueryPlanDialogProps> {
   constructor(props: QueryPlanDialogProps) {
     super(props);
-    this.state = {};
   }
 
-  private queryString: string = '';
-
   render(): JSX.Element {
     const { explainResult, explainError, onClose, setQueryString } = this.props;
 
     let content: JSX.Element;
+    let queryString: string | undefined;
 
     if (explainError) {
       content = <div>{explainError}</div>;
@@ -71,15 +64,11 @@ export class QueryPlanDialog extends React.PureComponent<
         );
       }
 
-      this.queryString = JSON.stringify(
-        (explainResult as BasicQueryExplanation).query[0],
-        undefined,
-        2,
-      );
+      queryString = JSON.stringify((explainResult as BasicQueryExplanation).query[0], undefined, 2);
       content = (
         <div className="one-query">
           <FormGroup label="Query">
-            <TextArea readOnly value={this.queryString} />
+            <TextArea readOnly value={queryString} />
           </FormGroup>
           {signature}
         </div>
@@ -136,7 +125,7 @@ export class QueryPlanDialog extends React.PureComponent<
         </div>
       );
     } else {
-      content = <div>{explainResult}</div>;
+      content = <div className="generic-result">{explainResult}</div>;
     }
 
     return (
@@ -145,14 +134,16 @@ export class QueryPlanDialog extends React.PureComponent<
         <div className={Classes.DIALOG_FOOTER}>
           <div className={Classes.DIALOG_FOOTER_ACTIONS}>
             <Button text="Close" onClick={onClose} />
-            <Button
-              text="Open"
-              intent={Intent.PRIMARY}
-              onClick={() => {
-                setQueryString(this.queryString);
-                onClose();
-              }}
-            />
+            {queryString && (
+              <Button
+                text="Open query"
+                intent={Intent.PRIMARY}
+                onClick={() => {
+                  if (queryString) setQueryString(queryString);
+                  onClose();
+                }}
+              />
+            )}
           </div>
         </div>
       </Dialog>
diff --git a/web-console/src/dialogs/show-value-dialog/show-value-dialog.tsx b/web-console/src/dialogs/show-value-dialog/show-value-dialog.tsx
index 60f4a2f..4689dd4 100644
--- a/web-console/src/dialogs/show-value-dialog/show-value-dialog.tsx
+++ b/web-console/src/dialogs/show-value-dialog/show-value-dialog.tsx
@@ -15,6 +15,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 import { Button, Classes, Dialog, Intent, TextArea } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import copy from 'copy-to-clipboard';
diff --git a/web-console/src/utils/local-storage-keys.tsx b/web-console/src/utils/local-storage-keys.tsx
index 4f116df..b8fc6f1 100644
--- a/web-console/src/utils/local-storage-keys.tsx
+++ b/web-console/src/utils/local-storage-keys.tsx
@@ -26,6 +26,7 @@ export const LocalStorageKeys = {
   SERVER_TABLE_COLUMN_SELECTION: 'historical-table-column-selection' as 'historical-table-column-selection',
   LOOKUP_TABLE_COLUMN_SELECTION: 'lookup-table-column-selection' as 'lookup-table-column-selection',
   QUERY_KEY: 'druid-console-query' as 'druid-console-query',
+  QUERY_CONTEXT: 'query-context' as 'query-context',
   TASKS_VIEW_PANE_SIZE: 'tasks-view-pane-size' as 'tasks-view-pane-size',
   QUERY_VIEW_PANE_SIZE: 'query-view-pane-size' as 'query-view-pane-size',
   TASKS_REFRESH_RATE: 'task-refresh-rate' as 'task-refresh-rate',
@@ -46,7 +47,21 @@ export function localStorageSet(key: LocalStorageKeys, value: string): void {
   localStorage.setItem(key, value);
 }
 
+export function localStorageSetJson(key: LocalStorageKeys, value: any): void {
+  localStorageSet(key, JSON.stringify(value));
+}
+
 export function localStorageGet(key: LocalStorageKeys): string | undefined {
   if (typeof localStorage === 'undefined') return;
   return localStorage.getItem(key) || undefined;
 }
+
+export function localStorageGetJson(key: LocalStorageKeys): any {
+  const value = localStorageGet(key);
+  if (!value) return;
+  try {
+    return JSON.parse(value);
+  } catch {
+    return;
+  }
+}
diff --git a/web-console/src/utils/query-context.tsx b/web-console/src/utils/query-context.tsx
index d470885..4449c5d 100644
--- a/web-console/src/utils/query-context.tsx
+++ b/web-console/src/utils/query-context.tsx
@@ -23,6 +23,7 @@ export interface QueryContext {
   populateCache?: boolean | undefined;
   useApproximateCountDistinct?: boolean | undefined;
   useApproximateTopN?: boolean | undefined;
+  [key: string]: any;
 }
 
 export function isEmptyContext(context: QueryContext): boolean {
diff --git a/web-console/src/utils/sampler.ts b/web-console/src/utils/sampler.ts
index 144dc73..bd33dd2 100644
--- a/web-console/src/utils/sampler.ts
+++ b/web-console/src/utils/sampler.ts
@@ -555,7 +555,7 @@ export async function sampleForExampleManifests(
         },
       },
     },
-    samplerConfig: { numRows: 50, timeoutMs: 10000 },
+    samplerConfig: { numRows: 50, timeoutMs: 10000, skipCache: true },
   };
 
   const exampleData = await postToSampler(sampleSpec, 'example-manifest');
diff --git a/web-console/src/views/index.ts b/web-console/src/views/index.ts
index 0a4a0bb..bea772f 100644
--- a/web-console/src/views/index.ts
+++ b/web-console/src/views/index.ts
@@ -15,6 +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';
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 47be1c6..71d71c8 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
@@ -1044,8 +1044,9 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
             </FormGroup>
           )}
           <Button
-            text={inlineMode ? 'Register' : 'Preview'}
+            text={inlineMode ? 'Register data' : 'Preview'}
             disabled={isBlank}
+            intent={inputQueryState.data ? undefined : Intent.PRIMARY}
             onClick={() => this.queryForConnect()}
           />
         </div>
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
index 21f6ac7..38b279c 100644
--- 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
@@ -5,16 +5,10 @@ exports[`sql view matches snapshot 1`] = `
   className="query-view app-view"
 >
   <ColumnTree
-    addAggregateColumn={[Function]}
-    addFunctionToGroupBy={[Function]}
-    addToGroupBy={[Function]}
-    clear={[Function]}
     columnMetadataLoading={true}
     defaultSchema="druid"
-    filterByRow={[Function]}
+    getParsedQuery={[Function]}
     onQueryStringChange={[Function]}
-    queryAst={[Function]}
-    replaceFrom={[Function]}
   />
   <t
     customClassName=""
@@ -41,8 +35,6 @@ exports[`sql view matches snapshot 1`] = `
         className="control-bar"
       >
         <HotkeysTarget(RunButton)
-          autoRun={true}
-          onAutoRunChange={[Function]}
           onEditContext={[Function]}
           onExplain={[Function]}
           onHistory={[Function]}
@@ -52,7 +44,20 @@ exports[`sql view matches snapshot 1`] = `
           runeMode={false}
         />
         <Blueprint3.Tooltip
-          content="Automatically wrap the query with a limit to protect against queries with very large result sets"
+          content="Automatically run queries when modified via helper action menus."
+          hoverCloseDelay={0}
+          hoverOpenDelay={800}
+          transitionDuration={100}
+        >
+          <Blueprint3.Switch
+            checked={true}
+            className="auto-run"
+            label="Auto run"
+            onChange={[Function]}
+          />
+        </Blueprint3.Tooltip>
+        <Blueprint3.Tooltip
+          content="Automatically wrap the query with a limit to protect against queries with very large result sets."
           hoverCloseDelay={0}
           hoverOpenDelay={800}
           transitionDuration={100}
@@ -68,10 +73,8 @@ exports[`sql view matches snapshot 1`] = `
     </div>
     <QueryOutput
       loading={false}
+      onQueryChange={[Function]}
       runeMode={false}
-      sqlExcludeColumn={[Function]}
-      sqlFilterRow={[Function]}
-      sqlOrderBy={[Function]}
     />
   </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
index 2a29f0b..3ce9146 100644
--- 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
@@ -49,13 +49,13 @@ exports[`column tree matches snapshot 1`] = `
         class="bp3-tree-node-list bp3-tree-root"
       >
         <li
-          class="bp3-tree-node"
+          class="bp3-tree-node bp3-tree-node-expanded"
         >
           <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"
+              class="bp3-icon bp3-icon-chevron-right bp3-tree-node-caret bp3-tree-node-caret-open"
               icon="chevron-right"
             >
               <svg
@@ -104,7 +104,7 @@ exports[`column tree matches snapshot 1`] = `
                   <div
                     class=""
                   >
-                    deletion-tutorial
+                    wikipedia
                   </div>
                 </span>
               </span>
@@ -112,12 +112,186 @@ exports[`column tree matches snapshot 1`] = `
           </div>
           <div
             class="bp3-collapse"
+            style="height: auto; overflow-y: visible; transition: none;"
           >
             <div
               aria-hidden="false"
               class="bp3-collapse-body"
-              style="transform: translateY(-0px);"
-            />
+              style="transform: translateY(0); transition: none;"
+            >
+              <ul
+                class="bp3-tree-node-list"
+              >
+                <li
+                  class="bp3-tree-node"
+                >
+                  <div
+                    class="bp3-tree-node-content bp3-tree-node-content-1"
+                  >
+                    <span
+                      class="bp3-tree-node-caret-none"
+                    />
+                    <span
+                      class="bp3-icon bp3-icon-time bp3-tree-node-icon"
+                      icon="time"
+                    >
+                      <svg
+                        data-icon="time"
+                        height="16"
+                        viewBox="0 0 16 16"
+                        width="16"
+                      >
+                        <desc>
+                          time
+                        </desc>
+                        <path
+                          d="M8 0C3.58 0 0 3.58 0 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6zm1-6.41V4c0-.55-.45-1-1-1s-1 .45-1 1v4c0 .28.11.53.29.71l2 2a1.003 1.003 0 001.42-1.42L9 7.59z"
+                          fill-rule="evenodd"
+                        />
+                      </svg>
+                    </span>
+                    <span
+                      class="bp3-tree-node-label"
+                    >
+                      <span
+                        class="bp3-popover-wrapper"
+                      >
+                        <span
+                          class="bp3-popover-target bp3-popover-open"
+                        >
+                          <div
+                            class=""
+                          >
+                            __time
+                          </div>
+                        </span>
+                      </span>
+                    </span>
+                  </div>
+                  <div
+                    class="bp3-collapse"
+                  >
+                    <div
+                      aria-hidden="false"
+                      class="bp3-collapse-body"
+                      style="transform: translateY(-0px);"
+                    />
+                  </div>
+                </li>
+                <li
+                  class="bp3-tree-node"
+                >
+                  <div
+                    class="bp3-tree-node-content bp3-tree-node-content-1"
+                  >
+                    <span
+                      class="bp3-tree-node-caret-none"
+                    />
+                    <span
+                      class="bp3-icon bp3-icon-numerical bp3-tree-node-icon"
+                      icon="numerical"
+                    >
+                      <svg
+                        data-icon="numerical"
+                        height="16"
+                        viewBox="0 0 16 16"
+                        width="16"
+                      >
+                        <desc>
+                          numerical
+                        </desc>
+                        <path
+                          d="M2.79 4.61c-.13.17-.29.3-.48.41-.18.11-.39.18-.62.23-.23.04-.46.07-.71.07v1.03h1.74V12h1.26V4h-.94c-.04.23-.12.44-.25.61zm4.37 5.31c.18-.14.37-.28.58-.42l.63-.45c.21-.16.41-.33.61-.51s.37-.38.52-.59c.15-.21.28-.45.37-.7.09-.25.13-.54.13-.85 0-.25-.04-.52-.12-.8-.07-.29-.2-.55-.39-.79a2.18 2.18 0 00-.73-.6c-.29-.15-.66-.23-1.11-.23-.41 0-.77.08-1.08.23-.31.16-.58.37-.79.64-.22.27-.38.59-.49.96-.11.37-.16.77-.16 1.2h1.19c.01-.27.03-.53.08-.77.04-.24.11-.45.21-. [...]
+                          fill-rule="evenodd"
+                        />
+                      </svg>
+                    </span>
+                    <span
+                      class="bp3-tree-node-label"
+                    >
+                      <span
+                        class="bp3-popover-wrapper"
+                      >
+                        <span
+                          class="bp3-popover-target bp3-popover-open"
+                        >
+                          <div
+                            class=""
+                          >
+                            added
+                          </div>
+                        </span>
+                      </span>
+                    </span>
+                  </div>
+                  <div
+                    class="bp3-collapse"
+                  >
+                    <div
+                      aria-hidden="false"
+                      class="bp3-collapse-body"
+                      style="transform: translateY(-0px);"
+                    />
+                  </div>
+                </li>
+                <li
+                  class="bp3-tree-node"
+                >
+                  <div
+                    class="bp3-tree-node-content bp3-tree-node-content-1"
+                  >
+                    <span
+                      class="bp3-tree-node-caret-none"
+                    />
+                    <span
+                      class="bp3-icon bp3-icon-numerical bp3-tree-node-icon"
+                      icon="numerical"
+                    >
+                      <svg
+                        data-icon="numerical"
+                        height="16"
+                        viewBox="0 0 16 16"
+                        width="16"
+                      >
+                        <desc>
+                          numerical
+                        </desc>
+                        <path
+                          d="M2.79 4.61c-.13.17-.29.3-.48.41-.18.11-.39.18-.62.23-.23.04-.46.07-.71.07v1.03h1.74V12h1.26V4h-.94c-.04.23-.12.44-.25.61zm4.37 5.31c.18-.14.37-.28.58-.42l.63-.45c.21-.16.41-.33.61-.51s.37-.38.52-.59c.15-.21.28-.45.37-.7.09-.25.13-.54.13-.85 0-.25-.04-.52-.12-.8-.07-.29-.2-.55-.39-.79a2.18 2.18 0 00-.73-.6c-.29-.15-.66-.23-1.11-.23-.41 0-.77.08-1.08.23-.31.16-.58.37-.79.64-.22.27-.38.59-.49.96-.11.37-.16.77-.16 1.2h1.19c.01-.27.03-.53.08-.77.04-.24.11-.45.21-. [...]
+                          fill-rule="evenodd"
+                        />
+                      </svg>
+                    </span>
+                    <span
+                      class="bp3-tree-node-label"
+                    >
+                      <span
+                        class="bp3-popover-wrapper"
+                      >
+                        <span
+                          class="bp3-popover-target bp3-popover-open"
+                        >
+                          <div
+                            class=""
+                          >
+                            addedBy10
+                          </div>
+                        </span>
+                      </span>
+                    </span>
+                  </div>
+                  <div
+                    class="bp3-collapse"
+                  >
+                    <div
+                      aria-hidden="false"
+                      class="bp3-collapse-body"
+                      style="transform: translateY(-0px);"
+                    />
+                  </div>
+                </li>
+              </ul>
+            </div>
           </div>
         </li>
       </ul>
diff --git a/web-console/console-config.js b/web-console/src/views/query-view/column-tree/column-tree-menu/index.ts
similarity index 83%
copy from web-console/console-config.js
copy to web-console/src/views/query-view/column-tree/column-tree-menu/index.ts
index 127d3a0..0af13c5 100644
--- a/web-console/console-config.js
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/index.ts
@@ -16,4 +16,6 @@
  * limitations under the License.
  */
 
-window.consoleConfig = { /* future configs may go here */ };
+export * from './number-menu-items/number-menu-items';
+export * from './string-menu-items/string-menu-items';
+export * from './time-menu-items/time-menu-items';
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx
index 0afa060..d41537d 100644
--- a/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx
@@ -28,14 +28,9 @@ describe('number menu', () => {
   it('matches snapshot', () => {
     const numberMenu = (
       <NumberMenuItems
-        hasFilter
-        clear={() => null}
-        addFunctionToGroupBy={() => null}
-        addToGroupBy={() => null}
-        addAggregateColumn={() => null}
-        filterByRow={() => null}
         columnName={'added'}
-        queryAst={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
+        parsedQuery={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
+        onQueryChange={() => {}}
       />
     );
 
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx
index 3ec2b9f..e08195e 100644
--- a/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx
@@ -18,139 +18,134 @@
 
 import { MenuItem } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
-import { Alias, FilterClause, SqlQuery, StringType } from 'druid-query-toolkit';
+import { SqlQuery, StringType } from 'druid-query-toolkit';
 import { aliasFactory } from 'druid-query-toolkit/build/ast/sql-query/helpers';
 import React from 'react';
 
-import { RowFilter } from '../../../query-view';
-
 export interface NumberMenuItemsProps {
-  addFunctionToGroupBy: (
-    functionName: string,
-    spacing: string[],
-    argumentsArray: (StringType | number)[],
-    run: boolean,
-    alias: Alias,
-  ) => void;
-  addToGroupBy: (columnName: string, run: boolean) => void;
-  addAggregateColumn: (
-    columnName: string,
-    functionName: string,
-    run: boolean,
-    alias?: Alias,
-    distinct?: boolean,
-    filter?: FilterClause,
-  ) => void;
-  filterByRow: (filters: RowFilter[], preferablyRun: boolean) => void;
-  queryAst?: SqlQuery;
   columnName: string;
-  clear: (column: string, preferablyRun: boolean) => void;
-  hasFilter: boolean;
+  parsedQuery: SqlQuery;
+  onQueryChange: (queryString: SqlQuery, run?: boolean) => void;
 }
 
 export class NumberMenuItems extends React.PureComponent<NumberMenuItemsProps> {
-  constructor(props: NumberMenuItemsProps, context: any) {
-    super(props, context);
-  }
-
   renderFilterMenu(): JSX.Element {
-    const { columnName, filterByRow } = this.props;
+    const { columnName, parsedQuery, onQueryChange } = this.props;
 
     return (
       <MenuItem icon={IconNames.FILTER} text={`Filter`}>
         <MenuItem
           text={`"${columnName}" > 100`}
-          onClick={() => filterByRow([{ row: 100, header: columnName, operator: '>' }], false)}
+          onClick={() => {
+            onQueryChange(parsedQuery.filterRow(columnName, 100, '>'));
+          }}
         />
         <MenuItem
           text={`"${columnName}" <= 100`}
-          onClick={() => filterByRow([{ row: 100, header: columnName, operator: '<=' }], false)}
+          onClick={() => {
+            onQueryChange(parsedQuery.filterRow(columnName, 100, '<='));
+          }}
         />
       </MenuItem>
     );
   }
 
-  renderRemoveFilter() {
-    const { columnName, clear } = this.props;
+  renderRemoveFilter(): JSX.Element | undefined {
+    const { columnName, parsedQuery, onQueryChange } = this.props;
+    if (!parsedQuery.hasFilterForColumn(columnName)) return;
+
     return (
       <MenuItem
         icon={IconNames.FILTER_REMOVE}
         text={`Remove filter`}
         onClick={() => {
-          clear(columnName, true);
+          onQueryChange(parsedQuery.removeFilter(columnName), true);
         }}
       />
     );
   }
 
-  renderGroupByMenu(): JSX.Element {
-    const { columnName, addFunctionToGroupBy, addToGroupBy } = this.props;
+  renderGroupByMenu(): JSX.Element | undefined {
+    const { columnName, parsedQuery, onQueryChange } = this.props;
+    if (!parsedQuery.groupByClause) return;
 
     return (
       <MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
-        <MenuItem text={`"${columnName}"`} onClick={() => addToGroupBy(columnName, true)} />
         <MenuItem
-          text={`TRUNCATE("${columnName}", 1) AS "${columnName}_truncated"`}
-          onClick={() =>
-            addFunctionToGroupBy(
-              'TRUNCATE',
-              [' '],
-              [
-                new StringType({
-                  spacing: [],
-                  chars: columnName,
-                  quote: '"',
-                }),
-                1,
-              ],
+          text={`"${columnName}"`}
+          onClick={() => {
+            onQueryChange(parsedQuery.addToGroupBy(columnName), true);
+          }}
+        />
+        <MenuItem
+          text={`TRUNC("${columnName}", -1) AS "${columnName}_trunc"`}
+          onClick={() => {
+            onQueryChange(
+              parsedQuery.addFunctionToGroupBy(
+                'TRUNC',
+                [' '],
+                [
+                  new StringType({
+                    spacing: [],
+                    chars: columnName,
+                    quote: '"',
+                  }),
+                  -1,
+                ],
+                aliasFactory(`${columnName}_truncated`),
+              ),
               true,
-              aliasFactory(`${columnName}_truncated`),
-            )
-          }
+            );
+          }}
         />
       </MenuItem>
     );
   }
 
-  renderAggregateMenu(): JSX.Element {
-    const { columnName, addAggregateColumn } = this.props;
+  renderAggregateMenu(): JSX.Element | undefined {
+    const { columnName, parsedQuery, onQueryChange } = this.props;
+    if (!parsedQuery.groupByClause) return;
+
     return (
       <MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
         <MenuItem
           text={`SUM(${columnName}) AS "sum_${columnName}"`}
-          onClick={() =>
-            addAggregateColumn(columnName, 'SUM', true, aliasFactory(`sum_${columnName}`))
-          }
+          onClick={() => {
+            onQueryChange(
+              parsedQuery.addAggregateColumn(columnName, 'SUM', aliasFactory(`sum_${columnName}`)),
+              true,
+            );
+          }}
         />
         <MenuItem
           text={`MAX(${columnName}) AS "max_${columnName}"`}
-          onClick={() =>
-            addAggregateColumn(columnName, 'MAX', true, aliasFactory(`max_${columnName}`))
-          }
+          onClick={() => {
+            onQueryChange(
+              parsedQuery.addAggregateColumn(columnName, 'MAX', aliasFactory(`max_${columnName}`)),
+              true,
+            );
+          }}
         />
         <MenuItem
           text={`MIN(${columnName}) AS "min_${columnName}"`}
-          onClick={() =>
-            addAggregateColumn(columnName, 'MIN', true, aliasFactory(`min_${columnName}`))
-          }
+          onClick={() => {
+            onQueryChange(
+              parsedQuery.addAggregateColumn(columnName, 'MIN', aliasFactory(`min_${columnName}`)),
+              true,
+            );
+          }}
         />
       </MenuItem>
     );
   }
 
   render(): JSX.Element {
-    const { queryAst, hasFilter } = this.props;
-    let hasGroupBy;
-    if (queryAst) {
-      hasGroupBy = queryAst.groupByClause;
-    }
-
     return (
       <>
-        {queryAst && this.renderFilterMenu()}
-        {hasFilter && this.renderRemoveFilter()}
-        {hasGroupBy && this.renderGroupByMenu()}
-        {hasGroupBy && this.renderAggregateMenu()}
+        {this.renderFilterMenu()}
+        {this.renderRemoveFilter()}
+        {this.renderGroupByMenu()}
+        {this.renderAggregateMenu()}
       </>
     );
   }
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx
index 1e4ffb5..950b74e 100644
--- a/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx
@@ -28,14 +28,9 @@ describe('string menu', () => {
   it('matches snapshot', () => {
     const stringMenu = (
       <StringMenuItems
-        hasFilter
-        clear={() => null}
-        addFunctionToGroupBy={() => null}
-        addToGroupBy={() => null}
-        addAggregateColumn={() => null}
-        filterByRow={() => null}
         columnName={'channel'}
-        queryAst={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
+        parsedQuery={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
+        onQueryChange={() => {}}
       />
     );
 
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx
index 9b508f9..deabcf2 100644
--- a/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx
@@ -19,160 +19,151 @@
 import { MenuItem } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
 import {
-  Alias,
   ComparisonExpression,
   ComparisonExpressionRhs,
   FilterClause,
-  RefExpression,
   refExpressionFactory,
   SqlQuery,
-  StringType,
   WhereClause,
 } from 'druid-query-toolkit';
 import { aliasFactory, stringFactory } from 'druid-query-toolkit/build/ast/sql-query/helpers';
 import React from 'react';
 
-import { RowFilter } from '../../../query-view';
-
 export interface StringMenuItemsProps {
-  addFunctionToGroupBy: (
-    functionName: string,
-    spacing: string[],
-    argumentsArray: (StringType | number)[],
-    run: boolean,
-    alias: Alias,
-  ) => void;
-  addToGroupBy: (columnName: string, run: boolean) => void;
-  addAggregateColumn: (
-    columnName: string | RefExpression,
-    functionName: string,
-    run: boolean,
-    alias?: Alias,
-    distinct?: boolean,
-    filter?: FilterClause,
-  ) => void;
-  filterByRow: (filters: RowFilter[], preferablyRun: boolean) => void;
-  queryAst?: SqlQuery;
   columnName: string;
-  clear: (column: string, preferablyRun: boolean) => void;
-  hasFilter: boolean;
+  parsedQuery: SqlQuery;
+  onQueryChange: (queryString: SqlQuery, run?: boolean) => void;
 }
 
 export class StringMenuItems extends React.PureComponent<StringMenuItemsProps> {
-  constructor(props: StringMenuItemsProps, context: any) {
-    super(props, context);
-  }
-
-  renderFilterMenu(): JSX.Element {
-    const { columnName, filterByRow } = this.props;
+  renderFilterMenu(): JSX.Element | undefined {
+    const { columnName, parsedQuery, onQueryChange } = this.props;
 
     return (
       <MenuItem icon={IconNames.FILTER} text={`Filter`}>
         <MenuItem
           text={`"${columnName}" = 'xxx'`}
-          onClick={() => filterByRow([{ row: 'xxx', header: columnName, operator: '=' }], false)}
+          onClick={() => {
+            onQueryChange(parsedQuery.filterRow(columnName, 'xxx', '='), false);
+          }}
         />
         <MenuItem
           text={`"${columnName}" LIKE '%xxx%'`}
-          onClick={() =>
-            filterByRow([{ row: '%xxx%', header: columnName, operator: 'LIKE' }], false)
-          }
+          onClick={() => {
+            onQueryChange(parsedQuery.filterRow(columnName, '%xxx%', 'LIKE'), false);
+          }}
         />
       </MenuItem>
     );
   }
 
-  renderRemoveFilter() {
-    const { columnName, clear } = this.props;
+  renderRemoveFilter(): JSX.Element | undefined {
+    const { columnName, parsedQuery, onQueryChange } = this.props;
+    if (!parsedQuery.hasFilterForColumn(columnName)) return;
+
     return (
       <MenuItem
         icon={IconNames.FILTER_REMOVE}
         text={`Remove filter`}
         onClick={() => {
-          clear(columnName, true);
+          onQueryChange(parsedQuery.removeFilter(columnName), true);
         }}
       />
     );
   }
 
-  renderGroupByMenu(): JSX.Element {
-    const { columnName, addFunctionToGroupBy, addToGroupBy } = this.props;
+  renderGroupByMenu(): JSX.Element | undefined {
+    const { columnName, parsedQuery, onQueryChange } = this.props;
+    if (!parsedQuery.hasGroupBy()) return;
 
     return (
       <MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
-        <MenuItem text={`"${columnName}"`} onClick={() => addToGroupBy(columnName, true)} />
+        <MenuItem
+          text={`"${columnName}"`}
+          onClick={() => {
+            onQueryChange(parsedQuery.addToGroupBy(columnName), true);
+          }}
+        />
         <MenuItem
           text={`SUBSTRING("${columnName}", 1, 2) AS "${columnName}_substring"`}
-          onClick={() =>
-            addFunctionToGroupBy(
-              'SUBSTRING',
-              [' ', ' '],
-              [stringFactory(columnName, `"`), 1, 2],
+          onClick={() => {
+            onQueryChange(
+              parsedQuery.addFunctionToGroupBy(
+                'SUBSTRING',
+                [' ', ' '],
+                [stringFactory(columnName, `"`), 1, 2],
+
+                aliasFactory(`${columnName}_substring`),
+              ),
               true,
-              aliasFactory(`${columnName}_substring`),
-            )
-          }
+            );
+          }}
         />
       </MenuItem>
     );
   }
 
-  renderAggregateMenu(): JSX.Element {
-    const { columnName, addAggregateColumn } = this.props;
+  renderAggregateMenu(): JSX.Element | undefined {
+    const { columnName, parsedQuery, onQueryChange } = this.props;
+    if (!parsedQuery.hasGroupBy()) return;
+
     return (
       <MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
         <MenuItem
           text={`COUNT(DISTINCT "${columnName}") AS "dist_${columnName}"`}
           onClick={() =>
-            addAggregateColumn(columnName, 'COUNT', true, aliasFactory(`dist_${columnName}`), true)
+            onQueryChange(
+              parsedQuery.addAggregateColumn(
+                columnName,
+                'COUNT',
+                aliasFactory(`dist_${columnName}`),
+              ),
+              true,
+            )
           }
         />
         <MenuItem
           text={`COUNT(*) FILTER (WHERE "${columnName}" = 'xxx') AS ${columnName}_filtered_count `}
-          onClick={() =>
-            addAggregateColumn(
-              refExpressionFactory('*'),
-              'COUNT',
-              false,
-              aliasFactory(`${columnName}_filtered_count`),
-              false,
-              new FilterClause({
-                keyword: 'FILTER',
-                spacing: [' '],
-                ex: new WhereClause({
-                  keyword: 'WHERE',
+          onClick={() => {
+            onQueryChange(
+              parsedQuery.addAggregateColumn(
+                refExpressionFactory('*'),
+                'COUNT',
+                aliasFactory(`${columnName}_filtered_count`),
+                false,
+                new FilterClause({
+                  keyword: 'FILTER',
                   spacing: [' '],
-                  filter: new ComparisonExpression({
-                    parens: [],
-                    ex: stringFactory(columnName, '"'),
-                    rhs: new ComparisonExpressionRhs({
+                  ex: new WhereClause({
+                    keyword: 'WHERE',
+                    spacing: [' '],
+                    filter: new ComparisonExpression({
                       parens: [],
-                      op: '=',
-                      rhs: stringFactory('xxx', `'`),
-                      spacing: [' ', ' '],
+                      ex: stringFactory(columnName, '"'),
+                      rhs: new ComparisonExpressionRhs({
+                        parens: [],
+                        op: '=',
+                        rhs: stringFactory('xxx', `'`),
+                        spacing: [' ', ' '],
+                      }),
                     }),
                   }),
                 }),
-              }),
-            )
-          }
+              ),
+            );
+          }}
         />
       </MenuItem>
     );
   }
 
   render(): JSX.Element {
-    const { queryAst, hasFilter } = this.props;
-    let hasGroupBy;
-    if (queryAst) {
-      hasGroupBy = queryAst.groupByClause;
-    }
     return (
       <>
-        {queryAst && this.renderFilterMenu()}
-        {hasFilter && this.renderRemoveFilter()}
-        {hasGroupBy && this.renderGroupByMenu()}
-        {hasGroupBy && this.renderAggregateMenu()}
+        {this.renderFilterMenu()}
+        {this.renderRemoveFilter()}
+        {this.renderGroupByMenu()}
+        {this.renderAggregateMenu()}
       </>
     );
   }
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx
index 7e6725d..3e46135 100644
--- a/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx
@@ -28,14 +28,9 @@ describe('time menu', () => {
   it('matches snapshot', () => {
     const timeMenu = (
       <TimeMenuItems
-        hasFilter
-        clear={() => null}
-        addFunctionToGroupBy={() => null}
-        addToGroupBy={() => null}
-        addAggregateColumn={() => null}
-        filterByRow={() => null}
         columnName={'__time'}
-        queryAst={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
+        parsedQuery={parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`)}
+        onQueryChange={() => {}}
       />
     );
 
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx
index b948d47..231486d 100644
--- a/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx
@@ -18,14 +18,7 @@
 
 import { MenuDivider, MenuItem } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
-import {
-  AdditiveExpression,
-  Alias,
-  FilterClause,
-  SqlQuery,
-  StringType,
-  timestampFactory,
-} from 'druid-query-toolkit';
+import { AdditiveExpression, SqlQuery, Timestamp, timestampFactory } from 'druid-query-toolkit';
 import {
   aliasFactory,
   intervalFactory,
@@ -34,107 +27,76 @@ import {
 } from 'druid-query-toolkit/build/ast/sql-query/helpers';
 import React from 'react';
 
-import { RowFilter } from '../../../query-view';
-
 export interface TimeMenuItemsProps {
-  addFunctionToGroupBy: (
-    functionName: string,
-    spacing: string[],
-    argumentsArray: (StringType | number)[],
-    run: boolean,
-    alias: Alias,
-  ) => void;
-  addToGroupBy: (columnName: string, run: boolean) => void;
-  addAggregateColumn: (
-    columnName: string,
-    functionName: string,
-    run: boolean,
-    alias?: Alias,
-    distinct?: boolean,
-    filter?: FilterClause,
-  ) => void;
-  filterByRow: (filters: RowFilter[], preferablyRun: boolean) => void;
-  queryAst?: SqlQuery;
   columnName: string;
-  clear: (column: string, preferablyRun: boolean) => void;
-  hasFilter: boolean;
+  parsedQuery: SqlQuery;
+  onQueryChange: (queryString: SqlQuery, run?: boolean) => void;
 }
 
 export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
-  constructor(props: TimeMenuItemsProps, context: any) {
-    super(props, context);
+  static dateToTimestamp(date: Date): Timestamp {
+    return timestampFactory(
+      date
+        .toISOString()
+        .split('.')[0]
+        .split('T')
+        .join(' '),
+    );
+  }
+
+  static floorHour(dt: Date): Date {
+    dt = new Date(dt.valueOf());
+    dt.setUTCMinutes(0, 0, 0);
+    return dt;
+  }
+
+  static nextHour(dt: Date): Date {
+    dt = new Date(dt.valueOf());
+    dt.setUTCHours(dt.getUTCHours() + 1);
+    return dt;
+  }
+
+  static floorDay(dt: Date): Date {
+    dt = new Date(dt.valueOf());
+    dt.setUTCHours(0, 0, 0, 0);
+    return dt;
+  }
+
+  static nextDay(dt: Date): Date {
+    dt = new Date(dt.valueOf());
+    dt.setUTCDate(dt.getUTCDate() + 1);
+    return dt;
   }
 
-  formatTime(timePart: number): string {
-    if (timePart % 10 > 0) {
-      return String(timePart);
-    } else return '0' + String(timePart);
+  static floorMonth(dt: Date): Date {
+    dt = new Date(dt.valueOf());
+    dt.setUTCHours(0, 0, 0, 0);
+    dt.setUTCDate(1);
+    return dt;
   }
 
-  getNextMonth(month: number, year: number): { month: string; year: number } {
-    if (month === 12) {
-      return { month: '01', year: year + 1 };
-    }
-    return { month: this.formatTime(month + 1), year: year };
+  static nextMonth(dt: Date): Date {
+    dt = new Date(dt.valueOf());
+    dt.setUTCMonth(dt.getUTCMonth() + 1);
+    return dt;
   }
 
-  getNextDay(
-    day: number,
-    month: number,
-    year: number,
-  ): { day: string; month: string; year: number } {
-    if (
-      month === 1 ||
-      month === 3 ||
-      month === 5 ||
-      month === 7 ||
-      month === 8 ||
-      month === 10 ||
-      month === 12
-    ) {
-      if (day === 31) {
-        const next = this.getNextMonth(month, year);
-        return { day: '01', month: next.month, year: next.year };
-      }
-    } else if (month === 4 || month === 6 || month === 9 || month === 11) {
-      if (day === 30) {
-        const next = this.getNextMonth(month, year);
-        return { day: '01', month: next.month, year: next.year };
-      }
-    } else if (month === 2) {
-      if ((day === 29 && year % 4 === 0) || (day === 28 && year % 4)) {
-        const next = this.getNextMonth(month, year);
-        return { day: '01', month: next.month, year: next.year };
-      }
-    }
-    return { day: this.formatTime(day + 1), month: this.formatTime(month), year: year };
+  static floorYear(dt: Date): Date {
+    dt = new Date(dt.valueOf());
+    dt.setUTCHours(0, 0, 0, 0);
+    dt.setUTCMonth(0, 1);
+    return dt;
   }
 
-  getNextHour(
-    hour: number,
-    day: number,
-    month: number,
-    year: number,
-  ): { hour: string; day: string; month: string; year: number } {
-    if (hour === 23) {
-      const next = this.getNextDay(day, month, year);
-      return { hour: '00', day: next.day, month: next.month, year: next.year };
-    }
-    return {
-      hour: this.formatTime(hour + 1),
-      day: this.formatTime(day),
-      month: this.formatTime(month),
-      year: year,
-    };
+  static nextYear(dt: Date): Date {
+    dt = new Date(dt.valueOf());
+    dt.setUTCFullYear(dt.getUTCFullYear() + 1);
+    return dt;
   }
 
-  renderFilterMenu(): JSX.Element {
-    const { columnName, filterByRow, clear } = this.props;
-    const date = new Date();
-    const year = date.getFullYear();
-    const month = date.getMonth();
-    const day = date.getDay();
-    const hour = date.getHours();
+  renderFilterMenu(): JSX.Element | undefined {
+    const { columnName, parsedQuery, onQueryChange } = this.props;
+    const now = new Date();
 
     return (
       <MenuItem icon={IconNames.FILTER} text={`Filter`}>
@@ -147,8 +109,10 @@ export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
               ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('HOUR', '1')],
               spacing: [' ', ' '],
             });
-            clear(columnName, false);
-            filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
+            onQueryChange(
+              parsedQuery.removeFilter(columnName).filterRow(columnName, additiveExpression, '>='),
+              true,
+            );
           }}
         />
         <MenuItem
@@ -160,8 +124,10 @@ export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
               ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('DAY', '1')],
               spacing: [' ', ' '],
             });
-            clear(columnName, false);
-            filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
+            onQueryChange(
+              parsedQuery.removeFilter(columnName).filterRow(columnName, additiveExpression, '>='),
+              true,
+            );
           }}
         />
         <MenuItem
@@ -173,8 +139,10 @@ export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
               ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('DAY', '7')],
               spacing: [' ', ' '],
             });
-            clear(columnName, false);
-            filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
+            onQueryChange(
+              parsedQuery.removeFilter(columnName).filterRow(columnName, additiveExpression, '>='),
+              true,
+            );
           }}
         />
         <MenuItem
@@ -186,8 +154,10 @@ export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
               ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('MONTH', '1')],
               spacing: [' ', ' '],
             });
-            clear(columnName, false);
-            filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
+            onQueryChange(
+              parsedQuery.removeFilter(columnName).filterRow(columnName, additiveExpression, '>='),
+              true,
+            );
           }}
         />
         <MenuItem
@@ -199,33 +169,30 @@ export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
               ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('YEAR', '1')],
               spacing: [' ', ' '],
             });
-            clear(columnName, false);
-            filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
+            onQueryChange(
+              parsedQuery.removeFilter(columnName).filterRow(columnName, additiveExpression, '>='),
+              true,
+            );
           }}
         />
         <MenuDivider />
         <MenuItem
           text={`Current hour`}
           onClick={() => {
-            const next = this.getNextHour(hour, day, month, year);
-            clear(columnName, false);
-            filterByRow(
-              [
-                {
-                  row: stringFactory(columnName, `"`),
-                  header: timestampFactory(
-                    `${year}-${month}-${day} ${this.formatTime(hour)}:00:00`,
-                  ),
-                  operator: '<=',
-                },
-                {
-                  row: timestampFactory(
-                    `${next.year}-${next.month}-${next.day} ${next.hour}:00:00`,
-                  ),
-                  header: columnName,
-                  operator: '<',
-                },
-              ],
+            const hourStart = TimeMenuItems.floorHour(now);
+            onQueryChange(
+              parsedQuery
+                .removeFilter(columnName)
+                .filterRow(
+                  TimeMenuItems.dateToTimestamp(hourStart),
+                  stringFactory(columnName, `"`),
+                  '<=',
+                )
+                .filterRow(
+                  columnName,
+                  TimeMenuItems.dateToTimestamp(TimeMenuItems.nextHour(hourStart)),
+                  '<',
+                ),
               true,
             );
           }}
@@ -233,21 +200,20 @@ export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
         <MenuItem
           text={`Current day`}
           onClick={() => {
-            const next = this.getNextDay(day, month, year);
-            clear(columnName, false);
-            filterByRow(
-              [
-                {
-                  row: stringFactory(columnName, `"`),
-                  header: timestampFactory(`${year}-${month}-${day} 00:00:00`),
-                  operator: '<=',
-                },
-                {
-                  row: timestampFactory(`${next.year}-${next.month}-${next.day} 00:00:00`),
-                  header: columnName,
-                  operator: '<',
-                },
-              ],
+            const dayStart = TimeMenuItems.floorDay(now);
+            onQueryChange(
+              parsedQuery
+                .removeFilter(columnName)
+                .filterRow(
+                  TimeMenuItems.dateToTimestamp(dayStart),
+                  stringFactory(columnName, `"`),
+                  '<=',
+                )
+                .filterRow(
+                  columnName,
+                  TimeMenuItems.dateToTimestamp(TimeMenuItems.nextDay(dayStart)),
+                  '<',
+                ),
               true,
             );
           }}
@@ -255,21 +221,20 @@ export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
         <MenuItem
           text={`Current month`}
           onClick={() => {
-            const next = this.getNextMonth(month, year);
-            clear(columnName, false);
-            filterByRow(
-              [
-                {
-                  row: stringFactory(columnName, `"`),
-                  header: timestampFactory(`${year}-${month}-01 00:00:00`),
-                  operator: '<=',
-                },
-                {
-                  row: timestampFactory(`${next.year}-${next.month}-01 00:00:00`),
-                  header: columnName,
-                  operator: '<',
-                },
-              ],
+            const monthStart = TimeMenuItems.floorMonth(now);
+            onQueryChange(
+              parsedQuery
+                .removeFilter(columnName)
+                .filterRow(
+                  TimeMenuItems.dateToTimestamp(monthStart),
+                  stringFactory(columnName, `"`),
+                  '<=',
+                )
+                .filterRow(
+                  columnName,
+                  TimeMenuItems.dateToTimestamp(TimeMenuItems.nextMonth(monthStart)),
+                  '<',
+                ),
               true,
             );
           }}
@@ -277,20 +242,20 @@ export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
         <MenuItem
           text={`Current year`}
           onClick={() => {
-            clear(columnName, false);
-            filterByRow(
-              [
-                {
-                  row: stringFactory(columnName, `"`),
-                  header: timestampFactory(`${year}-01-01 00:00:00`),
-                  operator: '<=',
-                },
-                {
-                  row: timestampFactory(`${Number(year) + 1}-01-01 00:00:00`),
-                  header: columnName,
-                  operator: '<',
-                },
-              ],
+            const yearStart = TimeMenuItems.floorYear(now);
+            onQueryChange(
+              parsedQuery
+                .removeFilter(columnName)
+                .filterRow(
+                  TimeMenuItems.dateToTimestamp(yearStart),
+                  stringFactory(columnName, `"`),
+                  '<=',
+                )
+                .filterRow(
+                  columnName,
+                  TimeMenuItems.dateToTimestamp(TimeMenuItems.nextYear(yearStart)),
+                  '<',
+                ),
               true,
             );
           }}
@@ -299,96 +264,108 @@ export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
     );
   }
 
-  renderRemoveFilter() {
-    const { columnName, clear } = this.props;
+  renderRemoveFilter(): JSX.Element | undefined {
+    const { columnName, parsedQuery, onQueryChange } = this.props;
+    if (!parsedQuery.hasFilterForColumn(columnName)) return;
+
     return (
       <MenuItem
         icon={IconNames.FILTER_REMOVE}
         text={`Remove filter`}
         onClick={() => {
-          clear(columnName, true);
+          onQueryChange(parsedQuery.removeFilter(columnName), true);
         }}
       />
     );
   }
 
-  renderGroupByMenu(): JSX.Element {
-    const { columnName, addFunctionToGroupBy } = this.props;
+  renderGroupByMenu(): JSX.Element | undefined {
+    const { columnName, parsedQuery, onQueryChange } = this.props;
+    if (!parsedQuery.hasGroupBy()) return;
 
     return (
       <MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
         <MenuItem
           text={`TIME_FLOOR("${columnName}", 'PT1H') AS "${columnName}_time_floor"`}
-          onClick={() =>
-            addFunctionToGroupBy(
-              'TIME_FLOOR',
-              [' '],
-              [stringFactory(columnName, `"`), stringFactory('PT1H', `'`)],
+          onClick={() => {
+            onQueryChange(
+              parsedQuery.addFunctionToGroupBy(
+                'TIME_FLOOR',
+                [' '],
+                [stringFactory(columnName, `"`), stringFactory('PT1H', `'`)],
+                aliasFactory(`${columnName}_time_floor`),
+              ),
               true,
-              aliasFactory(`${columnName}_time_floor`),
-            )
-          }
+            );
+          }}
         />
         <MenuItem
           text={`TIME_FLOOR("${columnName}", 'P1D') AS "${columnName}_time_floor"`}
-          onClick={() =>
-            addFunctionToGroupBy(
-              'TIME_FLOOR',
-              [' '],
-              [stringFactory(columnName, `"`), stringFactory('P1D', `'`)],
+          onClick={() => {
+            onQueryChange(
+              parsedQuery.addFunctionToGroupBy(
+                'TIME_FLOOR',
+                [' '],
+                [stringFactory(columnName, `"`), stringFactory('P1D', `'`)],
+                aliasFactory(`${columnName}_time_floor`),
+              ),
               true,
-              aliasFactory(`${columnName}_time_floor`),
-            )
-          }
+            );
+          }}
         />
         <MenuItem
           text={`TIME_FLOOR("${columnName}", 'P7D') AS "${columnName}_time_floor"`}
-          onClick={() =>
-            addFunctionToGroupBy(
-              'TIME_FLOOR',
-              [' '],
-              [stringFactory(columnName, `"`), stringFactory('P7D', `'`)],
+          onClick={() => {
+            onQueryChange(
+              parsedQuery.addFunctionToGroupBy(
+                'TIME_FLOOR',
+                [' '],
+                [stringFactory(columnName, `"`), stringFactory('P7D', `'`)],
+                aliasFactory(`${columnName}_time_floor`),
+              ),
               true,
-              aliasFactory(`${columnName}_time_floor`),
-            )
-          }
+            );
+          }}
         />
       </MenuItem>
     );
   }
 
-  renderAggregateMenu(): JSX.Element {
-    const { columnName, addAggregateColumn } = this.props;
+  renderAggregateMenu(): JSX.Element | undefined {
+    const { columnName, parsedQuery, onQueryChange } = this.props;
+    if (!parsedQuery.hasGroupBy()) return;
+
     return (
       <MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
         <MenuItem
           text={`MAX("${columnName}") AS "max_${columnName}"`}
-          onClick={() =>
-            addAggregateColumn(columnName, 'MAX', true, aliasFactory(`max_${columnName}`))
-          }
+          onClick={() => {
+            onQueryChange(
+              parsedQuery.addAggregateColumn(columnName, 'MAX', aliasFactory(`max_${columnName}`)),
+              true,
+            );
+          }}
         />
         <MenuItem
           text={`MIN("${columnName}") AS "min_${columnName}"`}
-          onClick={() =>
-            addAggregateColumn(columnName, 'MIN', true, aliasFactory(`min_${columnName}`))
-          }
+          onClick={() => {
+            onQueryChange(
+              parsedQuery.addAggregateColumn(columnName, 'MIN', aliasFactory(`min_${columnName}`)),
+              true,
+            );
+          }}
         />
       </MenuItem>
     );
   }
 
   render(): JSX.Element {
-    const { queryAst, hasFilter } = this.props;
-    let hasGroupBy;
-    if (queryAst) {
-      hasGroupBy = queryAst.groupByClause;
-    }
     return (
       <>
-        {queryAst && this.renderFilterMenu()}
-        {hasFilter && this.renderRemoveFilter()}
-        {hasGroupBy && this.renderGroupByMenu()}
-        {hasGroupBy && this.renderAggregateMenu()}
+        {this.renderFilterMenu()}
+        {this.renderRemoveFilter()}
+        {this.renderGroupByMenu()}
+        {this.renderAggregateMenu()}
       </>
     );
   }
diff --git a/web-console/src/views/query-view/column-tree/column-tree.spec.tsx b/web-console/src/views/query-view/column-tree/column-tree.spec.tsx
index 7856d5e..e925eab 100644
--- a/web-console/src/views/query-view/column-tree/column-tree.spec.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree.spec.tsx
@@ -17,6 +17,7 @@
  */
 
 import { render } from '@testing-library/react';
+import { sqlParserFactory } from 'druid-query-toolkit';
 import React from 'react';
 
 import { ColumnMetadata } from '../../../utils/column-metadata';
@@ -24,31 +25,38 @@ import { ColumnMetadata } from '../../../utils/column-metadata';
 import { ColumnTree } from './column-tree';
 
 describe('column tree', () => {
+  const parser = sqlParserFactory(['COUNT']);
+
   it('matches snapshot', () => {
     const columnTree = (
       <ColumnTree
-        queryAst={() => undefined}
-        clear={() => null}
-        addFunctionToGroupBy={() => null}
-        filterByRow={() => null}
-        addAggregateColumn={() => null}
-        addToGroupBy={() => null}
+        getParsedQuery={() => {
+          return parser(`SELECT channel, count(*) as cnt FROM wikipedia GROUP BY 1`);
+        }}
+        defaultSchema="druid"
+        defaultTable="wikipedia"
         columnMetadataLoading={false}
         columnMetadata={
           [
             {
               TABLE_SCHEMA: 'druid',
-              TABLE_NAME: 'deletion-tutorial',
+              TABLE_NAME: 'wikipedia',
               COLUMN_NAME: '__time',
               DATA_TYPE: 'TIMESTAMP',
             },
             {
               TABLE_SCHEMA: 'druid',
-              TABLE_NAME: 'deletion-tutorial',
+              TABLE_NAME: 'wikipedia',
               COLUMN_NAME: 'added',
               DATA_TYPE: 'BIGINT',
             },
             {
+              TABLE_SCHEMA: 'druid',
+              TABLE_NAME: 'wikipedia',
+              COLUMN_NAME: 'addedBy10',
+              DATA_TYPE: 'FLOAT',
+            },
+            {
               TABLE_SCHEMA: 'sys',
               TABLE_NAME: 'tasks',
               COLUMN_NAME: 'error_msg',
@@ -57,7 +65,6 @@ describe('column tree', () => {
           ] as ColumnMetadata[]
         }
         onQueryStringChange={() => {}}
-        replaceFrom={() => null}
       />
     );
 
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
index 72c3a95..91cfef8 100644
--- a/web-console/src/views/query-view/column-tree/column-tree.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree.tsx
@@ -27,26 +27,15 @@ import {
   Tree,
 } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
-import {
-  Alias,
-  FilterClause,
-  RefExpression,
-  refExpressionFactory,
-  SqlQuery,
-  stringFactory,
-  StringType,
-} from 'druid-query-toolkit';
+import { refExpressionFactory, SqlQuery, stringFactory } from 'druid-query-toolkit';
 import React, { ChangeEvent } from 'react';
 
 import { Loader } from '../../../components';
 import { Deferred } from '../../../components/deferred/deferred';
 import { copyAndAlert, escapeSqlIdentifier, groupBy } from '../../../utils';
 import { ColumnMetadata } from '../../../utils/column-metadata';
-import { RowFilter } from '../query-view';
 
-import { NumberMenuItems } from './column-tree-menu/number-menu-items/number-menu-items';
-import { StringMenuItems } from './column-tree-menu/string-menu-items/string-menu-items';
-import { TimeMenuItems } from './column-tree-menu/time-menu-items/time-menu-items';
+import { NumberMenuItems, StringMenuItems, TimeMenuItems } from './column-tree-menu';
 
 import './column-tree.scss';
 
@@ -123,29 +112,10 @@ ORDER BY "Count" DESC`,
 export interface ColumnTreeProps {
   columnMetadataLoading: boolean;
   columnMetadata?: readonly ColumnMetadata[];
-  onQueryStringChange: (queryString: string, run: boolean) => void;
+  getParsedQuery: () => SqlQuery | undefined;
+  onQueryStringChange: (queryString: string | SqlQuery, run?: boolean) => void;
   defaultSchema?: string;
   defaultTable?: string;
-  addFunctionToGroupBy: (
-    functionName: string,
-    spacing: string[],
-    argumentsArray: (StringType | number)[],
-    run: boolean,
-    alias: Alias,
-  ) => void;
-  addToGroupBy: (columnName: string, run: boolean) => void;
-  addAggregateColumn: (
-    columnName: string | RefExpression,
-    functionName: string,
-    run: boolean,
-    alias?: Alias,
-    distinct?: boolean,
-    filter?: FilterClause,
-  ) => void;
-  filterByRow: (filters: RowFilter[], preferablyRun: boolean) => void;
-  replaceFrom: (table: RefExpression, preferablyRun: boolean) => void;
-  queryAst: () => SqlQuery | undefined;
-  clear: (column: string, preferablyRun: boolean) => void;
 }
 
 export interface ColumnTreeState {
@@ -169,7 +139,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
           childNodes: groupBy(
             metadata,
             r => r.TABLE_NAME,
-            (metadata, table) => ({
+            (metadata, table): ITreeNode => ({
               id: table,
               icon: IconNames.TH,
               label: (
@@ -177,149 +147,143 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
                   boundary={'window'}
                   position={Position.RIGHT}
                   content={
-                    <Menu>
-                      <MenuItem
-                        icon={IconNames.FULLSCREEN}
-                        text={`SELECT ... FROM ${table}`}
-                        onClick={() => {
-                          handleTableClick(
-                            schema,
-                            {
-                              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,
-                              })),
-                            },
-                            props.onQueryStringChange,
-                          );
-                        }}
-                      />
-                      <MenuItem
-                        icon={IconNames.CLIPBOARD}
-                        text={`Copy: ${table}`}
-                        onClick={() => {
-                          copyAndAlert(table, `${table} query copied to clipboard`);
-                        }}
-                      />
-                      <Deferred
-                        content={() => (
-                          <>
-                            {props.queryAst() && (
+                    <Deferred
+                      content={() => {
+                        const parsedQuery = props.getParsedQuery();
+                        return (
+                          <Menu>
+                            <MenuItem
+                              icon={IconNames.FULLSCREEN}
+                              text={`SELECT ... FROM ${table}`}
+                              onClick={() => {
+                                handleTableClick(
+                                  schema,
+                                  {
+                                    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,
+                                    })),
+                                  },
+                                  props.onQueryStringChange,
+                                );
+                              }}
+                            />
+                            <MenuItem
+                              icon={IconNames.CLIPBOARD}
+                              text={`Copy: ${table}`}
+                              onClick={() => {
+                                copyAndAlert(table, `${table} query copied to clipboard`);
+                              }}
+                            />
+                            {parsedQuery && (
                               <MenuItem
                                 icon={IconNames.EXCHANGE}
                                 text={`Replace FROM with: ${table}`}
                                 onClick={() => {
-                                  props.replaceFrom(
-                                    refExpressionFactory(stringFactory(table, `"`)),
+                                  props.onQueryStringChange(
+                                    parsedQuery.replaceFrom(
+                                      refExpressionFactory(stringFactory(table, `"`)),
+                                    ),
                                     true,
                                   );
                                 }}
                               />
                             )}
-                          </>
-                        )}
-                      />
-                    </Menu>
+                          </Menu>
+                        );
+                      }}
+                    />
                   }
                 >
                   <div>{table}</div>
                 </Popover>
               ),
-              childNodes: metadata.map(columnData => ({
-                id: columnData.COLUMN_NAME,
-                icon: ColumnTree.dataTypeToIcon(columnData.DATA_TYPE),
-                label: (
-                  <Popover
-                    boundary={'window'}
-                    position={Position.RIGHT}
-                    autoFocus={false}
-                    targetClassName={'bp3-popover-open'}
-                    content={
-                      <Deferred
-                        content={() => {
-                          const queryAst = props.queryAst();
-                          const hasFilter = queryAst
-                            ? queryAst.getCurrentFilters().includes(columnData.COLUMN_NAME)
-                            : false;
-
-                          return (
-                            <Menu>
-                              <MenuItem
-                                icon={IconNames.FULLSCREEN}
-                                text={`Show: ${columnData.COLUMN_NAME}`}
-                                onClick={() => {
-                                  handleColumnClick(
-                                    schema,
-                                    table,
-                                    {
-                                      id: columnData.COLUMN_NAME,
-                                      icon: ColumnTree.dataTypeToIcon(columnData.DATA_TYPE),
-                                      label: columnData.COLUMN_NAME,
-                                    },
-                                    props.onQueryStringChange,
-                                  );
-                                }}
-                              />
-                              {columnData.DATA_TYPE === 'BIGINT' && (
-                                <NumberMenuItems
-                                  addFunctionToGroupBy={props.addFunctionToGroupBy}
-                                  addToGroupBy={props.addToGroupBy}
-                                  addAggregateColumn={props.addAggregateColumn}
-                                  filterByRow={props.filterByRow}
-                                  columnName={columnData.COLUMN_NAME}
-                                  queryAst={props.queryAst()}
-                                  clear={props.clear}
-                                  hasFilter={hasFilter}
-                                />
-                              )}
-                              {columnData.DATA_TYPE === 'VARCHAR' && (
-                                <StringMenuItems
-                                  addFunctionToGroupBy={props.addFunctionToGroupBy}
-                                  addToGroupBy={props.addToGroupBy}
-                                  addAggregateColumn={props.addAggregateColumn}
-                                  filterByRow={props.filterByRow}
-                                  columnName={columnData.COLUMN_NAME}
-                                  queryAst={props.queryAst()}
-                                  clear={props.clear}
-                                  hasFilter={hasFilter}
-                                />
-                              )}
-                              {columnData.DATA_TYPE === 'TIMESTAMP' && (
-                                <TimeMenuItems
-                                  clear={props.clear}
-                                  addFunctionToGroupBy={props.addFunctionToGroupBy}
-                                  addToGroupBy={props.addToGroupBy}
-                                  addAggregateColumn={props.addAggregateColumn}
-                                  filterByRow={props.filterByRow}
-                                  columnName={columnData.COLUMN_NAME}
-                                  queryAst={props.queryAst()}
-                                  hasFilter={hasFilter}
-                                />
-                              )}
-                              <MenuItem
-                                icon={IconNames.CLIPBOARD}
-                                text={`Copy: ${columnData.COLUMN_NAME}`}
-                                onClick={() => {
-                                  copyAndAlert(
-                                    columnData.COLUMN_NAME,
-                                    `${columnData.COLUMN_NAME} query copied to clipboard`,
-                                  );
-                                }}
-                              />
-                            </Menu>
-                          );
-                        }}
-                      />
-                    }
-                  >
-                    <div>{columnData.COLUMN_NAME}</div>
-                  </Popover>
+              childNodes: metadata
+                .map(
+                  (columnData): ITreeNode => ({
+                    id: columnData.COLUMN_NAME,
+                    icon: ColumnTree.dataTypeToIcon(columnData.DATA_TYPE),
+                    label: (
+                      <Popover
+                        boundary={'window'}
+                        position={Position.RIGHT}
+                        autoFocus={false}
+                        targetClassName={'bp3-popover-open'}
+                        content={
+                          <Deferred
+                            content={() => {
+                              const parsedQuery = props.getParsedQuery();
+                              return (
+                                <Menu>
+                                  <MenuItem
+                                    icon={IconNames.FULLSCREEN}
+                                    text={`Show: ${columnData.COLUMN_NAME}`}
+                                    onClick={() => {
+                                      handleColumnClick(
+                                        schema,
+                                        table,
+                                        {
+                                          id: columnData.COLUMN_NAME,
+                                          icon: ColumnTree.dataTypeToIcon(columnData.DATA_TYPE),
+                                          label: columnData.COLUMN_NAME,
+                                        },
+                                        props.onQueryStringChange,
+                                      );
+                                    }}
+                                  />
+                                  {parsedQuery &&
+                                    (columnData.DATA_TYPE === 'BIGINT' ||
+                                      columnData.DATA_TYPE === 'FLOAT') && (
+                                      <NumberMenuItems
+                                        columnName={columnData.COLUMN_NAME}
+                                        parsedQuery={parsedQuery}
+                                        onQueryChange={props.onQueryStringChange}
+                                      />
+                                    )}
+                                  {parsedQuery && columnData.DATA_TYPE === 'VARCHAR' && (
+                                    <StringMenuItems
+                                      columnName={columnData.COLUMN_NAME}
+                                      parsedQuery={parsedQuery}
+                                      onQueryChange={props.onQueryStringChange}
+                                    />
+                                  )}
+                                  {parsedQuery && columnData.DATA_TYPE === 'TIMESTAMP' && (
+                                    <TimeMenuItems
+                                      columnName={columnData.COLUMN_NAME}
+                                      parsedQuery={parsedQuery}
+                                      onQueryChange={props.onQueryStringChange}
+                                    />
+                                  )}
+                                  <MenuItem
+                                    icon={IconNames.CLIPBOARD}
+                                    text={`Copy: ${columnData.COLUMN_NAME}`}
+                                    onClick={() => {
+                                      copyAndAlert(
+                                        columnData.COLUMN_NAME,
+                                        `${columnData.COLUMN_NAME} query copied to clipboard`,
+                                      );
+                                    }}
+                                  />
+                                </Menu>
+                              );
+                            }}
+                          />
+                        }
+                      >
+                        <div>{columnData.COLUMN_NAME}</div>
+                      </Popover>
+                    ),
+                  }),
+                )
+                .sort((a, b) =>
+                  String(a.id)
+                    .toLowerCase()
+                    .localeCompare(String(b.id).toLowerCase()),
                 ),
-              })),
             }),
           ),
         }),
@@ -370,6 +334,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
       case 'VARCHAR':
         return IconNames.FONT;
       case 'BIGINT':
+      case 'FLOAT':
         return IconNames.NUMERICAL;
       default:
         return IconNames.HELP;
diff --git a/web-console/src/views/query-view/query-output/query-output.spec.tsx b/web-console/src/views/query-view/query-output/query-output.spec.tsx
index 3cfb424..ae3d06e 100644
--- a/web-console/src/views/query-view/query-output/query-output.spec.tsx
+++ b/web-console/src/views/query-view/query-output/query-output.spec.tsx
@@ -24,14 +24,7 @@ import { QueryOutput } from './query-output';
 describe('query output', () => {
   it('matches snapshot', () => {
     const queryOutput = (
-      <QueryOutput
-        runeMode={false}
-        sqlOrderBy={() => null}
-        sqlFilterRow={() => null}
-        sqlExcludeColumn={() => null}
-        loading={false}
-        error="lol"
-      />
+      <QueryOutput runeMode={false} onQueryChange={() => {}} loading={false} error="lol" />
     );
 
     const { container } = render(queryOutput);
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
index b56e1bf..17878d5 100644
--- a/web-console/src/views/query-view/query-output/query-output.tsx
+++ b/web-console/src/views/query-view/query-output/query-output.tsx
@@ -28,17 +28,14 @@ import ReactTable from 'react-table';
 
 import { copyAndAlert } from '../../../utils';
 import { BasicAction, basicActionsToMenu } from '../../../utils/basic-action';
-import { RowFilter } from '../query-view';
 
 import './query-output.scss';
 
 export interface QueryOutputProps {
   loading: boolean;
-  sqlFilterRow: (filters: RowFilter[], run: boolean) => void;
-  sqlExcludeColumn: (header: string, run: boolean) => void;
-  sqlOrderBy: (header: string, direction: 'ASC' | 'DESC', run: boolean) => void;
   queryResult?: HeaderRows;
   parsedQuery?: SqlQuery;
+  onQueryChange: (query: SqlQuery, run?: boolean) => void;
   error?: string;
   runeMode: boolean;
 }
@@ -97,7 +94,7 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
     );
   }
   getHeaderActions(h: string) {
-    const { parsedQuery, sqlExcludeColumn, sqlOrderBy, runeMode } = this.props;
+    const { parsedQuery, onQueryChange, runeMode } = this.props;
 
     let actionsMenu;
     if (parsedQuery) {
@@ -110,7 +107,9 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
             basicActions.push({
               icon: sorted.desc ? IconNames.SORT_ASC : IconNames.SORT_DESC,
               title: `Order by: ${h} ${sorted.desc ? 'ASC' : 'DESC'}`,
-              onAction: () => sqlOrderBy(h, sorted.desc ? 'ASC' : 'DESC', true),
+              onAction: () => {
+                onQueryChange(parsedQuery.orderBy(h, sorted.desc ? 'ASC' : 'DESC'), true);
+              },
             });
           }
         });
@@ -120,19 +119,25 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
           {
             icon: IconNames.SORT_ASC,
             title: `Order by: ${h} ASC`,
-            onAction: () => sqlOrderBy(h, 'ASC', true),
+            onAction: () => {
+              onQueryChange(parsedQuery.orderBy(h, 'ASC'), true);
+            },
           },
           {
             icon: IconNames.SORT_DESC,
             title: `Order by: ${h} DESC`,
-            onAction: () => sqlOrderBy(h, 'DESC', true),
+            onAction: () => {
+              onQueryChange(parsedQuery.orderBy(h, 'DESC'), true);
+            },
           },
         );
       }
       basicActions.push({
         icon: IconNames.CROSS,
         title: `Remove: ${h}`,
-        onAction: () => sqlExcludeColumn(h, true),
+        onAction: () => {
+          onQueryChange(parsedQuery.excludeColumn(h), true);
+        },
       });
       actionsMenu = basicActionsToMenu(basicActions);
     } else {
@@ -176,7 +181,7 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
   }
 
   getRowActions(row: string, header: string) {
-    const { parsedQuery, sqlFilterRow, runeMode } = this.props;
+    const { parsedQuery, onQueryChange, runeMode } = this.props;
 
     let actionsMenu;
     if (parsedQuery) {
@@ -185,24 +190,32 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
           <MenuItem
             icon={IconNames.FILTER_KEEP}
             text={`Filter by: ${header} = ${row}`}
-            onClick={() => sqlFilterRow([{ row, header, operator: '=' }], true)}
+            onClick={() => {
+              onQueryChange(parsedQuery.filterRow(header, row, '='), true);
+            }}
           />
           <MenuItem
             icon={IconNames.FILTER_REMOVE}
             text={`Filter by: ${header} != ${row}`}
-            onClick={() => sqlFilterRow([{ row, header, operator: '!=' }], true)}
+            onClick={() => {
+              onQueryChange(parsedQuery.filterRow(header, row, '!='), true);
+            }}
           />
           {!isNaN(Number(row)) && (
             <>
               <MenuItem
                 icon={IconNames.FILTER_KEEP}
-                text={`Filter by: ${header} > ${row}`}
-                onClick={() => sqlFilterRow([{ row, header, operator: '>' }], true)}
+                text={`Filter by: ${header} >= ${row}`}
+                onClick={() => {
+                  onQueryChange(parsedQuery.filterRow(header, row, '>='), true);
+                }}
               />
               <MenuItem
                 icon={IconNames.FILTER_KEEP}
                 text={`Filter by: ${header} <= ${row}`}
-                onClick={() => sqlFilterRow([{ row, header, operator: '<=' }], true)}
+                onClick={() => {
+                  onQueryChange(parsedQuery.filterRow(header, row, '<='), true);
+                }}
               />
             </>
           )}
diff --git a/web-console/src/views/query-view/query-view.scss b/web-console/src/views/query-view/query-view.scss
index 69d3798..c1a9177 100644
--- a/web-console/src/views/query-view/query-view.scss
+++ b/web-console/src/views/query-view/query-view.scss
@@ -63,6 +63,7 @@ $nav-width: 250px;
           margin-right: 15px;
         }
 
+        .auto-run,
         .smart-query-limit {
           display: inline-block;
           margin-bottom: 8px;
diff --git a/web-console/src/views/query-view/query-view.tsx b/web-console/src/views/query-view/query-view.tsx
index 22eb5e7..b49a574 100644
--- a/web-console/src/views/query-view/query-view.tsx
+++ b/web-console/src/views/query-view/query-view.tsx
@@ -20,18 +20,12 @@ import { Intent, Switch, Tooltip } from '@blueprintjs/core';
 import axios from 'axios';
 import classNames from 'classnames';
 import {
-  AdditiveExpression,
-  Alias,
-  FilterClause,
   HeaderRows,
   isFirstRowHeader,
   normalizeQueryResult,
-  RefExpression,
   shouldIncludeTimestamp,
   sqlParserFactory,
   SqlQuery,
-  StringType,
-  Timestamp,
 } from 'druid-query-toolkit';
 import Hjson from 'hjson';
 import memoizeOne from 'memoize-one';
@@ -51,8 +45,10 @@ import {
   downloadFile,
   getDruidErrorMessage,
   localStorageGet,
+  localStorageGetJson,
   LocalStorageKeys,
   localStorageSet,
+  localStorageSetJson,
   parseQueryPlan,
   queryDruidSql,
   QueryManager,
@@ -89,15 +85,9 @@ export interface QueryViewProps {
   initQuery: string | undefined;
 }
 
-export interface RowFilter {
-  row: string | number | AdditiveExpression | Timestamp | StringType;
-  header: string | Timestamp | StringType;
-  operator: '!=' | '=' | '>' | '<' | 'like' | '>=' | '<=' | 'LIKE';
-}
-
 export interface QueryViewState {
   queryString: string;
-  queryAst: SqlQuery;
+  parsedQuery: SqlQuery;
   queryContext: QueryContext;
   wrapQueryLimit: number | undefined;
   autoRun: boolean;
@@ -196,32 +186,20 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
     super(props, context);
 
     const queryString = props.initQuery || localStorageGet(LocalStorageKeys.QUERY_KEY) || '';
-    const queryAst = queryString ? parser(queryString) : undefined;
-
-    const localStorageQueryHistory = localStorageGet(LocalStorageKeys.QUERY_HISTORY);
-    let queryHistory = [];
-    if (localStorageQueryHistory) {
-      let possibleQueryHistory: unknown;
-      try {
-        possibleQueryHistory = JSON.parse(localStorageQueryHistory);
-      } catch {}
-      if (Array.isArray(possibleQueryHistory)) queryHistory = possibleQueryHistory;
-    }
+    const parsedQuery = queryString ? parser(queryString) : undefined;
 
-    const localStorageAutoRun = localStorageGet(LocalStorageKeys.AUTO_RUN);
-    let autoRun = true;
-    if (localStorageAutoRun) {
-      let possibleAutoRun: unknown;
-      try {
-        possibleAutoRun = JSON.parse(localStorageAutoRun);
-      } catch {}
-      if (typeof possibleAutoRun === 'boolean') autoRun = possibleAutoRun;
-    }
+    const queryContext = localStorageGetJson(LocalStorageKeys.QUERY_CONTEXT) || {};
+
+    const possibleQueryHistory = localStorageGetJson(LocalStorageKeys.QUERY_HISTORY);
+    const queryHistory = Array.isArray(possibleQueryHistory) ? possibleQueryHistory : [];
+
+    const possibleAutoRun = localStorageGetJson(LocalStorageKeys.AUTO_RUN);
+    const autoRun = typeof possibleAutoRun === 'boolean' ? possibleAutoRun : true;
 
     this.state = {
       queryString,
-      queryAst,
-      queryContext: {},
+      parsedQuery,
+      queryContext,
       wrapQueryLimit: 100,
       autoRun,
 
@@ -429,7 +407,10 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
     return (
       <QueryHistoryDialog
         queryRecords={queryHistory}
-        setQueryString={this.handleQueryStringChange}
+        setQueryString={(queryString, queryContext) => {
+          this.handleQueryContextChange(queryContext);
+          this.handleQueryStringChange(queryString);
+        }}
         onClose={() => this.setState({ historyDialogOpen: false })}
       />
     );
@@ -441,22 +422,41 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
 
     return (
       <EditContextDialog
-        onQueryContextChange={(queryContext: QueryContext) =>
-          this.setState({ queryContext, editContextDialogOpen: false })
-        }
-        onClose={() => this.setState({ editContextDialogOpen: false })}
+        onQueryContextChange={this.handleQueryContextChange}
+        onClose={() => {
+          this.setState({ editContextDialogOpen: false });
+        }}
         queryContext={queryContext}
       />
     );
   }
 
+  renderAutoRunSwitch() {
+    const { autoRun, queryString } = this.state;
+    if (QueryView.isJsonLike(queryString)) return;
+
+    return (
+      <Tooltip
+        content="Automatically run queries when modified via helper action menus."
+        hoverOpenDelay={800}
+      >
+        <Switch
+          className="auto-run"
+          checked={autoRun}
+          label="Auto run"
+          onChange={() => this.handleAutoRunChange(!autoRun)}
+        />
+      </Tooltip>
+    );
+  }
+
   renderWrapQueryLimitSelector() {
     const { wrapQueryLimit, queryString } = this.state;
     if (QueryView.isJsonLike(queryString)) return;
 
     return (
       <Tooltip
-        content="Automatically wrap the query with a limit to protect against queries with very large result sets"
+        content="Automatically wrap the query with a limit to protect against queries with very large result sets."
         hoverOpenDelay={800}
       >
         <Switch
@@ -470,15 +470,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
   }
 
   renderMainArea() {
-    const {
-      queryString,
-      queryContext,
-      loading,
-      result,
-      error,
-      columnMetadata,
-      autoRun,
-    } = this.state;
+    const { queryString, queryContext, loading, result, error, columnMetadata } = this.state;
     const emptyQuery = QueryView.isEmptyQuery(queryString);
 
     let currentSchema;
@@ -519,8 +511,6 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
           />
           <div className="control-bar">
             <RunButton
-              autoRun={autoRun}
-              onAutoRunChange={this.handleAutoRunChange}
               onEditContext={() => this.setState({ editContextDialogOpen: true })}
               runeMode={runeMode}
               queryContext={queryContext}
@@ -529,6 +519,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
               onExplain={emptyQuery ? undefined : this.handleExplain}
               onHistory={() => this.setState({ historyDialogOpen: true })}
             />
+            {this.renderAutoRunSwitch()}
             {this.renderWrapQueryLimitSelector()}
             {result && (
               <QueryExtraInfo
@@ -539,110 +530,23 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
           </div>
         </div>
         <QueryOutput
-          sqlExcludeColumn={this.sqlExcludeColumn}
-          sqlFilterRow={this.sqlFilterRow}
-          sqlOrderBy={this.sqlOrderBy}
           runeMode={runeMode}
           loading={loading}
+          error={error}
           queryResult={result ? result.queryResult : undefined}
           parsedQuery={result ? result.parsedQuery : undefined}
-          error={error}
+          onQueryChange={this.handleQueryStringChange}
         />
       </SplitterLayout>
     );
   }
 
-  private addFunctionToGroupBy = (
-    functionName: string,
-    spacing: string[],
-    argumentsArray: (StringType | number)[],
-    preferablyRun: boolean,
-    alias: Alias,
+  private handleQueryStringChange = (
+    queryString: string | SqlQuery,
+    preferablyRun?: boolean,
   ): void => {
-    const { queryAst } = this.state;
-    if (!queryAst) return;
-
-    const modifiedAst = queryAst.addFunctionToGroupBy(functionName, spacing, argumentsArray, alias);
-    this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
-  };
-
-  private addToGroupBy = (columnName: string, preferablyRun: boolean): void => {
-    const { queryAst } = this.state;
-    if (!queryAst) return;
-
-    const modifiedAst = queryAst.addToGroupBy(columnName);
-    this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
-  };
-
-  private replaceFrom = (table: RefExpression, preferablyRun: boolean): void => {
-    const { queryAst } = this.state;
-    if (!queryAst) return;
-
-    const modifiedAst = queryAst.replaceFrom(table);
-    this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
-  };
-
-  private addAggregateColumn = (
-    columnName: string | RefExpression,
-    functionName: string,
-    preferablyRun: boolean,
-    alias?: Alias,
-    distinct?: boolean,
-    filter?: FilterClause,
-  ): void => {
-    const { queryAst } = this.state;
-    if (!queryAst) return;
-
-    const modifiedAst = queryAst.addAggregateColumn(
-      columnName,
-      functionName,
-      alias,
-      distinct,
-      filter,
-    );
-    this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
-  };
-
-  private sqlOrderBy = (
-    header: string,
-    direction: 'ASC' | 'DESC',
-    preferablyRun: boolean,
-  ): void => {
-    const { queryAst } = this.state;
-    if (!queryAst) return;
-
-    const modifiedAst = queryAst.orderBy(header, direction);
-    this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
-  };
-
-  private sqlExcludeColumn = (header: string, preferablyRun: boolean): void => {
-    const { queryAst } = this.state;
-    if (!queryAst) return;
-
-    const modifiedAst = queryAst.excludeColumn(header);
-    this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
-  };
-
-  private sqlFilterRow = (filters: RowFilter[], preferablyRun: boolean): void => {
-    const { queryAst } = this.state;
-    if (!queryAst) return;
-
-    let modifiedAst: SqlQuery = queryAst;
-    for (const filter of filters) {
-      modifiedAst = modifiedAst.filterRow(filter.header, filter.row, filter.operator);
-    }
-    this.handleQueryStringChange(modifiedAst.toString(), preferablyRun);
-  };
-
-  private sqlClearWhere = (column: string, preferablyRun: boolean): void => {
-    const { queryAst } = this.state;
-
-    if (!queryAst) return;
-    this.handleQueryStringChange(queryAst.removeFilter(column).toString(), preferablyRun);
-  };
-
-  private handleQueryStringChange = (queryString: string, preferablyRun?: boolean): void => {
-    this.setState({ queryString, queryAst: parser(queryString) }, () => {
+    if (queryString instanceof SqlQuery) queryString = queryString.toString();
+    this.setState({ queryString, parsedQuery: parser(queryString) }, () => {
       const { autoRun } = this.state;
       if (preferablyRun && autoRun) this.handleRun();
     });
@@ -654,7 +558,7 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
 
   private handleAutoRunChange = (autoRun: boolean) => {
     this.setState({ autoRun });
-    localStorageSet(LocalStorageKeys.AUTO_RUN, String(autoRun));
+    localStorageSetJson(LocalStorageKeys.AUTO_RUN, autoRun);
   };
 
   private handleWrapQueryLimitChange = (wrapQueryLimit: number | undefined) => {
@@ -665,10 +569,15 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
     const { queryString, queryContext, wrapQueryLimit, queryHistory } = this.state;
     if (QueryView.isJsonLike(queryString) && !QueryView.validRune(queryString)) return;
 
-    const newQueryHistory = QueryHistoryDialog.addQueryToHistory(queryHistory, queryString);
+    const newQueryHistory = QueryHistoryDialog.addQueryToHistory(
+      queryHistory,
+      queryString,
+      queryContext,
+    );
 
-    localStorageSet(LocalStorageKeys.QUERY_HISTORY, JSON.stringify(newQueryHistory));
+    localStorageSetJson(LocalStorageKeys.QUERY_HISTORY, newQueryHistory);
     localStorageSet(LocalStorageKeys.QUERY_KEY, queryString);
+    localStorageSetJson(LocalStorageKeys.QUERY_CONTEXT, queryContext);
 
     this.setState({ queryHistory: newQueryHistory });
     this.sqlQueryManager.runQuery({ queryString, queryContext, wrapQueryLimit });
@@ -685,19 +594,19 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
     localStorageSet(LocalStorageKeys.QUERY_VIEW_PANE_SIZE, String(secondaryPaneSize));
   };
 
-  private getQueryAst = () => {
-    const { queryAst } = this.state;
-    return queryAst;
+  private getParsedQuery = () => {
+    const { parsedQuery } = this.state;
+    return parsedQuery;
   };
 
   render(): JSX.Element {
-    const { columnMetadata, columnMetadataLoading, columnMetadataError, queryAst } = this.state;
+    const { columnMetadata, columnMetadataLoading, columnMetadataError, parsedQuery } = this.state;
 
     let defaultSchema;
     let defaultTable;
-    if (queryAst instanceof SqlQuery) {
-      defaultSchema = queryAst.getSchema();
-      defaultTable = queryAst.getTableName();
+    if (parsedQuery instanceof SqlQuery) {
+      defaultSchema = parsedQuery.getSchema();
+      defaultTable = parsedQuery.getTableName();
     }
 
     return (
@@ -706,18 +615,12 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
       >
         {!columnMetadataError && (
           <ColumnTree
-            clear={this.sqlClearWhere}
-            filterByRow={this.sqlFilterRow}
-            addFunctionToGroupBy={this.addFunctionToGroupBy}
-            addAggregateColumn={this.addAggregateColumn}
-            addToGroupBy={this.addToGroupBy}
-            queryAst={this.getQueryAst}
+            getParsedQuery={this.getParsedQuery}
             columnMetadataLoading={columnMetadataLoading}
             columnMetadata={columnMetadata}
             onQueryStringChange={this.handleQueryStringChange}
             defaultSchema={defaultSchema ? defaultSchema : 'druid'}
             defaultTable={defaultTable}
-            replaceFrom={this.replaceFrom}
           />
         )}
         {this.renderMainArea()}
diff --git a/web-console/src/views/query-view/run-button/run-button.spec.tsx b/web-console/src/views/query-view/run-button/run-button.spec.tsx
index cbe2678..ff0fd77 100644
--- a/web-console/src/views/query-view/run-button/run-button.spec.tsx
+++ b/web-console/src/views/query-view/run-button/run-button.spec.tsx
@@ -25,12 +25,10 @@ describe('run button', () => {
   it('matches snapshot', () => {
     const runButton = (
       <RunButton
-        autoRun
-        onAutoRunChange={() => {}}
         onHistory={() => {}}
         onEditContext={() => {}}
         runeMode={false}
-        queryContext={{}}
+        queryContext={{ f: 3 }}
         onQueryContextChange={() => {}}
         onRun={() => {}}
         onExplain={() => {}}
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
index 956b8ac..733d5cf 100644
--- a/web-console/src/views/query-view/run-button/run-button.tsx
+++ b/web-console/src/views/query-view/run-button/run-button.tsx
@@ -33,6 +33,7 @@ import { IconNames } from '@blueprintjs/icons';
 import React from 'react';
 
 import { MenuCheckbox } from '../../../components';
+import { pluralIfNeeded } from '../../../utils';
 import {
   getUseApproximateCountDistinct,
   getUseApproximateTopN,
@@ -46,8 +47,6 @@ import { DRUID_DOCS_RUNE, DRUID_DOCS_SQL } from '../../../variables';
 
 export interface RunButtonProps {
   runeMode: boolean;
-  autoRun: boolean;
-  onAutoRunChange: (autoRun: boolean) => void;
   queryContext: QueryContext;
   onQueryContextChange: (newQueryContext: QueryContext) => void;
   onRun: (() => void) | undefined;
@@ -58,10 +57,6 @@ export interface RunButtonProps {
 
 @HotkeysTarget
 export class RunButton extends React.PureComponent<RunButtonProps> {
-  constructor(props: RunButtonProps, context: any) {
-    super(props, context);
-  }
-
   public renderHotkeys() {
     return (
       <Hotkeys>
@@ -90,31 +85,27 @@ export class RunButton extends React.PureComponent<RunButtonProps> {
       onQueryContextChange,
       onEditContext,
       onHistory,
-      autoRun,
-      onAutoRunChange,
     } = this.props;
 
     const useCache = getUseCache(queryContext);
     const useApproximateCountDistinct = getUseApproximateCountDistinct(queryContext);
     const useApproximateTopN = getUseApproximateTopN(queryContext);
+    const numContextKeys = Object.keys(queryContext).length;
 
     return (
       <Menu>
         <MenuItem
           icon={IconNames.HELP}
-          text="Query docs"
+          text={runeMode ? 'Native query documentation' : 'DruidSQL documentation'}
           href={runeMode ? DRUID_DOCS_RUNE : DRUID_DOCS_SQL}
           target="_blank"
         />
+        <MenuItem icon={IconNames.HISTORY} text="Query history" onClick={onHistory} />
         {!runeMode && (
           <>
-            {onExplain && <MenuItem icon={IconNames.CLEAN} text="Explain" onClick={onExplain} />}
-            <MenuItem icon={IconNames.HISTORY} text="History" onClick={onHistory} />
-            <MenuCheckbox
-              checked={autoRun}
-              label="Auto run queries"
-              onChange={() => onAutoRunChange(!autoRun)}
-            />
+            {onExplain && (
+              <MenuItem icon={IconNames.CLEAN} text="Explain SQL query" onClick={onExplain} />
+            )}
             <MenuCheckbox
               checked={useApproximateCountDistinct}
               label="Use approximate COUNT(DISTINCT)"
@@ -141,7 +132,12 @@ export class RunButton extends React.PureComponent<RunButtonProps> {
           }}
         />
         {!runeMode && (
-          <MenuItem icon={IconNames.PROPERTIES} text="Edit context" onClick={onEditContext} />
+          <MenuItem
+            icon={IconNames.PROPERTIES}
+            text="Edit context"
+            onClick={onEditContext}
+            labelElement={numContextKeys ? pluralIfNeeded(numContextKeys, 'key') : undefined}
+          />
         )}
       </Menu>
     );
@@ -166,7 +162,7 @@ export class RunButton extends React.PureComponent<RunButtonProps> {
           <Button icon={IconNames.CARET_RIGHT} text={runButtonText} disabled />
         )}
         <Popover position={Position.BOTTOM_LEFT} content={this.renderExtraMenu()}>
-          <Button icon={IconNames.MORE} intent={Intent.PRIMARY} />
+          <Button icon={IconNames.MORE} intent={onRun ? Intent.PRIMARY : undefined} />
         </Popover>
       </ButtonGroup>
     );
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 a64c9b7..226ad30 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
@@ -348,6 +348,12 @@ exports[`tasks view matches snapshot 1`] = `
             active={false}
             onClick={[Function]}
           >
+            Group ID
+          </Blueprint3.Button>
+          <Blueprint3.Button
+            active={false}
+            onClick={[Function]}
+          >
             Type
           </Blueprint3.Button>
           <Blueprint3.Button
@@ -455,6 +461,7 @@ exports[`tasks view matches snapshot 1`] = `
           columns={
             Array [
               "Task ID",
+              "Group ID",
               "Type",
               "Datasource",
               "Location",
@@ -533,6 +540,13 @@ exports[`tasks view matches snapshot 1`] = `
               "width": 300,
             },
             Object {
+              "Aggregated": [Function],
+              "Header": "Group ID",
+              "accessor": "group_id",
+              "show": true,
+              "width": 300,
+            },
+            Object {
               "Cell": [Function],
               "Header": "Type",
               "accessor": "type",
diff --git a/web-console/src/views/task-view/tasks-view.tsx b/web-console/src/views/task-view/tasks-view.tsx
index a02ed72..3b635ce 100644
--- a/web-console/src/views/task-view/tasks-view.tsx
+++ b/web-console/src/views/task-view/tasks-view.tsx
@@ -68,6 +68,7 @@ const supervisorTableColumns: string[] = [
 ];
 const taskTableColumns: string[] = [
   'Task ID',
+  'Group ID',
   'Type',
   'Datasource',
   'Location',
@@ -109,7 +110,7 @@ export interface TasksViewState {
   taskFilter: Filter[];
   supervisorFilter: Filter[];
 
-  groupTasksBy?: 'type' | 'datasource' | 'status';
+  groupTasksBy?: 'group_id' | 'type' | 'datasource' | 'status';
 
   killTaskId?: string;
 
@@ -191,7 +192,7 @@ export class TasksView extends React.PureComponent<TasksViewProps, TasksViewStat
   };
 
   static TASK_SQL = `SELECT
-  "task_id", "type", "datasource", "created_time", "location", "duration", "error_msg",
+  "task_id", "group_id", "type", "datasource", "created_time", "location", "duration", "error_msg",
   CASE WHEN "status" = 'RUNNING' THEN "runner_status" ELSE "status" END AS "status",
   (
     CASE WHEN "status" = 'RUNNING' THEN
@@ -745,6 +746,13 @@ ORDER BY "rank" DESC, "created_time" DESC`;
               show: hiddenTaskColumns.exists('Task ID'),
             },
             {
+              Header: 'Group ID',
+              accessor: 'group_id',
+              width: 300,
+              Aggregated: () => '',
+              show: hiddenTaskColumns.exists('Group ID'),
+            },
+            {
               Header: 'Type',
               accessor: 'type',
               Cell: row => {
@@ -1126,6 +1134,12 @@ ORDER BY "rank" DESC, "created_time" DESC`;
                   None
                 </Button>
                 <Button
+                  active={groupTasksBy === 'group_id'}
+                  onClick={() => this.setState({ groupTasksBy: 'group_id' })}
+                >
+                  Group ID
+                </Button>
+                <Button
                   active={groupTasksBy === 'type'}
                   onClick={() => this.setState({ groupTasksBy: 'type' })}
                 >
diff --git a/web-console/src/visualization/bar-group.tsx b/web-console/src/visualization/bar-group.tsx
index 0856905..50b188c 100644
--- a/web-console/src/visualization/bar-group.tsx
+++ b/web-console/src/visualization/bar-group.tsx
@@ -24,7 +24,7 @@ import { BarUnitData } from '../components/segment-timeline/segment-timeline';
 import { BarUnit } from './bar-unit';
 import { HoveredBarInfo } from './stacked-bar-chart';
 
-interface BarGroupProps extends React.Props<any> {
+interface BarGroupProps {
   dataToRender: BarUnitData[];
   changeActiveDatasource: (e: string) => void;
   formatTick: (e: number) => string;
diff --git a/web-console/src/visualization/bar-unit.tsx b/web-console/src/visualization/bar-unit.tsx
index 6c43b1c..aeda205 100644
--- a/web-console/src/visualization/bar-unit.tsx
+++ b/web-console/src/visualization/bar-unit.tsx
@@ -20,7 +20,7 @@ import React from 'react';
 
 import './bar-unit.scss';
 
-interface BarChartUnitProps extends React.Props<any> {
+interface BarChartUnitProps {
   x: number | undefined;
   y: number;
   width: number;
diff --git a/web-console/src/visualization/chart-axis.tsx b/web-console/src/visualization/chart-axis.tsx
index 294a64b..e71b6ba 100644
--- a/web-console/src/visualization/chart-axis.tsx
+++ b/web-console/src/visualization/chart-axis.tsx
@@ -19,7 +19,7 @@
 import * as d3 from 'd3';
 import React from 'react';
 
-interface ChartAxisProps extends React.Props<any> {
+interface ChartAxisProps {
   transform: string;
   scale: any;
   className?: string;
diff --git a/web-console/src/visualization/stacked-bar-chart.tsx b/web-console/src/visualization/stacked-bar-chart.tsx
index 8a96e07..8a6a47e 100644
--- a/web-console/src/visualization/stacked-bar-chart.tsx
+++ b/web-console/src/visualization/stacked-bar-chart.tsx
@@ -27,7 +27,7 @@ import { ChartAxis } from './chart-axis';
 
 import './stacked-bar-chart.scss';
 
-interface StackedBarChartProps extends React.Props<any> {
+interface StackedBarChartProps {
   svgWidth: number;
   svgHeight: number;
   margin: BarChartMargin;


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