You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@druid.apache.org by cw...@apache.org on 2019/08/15 08:22:38 UTC

[incubator-druid] branch master updated: Web-Console: add more side column functions to query view (#8283)

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

cwylie 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 8924d28  Web-Console: add more side column functions to query view (#8283)
8924d28 is described below

commit 8924d285dc39dc548db611aa4429ae5e62d64a44
Author: mcbrewster <37...@users.noreply.github.com>
AuthorDate: Thu Aug 15 01:22:25 2019 -0700

    Web-Console: add more side column functions to query view (#8283)
    
    * small fixes
    
    * run jest -u
    
    * add trim
    
    * add to groupby
    
    * save
    
    * add functionality to column tree menu
    
    * remove history dialog
    
    * rename file
    
    * fixes
    
    * add new values to time-menu
    
    * add query dialog
    
    * add more functions to collumn-tree menu
    
    * add menu functions
    
    * remove test
    
    * update tests, fix extra 0s, clear in between time filters
    
    * fixes
    
    * add deffered
    
    * save
    
    * add alias's
    
    * change menu style
    
    * fix substring
    
    * small fixes
    
    * add auto run to run button
    
    * move divider to a reasonable position
    
    * fix auto expland
    
    * run jest -u
    
    * fix auto run
    
    * prevent aggregate cooumn changes
    
    * fix group by bug
    
    * add quertAst to state
    
    * ui fixes
    
    * prevent filter on non existant ast
    
    * add auto run to shows
    
    * use handle run
    
    * handle *
    
    * add alias and tslint fix
---
 web-console/package-lock.json                      |   6 +-
 web-console/package.json                           |   2 +-
 .../deferred/__snapshots__/deferred.spec.tsx.snap  |   3 +
 .../deferred/deferred.spec.tsx}                    |  13 +-
 .../deferred/deferred.tsx}                         |  26 +-
 .../query-plan-history.spec.tsx.snap}              |  35 +-
 .../query-history-dialog.scss}                     |  25 +-
 .../query-history-dialog/query-history-dialog.tsx  |  91 +++++
 .../query-plan-history.spec.tsx}                   |   4 +-
 .../__snapshots__/query-plan-dialog.spec.tsx.snap  |  10 +
 .../query-plan-dialog/query-plan-dialog.spec.tsx   |   7 +-
 .../query-plan-dialog/query-plan-dialog.tsx        |  34 +-
 web-console/src/utils/local-storage-keys.tsx       |   2 +
 .../__snapshots__/query-view.spec.tsx.snap         |  12 +-
 .../__snapshots__/number-menu-items.spec.tsx.snap  |   3 +
 .../number-menu-items/number-menu-items.spec.tsx}  |  21 +-
 .../number-menu-items/number-menu-items.tsx        | 141 ++++++++
 .../__snapshots__/string-menu-items.spec.tsx.snap  |   3 +
 .../string-menu-items/string-menu-items.spec.tsx}  |  21 +-
 .../string-menu-items/string-menu-items.tsx        | 161 +++++++++
 .../__snapshots__/time-menu-items.spec.tsx.snap    |   3 +
 .../time-menu-items/time-menu-items.spec.tsx}      |  22 +-
 .../time-menu-items/time-menu-items.tsx            | 380 +++++++++++++++++++++
 .../query-view/column-tree/column-tree.spec.tsx    |   7 +
 .../views/query-view/column-tree/column-tree.tsx   | 155 ++++++---
 .../query-view/query-output/query-output.spec.tsx  |   1 -
 .../views/query-view/query-output/query-output.tsx | 164 ++++-----
 web-console/src/views/query-view/query-view.tsx    | 347 +++++++++++++++----
 .../query-view/run-button/run-button.spec.tsx      |   3 +
 .../src/views/query-view/run-button/run-button.tsx |  20 +-
 30 files changed, 1451 insertions(+), 271 deletions(-)

diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index fdac733..5c25e00 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -4395,9 +4395,9 @@
       "integrity": "sha512-0sYnfUHHMoajaud/i5BHKA12bUxiWEHJ9rxGqVEppFxsEcxef0TZQ5J59lU+UniEBcz/sG5fTESRyS7cOm3tSQ=="
     },
     "druid-query-toolkit": {
-      "version": "0.3.15",
-      "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.3.15.tgz",
-      "integrity": "sha512-q7uKfUdBItjOyNF1PlWF/rAhOim1uAjI085fsoKIBDZ2o5O4XRjaCKqXtW49Ovv92ks/22zLoYWNdU51i4PB/w==",
+      "version": "0.3.20",
+      "resolved": "https://registry.npmjs.org/druid-query-toolkit/-/druid-query-toolkit-0.3.20.tgz",
+      "integrity": "sha512-jrGNu+o/nD+uhbxAMLXEQrSWNEylCRmkiuFDJSPCMz7cjMNArsdIgyBQHPezNAeTDpReelAt59xJ7pvqXwPIvw==",
       "requires": {
         "tslib": "^1.10.0"
       }
diff --git a/web-console/package.json b/web-console/package.json
index 14ef194..8f3b214 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -61,7 +61,7 @@
     "d3": "^5.9.7",
     "d3-array": "^2.2.0",
     "druid-console": "^0.0.2",
-    "druid-query-toolkit": "^0.3.15",
+    "druid-query-toolkit": "^0.3.20",
     "file-saver": "^2.0.2",
     "has-own-prop": "^2.0.0",
     "hjson": "^3.1.2",
diff --git a/web-console/src/components/deferred/__snapshots__/deferred.spec.tsx.snap b/web-console/src/components/deferred/__snapshots__/deferred.spec.tsx.snap
new file mode 100644
index 0000000..60bc789
--- /dev/null
+++ b/web-console/src/components/deferred/__snapshots__/deferred.spec.tsx.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`deferred matches snapshot 1`] = `<div />`;
diff --git a/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.spec.tsx b/web-console/src/components/deferred/deferred.spec.tsx
similarity index 75%
copy from web-console/src/dialogs/query-plan-dialog/query-plan-dialog.spec.tsx
copy to web-console/src/components/deferred/deferred.spec.tsx
index 465436d..8125faf 100644
--- a/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.spec.tsx
+++ b/web-console/src/components/deferred/deferred.spec.tsx
@@ -19,14 +19,13 @@
 import { render } from '@testing-library/react';
 import React from 'react';
 
-import { QueryPlanDialog } from './query-plan-dialog';
+import { Deferred } from './deferred';
 
-describe('query plan dialog', () => {
+describe('deferred', () => {
   it('matches snapshot', () => {
-    const queryPlanDialog = (
-      <QueryPlanDialog explainResult={'test'} explainError={undefined} onClose={() => {}} />
-    );
-    render(queryPlanDialog);
-    expect(document.body.lastChild).toMatchSnapshot();
+    const deferred = <Deferred content={() => <div />} />;
+
+    const { container } = render(deferred);
+    expect(container.firstChild).toMatchSnapshot();
   });
 });
diff --git a/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.spec.tsx b/web-console/src/components/deferred/deferred.tsx
similarity index 67%
copy from web-console/src/dialogs/query-plan-dialog/query-plan-dialog.spec.tsx
copy to web-console/src/components/deferred/deferred.tsx
index 465436d..6194979 100644
--- a/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.spec.tsx
+++ b/web-console/src/components/deferred/deferred.tsx
@@ -16,17 +16,21 @@
  * limitations under the License.
  */
 
-import { render } from '@testing-library/react';
 import React from 'react';
 
-import { QueryPlanDialog } from './query-plan-dialog';
+export interface DeferredProps {
+  content: () => JSX.Element;
+}
 
-describe('query plan dialog', () => {
-  it('matches snapshot', () => {
-    const queryPlanDialog = (
-      <QueryPlanDialog explainResult={'test'} explainError={undefined} onClose={() => {}} />
-    );
-    render(queryPlanDialog);
-    expect(document.body.lastChild).toMatchSnapshot();
-  });
-});
+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/dialogs/query-plan-dialog/__snapshots__/query-plan-dialog.spec.tsx.snap b/web-console/src/dialogs/query-history-dialog/__snapshots__/query-plan-history.spec.tsx.snap
similarity index 71%
copy from web-console/src/dialogs/query-plan-dialog/__snapshots__/query-plan-dialog.spec.tsx.snap
copy to web-console/src/dialogs/query-history-dialog/__snapshots__/query-plan-history.spec.tsx.snap
index abd814e..d7efc74 100644
--- a/web-console/src/dialogs/query-plan-dialog/__snapshots__/query-plan-dialog.spec.tsx.snap
+++ b/web-console/src/dialogs/query-history-dialog/__snapshots__/query-plan-history.spec.tsx.snap
@@ -16,7 +16,7 @@ exports[`query plan dialog matches snapshot 1`] = `
       tabindex="0"
     >
       <div
-        class="bp3-dialog query-plan-dialog"
+        class="bp3-dialog query-history-dialog"
       >
         <div
           class="bp3-dialog-header"
@@ -24,7 +24,7 @@ exports[`query plan dialog matches snapshot 1`] = `
           <h4
             class="bp3-heading"
           >
-            Query plan
+            Query history
           </h4>
           <button
             aria-label="Close"
@@ -55,8 +55,25 @@ exports[`query plan dialog matches snapshot 1`] = `
         <div
           class="bp3-dialog-body"
         >
-          <div>
-            test
+          <div
+            class="bp3-tabs bp3-vertical tab-area"
+          >
+            <div
+              class="bp3-tab-list"
+              role="tablist"
+            >
+              <div
+                class="bp3-tab-indicator-wrapper"
+                style="display: none;"
+              >
+                <div
+                  class="bp3-tab-indicator"
+                />
+              </div>
+              <div
+                class="bp3-flex-expander"
+              />
+            </div>
           </div>
         </div>
         <div
@@ -75,6 +92,16 @@ 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.spec.tsx b/web-console/src/dialogs/query-history-dialog/query-history-dialog.scss
similarity index 65%
copy from web-console/src/dialogs/query-plan-dialog/query-plan-dialog.spec.tsx
copy to web-console/src/dialogs/query-history-dialog/query-history-dialog.scss
index 465436d..34bf4b7 100644
--- a/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.spec.tsx
+++ b/web-console/src/dialogs/query-history-dialog/query-history-dialog.scss
@@ -16,17 +16,18 @@
  * limitations under the License.
  */
 
-import { render } from '@testing-library/react';
-import React from 'react';
+.query-history-dialog {
+  &.bp3-dialog {
+    width: 900px;
+  }
 
-import { QueryPlanDialog } from './query-plan-dialog';
+  .panel {
+    width: 100%;
+  }
 
-describe('query plan dialog', () => {
-  it('matches snapshot', () => {
-    const queryPlanDialog = (
-      <QueryPlanDialog explainResult={'test'} explainError={undefined} onClose={() => {}} />
-    );
-    render(queryPlanDialog);
-    expect(document.body.lastChild).toMatchSnapshot();
-  });
-});
+  .text-area {
+    width: 100%;
+    height: 500px;
+    resize: none;
+  }
+}
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
new file mode 100644
index 0000000..c17dbc6
--- /dev/null
+++ b/web-console/src/dialogs/query-history-dialog/query-history-dialog.tsx
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Button, Classes, Dialog, Intent, Tab, Tabs, TextArea } from '@blueprintjs/core';
+import React from 'react';
+
+import './query-history-dialog.scss';
+
+export interface QueryRecord {
+  version: string;
+  queryString: string;
+}
+export interface QueryHistoryDialogProps {
+  setQueryString: (queryString: string) => void;
+  onClose: () => void;
+  queryRecords: QueryRecord[];
+}
+
+export interface QueryHistoryDialogState {
+  activeTab: number;
+}
+
+export class QueryHistoryDialog extends React.PureComponent<
+  QueryHistoryDialogProps,
+  QueryHistoryDialogState
+> {
+  constructor(props: QueryHistoryDialogProps) {
+    super(props);
+    this.state = {
+      activeTab: 0,
+    };
+  }
+
+  render(): JSX.Element {
+    const { onClose, queryRecords, setQueryString } = this.props;
+    const { activeTab } = this.state;
+
+    const versions = queryRecords.map((record, index) => (
+      <Tab
+        id={index}
+        key={index}
+        title={record.version}
+        panel={<TextArea readOnly value={record.queryString} className={'text-area'} />}
+        panelClassName={'panel'}
+      />
+    ));
+
+    return (
+      <Dialog className="query-history-dialog" isOpen onClose={onClose} title="Query history">
+        <div className={Classes.DIALOG_BODY}>
+          <Tabs
+            animate
+            renderActiveTabPanelOnly
+            vertical
+            className={'tab-area'}
+            selectedTabId={activeTab}
+            onChange={(tab: number) => this.setState({ activeTab: tab })}
+          >
+            {versions}
+            <Tabs.Expander />
+          </Tabs>
+        </div>
+        <div className={Classes.DIALOG_FOOTER}>
+          <div className={Classes.DIALOG_FOOTER_ACTIONS}>
+            <Button text="Close" onClick={onClose} />
+            <Button
+              text="Open"
+              intent={Intent.PRIMARY}
+              onClick={() => setQueryString(queryRecords[activeTab].queryString)}
+            />
+          </div>
+        </div>
+      </Dialog>
+    );
+  }
+}
diff --git a/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.spec.tsx b/web-console/src/dialogs/query-history-dialog/query-plan-history.spec.tsx
similarity index 87%
copy from web-console/src/dialogs/query-plan-dialog/query-plan-dialog.spec.tsx
copy to web-console/src/dialogs/query-history-dialog/query-plan-history.spec.tsx
index 465436d..d164c3e 100644
--- a/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.spec.tsx
+++ b/web-console/src/dialogs/query-history-dialog/query-plan-history.spec.tsx
@@ -19,12 +19,12 @@
 import { render } from '@testing-library/react';
 import React from 'react';
 
-import { QueryPlanDialog } from './query-plan-dialog';
+import { QueryHistoryDialog } from './query-history-dialog';
 
 describe('query plan dialog', () => {
   it('matches snapshot', () => {
     const queryPlanDialog = (
-      <QueryPlanDialog explainResult={'test'} explainError={undefined} onClose={() => {}} />
+      <QueryHistoryDialog setQueryString={() => null} queryRecords={[]} onClose={() => {}} />
     );
     render(queryPlanDialog);
     expect(document.body.lastChild).toMatchSnapshot();
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 abd814e..e91d4b8 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
@@ -75,6 +75,16 @@ 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.spec.tsx b/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.spec.tsx
index 465436d..de7bc91 100644
--- a/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.spec.tsx
+++ b/web-console/src/dialogs/query-plan-dialog/query-plan-dialog.spec.tsx
@@ -24,7 +24,12 @@ import { QueryPlanDialog } from './query-plan-dialog';
 describe('query plan dialog', () => {
   it('matches snapshot', () => {
     const queryPlanDialog = (
-      <QueryPlanDialog explainResult={'test'} explainError={undefined} onClose={() => {}} />
+      <QueryPlanDialog
+        setQueryString={() => null}
+        explainResult={'test'}
+        explainError={undefined}
+        onClose={() => {}}
+      />
     );
     render(queryPlanDialog);
     expect(document.body.lastChild).toMatchSnapshot();
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 94bcdf3..000cb9a 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
@@ -16,7 +16,15 @@
  * limitations under the License.
  */
 
-import { Button, Classes, Dialog, FormGroup, InputGroup, TextArea } from '@blueprintjs/core';
+import {
+  Button,
+  Classes,
+  Dialog,
+  FormGroup,
+  InputGroup,
+  Intent,
+  TextArea,
+} from '@blueprintjs/core';
 import React from 'react';
 
 import { BasicQueryExplanation, SemiJoinQueryExplanation } from '../../utils';
@@ -27,6 +35,7 @@ export interface QueryPlanDialogProps {
   explainResult?: BasicQueryExplanation | SemiJoinQueryExplanation | string;
   explainError?: string;
   onClose: () => void;
+  setQueryString: (queryString: string) => void;
 }
 
 export interface QueryPlanDialogState {}
@@ -40,8 +49,10 @@ export class QueryPlanDialog extends React.PureComponent<
     this.state = {};
   }
 
+  private queryString: string = '';
+
   render(): JSX.Element {
-    const { explainResult, explainError, onClose } = this.props;
+    const { explainResult, explainError, onClose, setQueryString } = this.props;
 
     let content: JSX.Element;
 
@@ -60,17 +71,15 @@ export class QueryPlanDialog extends React.PureComponent<
         );
       }
 
+      this.queryString = JSON.stringify(
+        (explainResult as BasicQueryExplanation).query[0],
+        undefined,
+        2,
+      );
       content = (
         <div className="one-query">
           <FormGroup label="Query">
-            <TextArea
-              readOnly
-              value={JSON.stringify(
-                (explainResult as BasicQueryExplanation).query[0],
-                undefined,
-                2,
-              )}
-            />
+            <TextArea readOnly value={this.queryString} />
           </FormGroup>
           {signature}
         </div>
@@ -136,6 +145,11 @@ 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)}
+            />
           </div>
         </div>
       </Dialog>
diff --git a/web-console/src/utils/local-storage-keys.tsx b/web-console/src/utils/local-storage-keys.tsx
index 0dec7d5..4f116df 100644
--- a/web-console/src/utils/local-storage-keys.tsx
+++ b/web-console/src/utils/local-storage-keys.tsx
@@ -34,6 +34,8 @@ export const LocalStorageKeys = {
   SERVERS_REFRESH_RATE: 'servers-refresh-rate' as 'servers-refresh-rate',
   SUPERVISORS_REFRESH_RATE: 'supervisors-refresh-rate' as 'supervisors-refresh-rate',
   LOOKUPS_REFRESH_RATE: 'lookups-refresh-rate' as 'lookups-refresh-rate',
+  QUERY_HISTORY: 'query-history' as 'query-history',
+  AUTO_RUN: 'auto-run' as 'auto-run',
 };
 export type LocalStorageKeys = typeof LocalStorageKeys[keyof typeof LocalStorageKeys];
 
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 4bc32b5..d5139b4 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,8 +5,16 @@ 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]}
+    hasGroupBy={[Function]}
     onQueryStringChange={[Function]}
+    queryAst={[Function]}
   />
   <t
     customClassName=""
@@ -32,17 +40,19 @@ exports[`sql view matches snapshot 1`] = `
         className="control-bar"
       >
         <HotkeysTarget(RunButton)
+          autoRun={true}
           onEditContext={[Function]}
           onExplain={[Function]}
+          onHistory={[Function]}
           onQueryContextChange={[Function]}
           onRun={[Function]}
           queryContext={Object {}}
           runeMode={false}
+          setAutoRun={[Function]}
         />
       </div>
     </div>
     <QueryOutput
-      disabled={true}
       loading={false}
       runeMode={false}
       sqlExcludeColumn={[Function]}
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/__snapshots__/number-menu-items.spec.tsx.snap b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/__snapshots__/number-menu-items.spec.tsx.snap
new file mode 100644
index 0000000..66ec883
--- /dev/null
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/__snapshots__/number-menu-items.spec.tsx.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`number menu matches snapshot 1`] = `null`;
diff --git a/web-console/src/views/query-view/run-button/run-button.spec.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx
similarity index 73%
copy from web-console/src/views/query-view/run-button/run-button.spec.tsx
copy to web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx
index 1560f7a..2705f67 100644
--- a/web-console/src/views/query-view/run-button/run-button.spec.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.spec.tsx
@@ -19,22 +19,21 @@
 import { render } from '@testing-library/react';
 import React from 'react';
 
-import { RunButton } from './run-button';
+import { NumberMenuItems } from './number-menu-items';
 
-describe('run button', () => {
+describe('number menu', () => {
   it('matches snapshot', () => {
-    const runButton = (
-      <RunButton
-        onEditContext={() => null}
-        runeMode={false}
-        queryContext={{}}
-        onQueryContextChange={() => {}}
-        onRun={() => {}}
-        onExplain={() => {}}
+    const numberMenu = (
+      <NumberMenuItems
+        addFunctionToGroupBy={() => null}
+        addToGroupBy={() => null}
+        addAggregateColumn={() => null}
+        filterByRow={() => null}
+        columnName={'text'}
       />
     );
 
-    const { container } = render(runButton);
+    const { container } = render(numberMenu);
     expect(container.firstChild).toMatchSnapshot();
   });
 });
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
new file mode 100644
index 0000000..5c67910
--- /dev/null
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/number-menu-items/number-menu-items.tsx
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { MenuItem } from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
+import { Alias, FilterClause, 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;
+}
+
+export class NumberMenuItems extends React.PureComponent<NumberMenuItemsProps> {
+  constructor(props: NumberMenuItemsProps, context: any) {
+    super(props, context);
+  }
+
+  renderFilterMenu(): JSX.Element {
+    const { columnName, filterByRow } = this.props;
+
+    return (
+      <MenuItem icon={IconNames.FILTER} text={`Filter`}>
+        <MenuItem
+          text={`"${columnName}" > 100`}
+          onClick={() => filterByRow([{ row: 100, header: columnName, operator: '>' }], false)}
+        />
+        <MenuItem
+          text={`"${columnName}" <= 100`}
+          onClick={() => filterByRow([{ row: 100, header: columnName, operator: '<=' }], false)}
+        />
+      </MenuItem>
+    );
+  }
+
+  renderGroupByMenu(): JSX.Element {
+    const { columnName, addFunctionToGroupBy, addToGroupBy } = this.props;
+
+    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,
+              ],
+              true,
+              aliasFactory(`${columnName}_truncated`),
+            )
+          }
+        />
+      </MenuItem>
+    );
+  }
+
+  renderAggregateMenu(): JSX.Element {
+    const { columnName, addAggregateColumn } = this.props;
+    return (
+      <MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
+        <MenuItem
+          text={`SUM(${columnName}) AS "sum_${columnName}"`}
+          onClick={() =>
+            addAggregateColumn(columnName, 'SUM', true, aliasFactory(`sum_${columnName}`))
+          }
+        />
+        <MenuItem
+          text={`MAX(${columnName}) AS "max_${columnName}"`}
+          onClick={() =>
+            addAggregateColumn(columnName, 'MAX', true, aliasFactory(`max_${columnName}`))
+          }
+        />
+        <MenuItem
+          text={`MIN(${columnName}) AS "min_${columnName}"`}
+          onClick={() =>
+            addAggregateColumn(columnName, 'MIN', true, aliasFactory(`min_${columnName}`))
+          }
+        />
+      </MenuItem>
+    );
+  }
+
+  render(): JSX.Element {
+    const { queryAst } = this.props;
+    let hasGroupBy;
+    if (queryAst) {
+      hasGroupBy = queryAst.groupByClause;
+    }
+
+    return (
+      <>
+        {queryAst && this.renderFilterMenu()}
+        {hasGroupBy && this.renderGroupByMenu()}
+        {hasGroupBy && this.renderAggregateMenu()}
+      </>
+    );
+  }
+}
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/__snapshots__/string-menu-items.spec.tsx.snap b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/__snapshots__/string-menu-items.spec.tsx.snap
new file mode 100644
index 0000000..dbd66e4
--- /dev/null
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/__snapshots__/string-menu-items.spec.tsx.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`string menu matches snapshot 1`] = `null`;
diff --git a/web-console/src/views/query-view/run-button/run-button.spec.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx
similarity index 73%
copy from web-console/src/views/query-view/run-button/run-button.spec.tsx
copy to web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx
index 1560f7a..7573435 100644
--- a/web-console/src/views/query-view/run-button/run-button.spec.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.spec.tsx
@@ -19,22 +19,21 @@
 import { render } from '@testing-library/react';
 import React from 'react';
 
-import { RunButton } from './run-button';
+import { StringMenuItems } from './string-menu-items';
 
-describe('run button', () => {
+describe('string menu', () => {
   it('matches snapshot', () => {
-    const runButton = (
-      <RunButton
-        onEditContext={() => null}
-        runeMode={false}
-        queryContext={{}}
-        onQueryContextChange={() => {}}
-        onRun={() => {}}
-        onExplain={() => {}}
+    const stringMenu = (
+      <StringMenuItems
+        addFunctionToGroupBy={() => null}
+        addToGroupBy={() => null}
+        addAggregateColumn={() => null}
+        filterByRow={() => null}
+        columnName={'text'}
       />
     );
 
-    const { container } = render(runButton);
+    const { container } = render(stringMenu);
     expect(container.firstChild).toMatchSnapshot();
   });
 });
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
new file mode 100644
index 0000000..79dcc49
--- /dev/null
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/string-menu-items/string-menu-items.tsx
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { 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;
+}
+
+export class StringMenuItems extends React.PureComponent<StringMenuItemsProps> {
+  constructor(props: StringMenuItemsProps, context: any) {
+    super(props, context);
+  }
+
+  renderFilterMenu(): JSX.Element {
+    const { columnName, filterByRow } = this.props;
+
+    return (
+      <MenuItem icon={IconNames.FILTER} text={`Filter`}>
+        <MenuItem
+          text={`"${columnName}" = 'xxx'`}
+          onClick={() => filterByRow([{ row: 'xxx', header: columnName, operator: '=' }], false)}
+        />
+        <MenuItem
+          text={`"${columnName}" LIKE '%xxx%'`}
+          onClick={() => filterByRow([{ row: 'xxx', header: columnName, operator: 'LIKE' }], false)}
+        />
+      </MenuItem>
+    );
+  }
+
+  renderGroupByMenu(): JSX.Element {
+    const { columnName, addFunctionToGroupBy, addToGroupBy } = this.props;
+
+    return (
+      <MenuItem icon={IconNames.GROUP_OBJECTS} text={`Group by`}>
+        <MenuItem text={`"${columnName}"`} onClick={() => addToGroupBy(columnName, true)} />
+        <MenuItem
+          text={`SUBSTRING("${columnName}", 1, 2) AS "${columnName}_substring"`}
+          onClick={() =>
+            addFunctionToGroupBy(
+              'SUBSTRING',
+              [' ', ' '],
+              [stringFactory(columnName, `"`), 1, 2],
+              true,
+              aliasFactory(`${columnName}_substring`),
+            )
+          }
+        />
+      </MenuItem>
+    );
+  }
+
+  renderAggregateMenu(): JSX.Element {
+    const { columnName, addAggregateColumn } = this.props;
+    return (
+      <MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
+        <MenuItem
+          text={`COUNT(DISTINCT "${columnName}") AS "dist_${columnName}"`}
+          onClick={() =>
+            addAggregateColumn(columnName, 'COUNT', true, 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',
+                  spacing: [' '],
+                  filter: new ComparisonExpression({
+                    parens: [],
+                    ex: stringFactory(columnName, '"'),
+                    rhs: new ComparisonExpressionRhs({
+                      parens: [],
+                      op: '=',
+                      rhs: stringFactory('xxx', `'`),
+                      spacing: [' ', ' '],
+                    }),
+                  }),
+                }),
+              }),
+            )
+          }
+        />
+      </MenuItem>
+    );
+  }
+
+  render(): JSX.Element {
+    const { queryAst } = this.props;
+    let hasGroupBy;
+    if (queryAst) {
+      hasGroupBy = queryAst.groupByClause;
+    }
+    return (
+      <>
+        {queryAst && this.renderFilterMenu()}
+        {hasGroupBy && this.renderGroupByMenu()}
+        {hasGroupBy && this.renderAggregateMenu()}
+      </>
+    );
+  }
+}
diff --git a/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/__snapshots__/time-menu-items.spec.tsx.snap b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/__snapshots__/time-menu-items.spec.tsx.snap
new file mode 100644
index 0000000..0e6d23e
--- /dev/null
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/__snapshots__/time-menu-items.spec.tsx.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`time menu matches snapshot 1`] = `null`;
diff --git a/web-console/src/views/query-view/run-button/run-button.spec.tsx b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx
similarity index 72%
copy from web-console/src/views/query-view/run-button/run-button.spec.tsx
copy to web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx
index 1560f7a..398ecc0 100644
--- a/web-console/src/views/query-view/run-button/run-button.spec.tsx
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.spec.tsx
@@ -19,22 +19,22 @@
 import { render } from '@testing-library/react';
 import React from 'react';
 
-import { RunButton } from './run-button';
+import { TimeMenuItems } from './time-menu-items';
 
-describe('run button', () => {
+describe('time menu', () => {
   it('matches snapshot', () => {
-    const runButton = (
-      <RunButton
-        onEditContext={() => null}
-        runeMode={false}
-        queryContext={{}}
-        onQueryContextChange={() => {}}
-        onRun={() => {}}
-        onExplain={() => {}}
+    const timeMenu = (
+      <TimeMenuItems
+        clear={() => null}
+        addFunctionToGroupBy={() => null}
+        addToGroupBy={() => null}
+        addAggregateColumn={() => null}
+        filterByRow={() => null}
+        columnName={'text'}
       />
     );
 
-    const { container } = render(runButton);
+    const { container } = render(timeMenu);
     expect(container.firstChild).toMatchSnapshot();
   });
 });
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
new file mode 100644
index 0000000..3400ae3
--- /dev/null
+++ b/web-console/src/views/query-view/column-tree/column-tree-menu/time-menu-items/time-menu-items.tsx
@@ -0,0 +1,380 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { MenuDivider, MenuItem } from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
+import {
+  AdditiveExpression,
+  Alias,
+  FilterClause,
+  SqlQuery,
+  StringType,
+  timestampFactory,
+} from 'druid-query-toolkit';
+import {
+  aliasFactory,
+  intervalFactory,
+  refExpressionFactory,
+  stringFactory,
+} 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: () => void;
+}
+
+export class TimeMenuItems extends React.PureComponent<TimeMenuItemsProps> {
+  constructor(props: TimeMenuItemsProps, context: any) {
+    super(props, context);
+  }
+
+  formatTime(timePart: number): string {
+    if (timePart % 10 > 0) {
+      return String(timePart);
+    } else return '0' + String(timePart);
+  }
+
+  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 };
+  }
+
+  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 };
+  }
+
+  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,
+    };
+  }
+
+  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();
+
+    return (
+      <MenuItem icon={IconNames.FILTER} text={`Filter`}>
+        <MenuItem
+          text={`Latest hour`}
+          onClick={() => {
+            const additiveExpression = new AdditiveExpression({
+              parens: [],
+              op: ['-'],
+              ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('HOUR', '1')],
+              spacing: [' ', ' '],
+            });
+            clear();
+            filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
+          }}
+        />
+        <MenuItem
+          text={`Latest day`}
+          onClick={() => {
+            const additiveExpression = new AdditiveExpression({
+              parens: [],
+              op: ['-'],
+              ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('DAY', '1')],
+              spacing: [' ', ' '],
+            });
+            clear();
+            filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
+          }}
+        />
+        <MenuItem
+          text={`Latest week`}
+          onClick={() => {
+            const additiveExpression = new AdditiveExpression({
+              parens: [],
+              op: ['-'],
+              ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('DAY', '7')],
+              spacing: [' ', ' '],
+            });
+            clear();
+            filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
+          }}
+        />
+        <MenuItem
+          text={`Latest month`}
+          onClick={() => {
+            const additiveExpression = new AdditiveExpression({
+              parens: [],
+              op: ['-'],
+              ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('MONTH', '1')],
+              spacing: [' ', ' '],
+            });
+            clear();
+            filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
+          }}
+        />
+        <MenuItem
+          text={`Latest year`}
+          onClick={() => {
+            const additiveExpression = new AdditiveExpression({
+              parens: [],
+              op: ['-'],
+              ex: [refExpressionFactory('CURRENT_TIMESTAMP'), intervalFactory('YEAR', '1')],
+              spacing: [' ', ' '],
+            });
+            clear();
+            filterByRow([{ row: additiveExpression, header: columnName, operator: '>=' }], true);
+          }}
+        />
+        <MenuDivider />
+        <MenuItem
+          text={`Current hour`}
+          onClick={() => {
+            const next = this.getNextHour(hour, day, month, year);
+            clear();
+            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: '<',
+                },
+              ],
+              true,
+            );
+          }}
+        />
+        <MenuItem
+          text={`Current day`}
+          onClick={() => {
+            const next = this.getNextDay(day, month, year);
+            clear();
+            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: '<',
+                },
+              ],
+              true,
+            );
+          }}
+        />
+        <MenuItem
+          text={`Current month`}
+          onClick={() => {
+            const next = this.getNextMonth(month, year);
+            clear();
+            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: '<',
+                },
+              ],
+              true,
+            );
+          }}
+        />
+        <MenuItem
+          text={`Current year`}
+          onClick={() => {
+            clear();
+            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: '<',
+                },
+              ],
+              true,
+            );
+          }}
+        />
+      </MenuItem>
+    );
+  }
+
+  renderGroupByMenu(): JSX.Element {
+    const { columnName, addFunctionToGroupBy } = this.props;
+
+    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', `'`)],
+              true,
+              aliasFactory(`${columnName}_time_floor`),
+            )
+          }
+        />
+        <MenuItem
+          text={`TIME_FLOOR("${columnName}", 'P1D') AS "${columnName}_time_floor"`}
+          onClick={() =>
+            addFunctionToGroupBy(
+              'TIME_FLOOR',
+              [' '],
+              [stringFactory(columnName, `"`), stringFactory('P1D', `'`)],
+              true,
+              aliasFactory(`${columnName}_time_floor`),
+            )
+          }
+        />
+        <MenuItem
+          text={`TIME_FLOOR("${columnName}", 'P7D') AS "${columnName}_time_floor"`}
+          onClick={() =>
+            addFunctionToGroupBy(
+              'TIME_FLOOR',
+              [' '],
+              [stringFactory(columnName, `"`), stringFactory('P7D', `'`)],
+              true,
+              aliasFactory(`${columnName}_time_floor`),
+            )
+          }
+        />
+      </MenuItem>
+    );
+  }
+
+  renderAggregateMenu(): JSX.Element {
+    const { columnName, addAggregateColumn } = this.props;
+    return (
+      <MenuItem icon={IconNames.FUNCTION} text={`Aggregate`}>
+        <MenuItem
+          text={`MAX("${columnName}") AS "max_${columnName}"`}
+          onClick={() =>
+            addAggregateColumn(columnName, 'MAX', true, aliasFactory(`max_${columnName}`))
+          }
+        />
+        <MenuItem
+          text={`MIN("${columnName}") AS "min_${columnName}"`}
+          onClick={() =>
+            addAggregateColumn(columnName, 'MIN', true, aliasFactory(`min_${columnName}`))
+          }
+        />
+      </MenuItem>
+    );
+  }
+
+  render(): JSX.Element {
+    const { queryAst } = this.props;
+    let hasGroupBy;
+    if (queryAst) {
+      hasGroupBy = queryAst.groupByClause;
+    }
+    return (
+      <>
+        {queryAst && this.renderFilterMenu()}
+        {hasGroupBy && this.renderGroupByMenu()}
+        {hasGroupBy && 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 e8ca98d..d210eb2 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
@@ -27,6 +27,13 @@ describe('column tree', () => {
   it('matches snapshot', () => {
     const columnTree = (
       <ColumnTree
+        queryAst={() => undefined}
+        hasGroupBy={() => false}
+        clear={() => null}
+        addFunctionToGroupBy={() => null}
+        filterByRow={() => null}
+        addAggregateColumn={() => null}
+        addToGroupBy={() => null}
         columnMetadataLoading={false}
         columnMetadata={
           [
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 ea62f20..0a491b0 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
@@ -16,14 +16,29 @@
  * limitations under the License.
  */
 
-import { HTMLSelect, IconName, ITreeNode, Menu, MenuItem, Position, Tree } from '@blueprintjs/core';
-import { Popover } from '@blueprintjs/core/lib/cjs';
+import {
+  HTMLSelect,
+  IconName,
+  ITreeNode,
+  Menu,
+  MenuItem,
+  Popover,
+  Position,
+  Tree,
+} from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
+import { Alias, FilterClause, RefExpression, SqlQuery, StringType } 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 './column-tree.scss';
 
@@ -88,10 +103,31 @@ export interface ColumnTreeProps {
   onQueryStringChange: (queryString: string) => 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;
+  hasGroupBy: () => boolean;
+  queryAst: () => SqlQuery | undefined;
+  clear: () => void;
 }
 
 export interface ColumnTreeState {
   prevColumnMetadata?: ColumnMetadata[];
+  prevGroupByStatus?: boolean;
   columnTree?: ITreeNode[];
   selectedTreeIndex: number;
   expandedNode: number;
@@ -100,7 +136,6 @@ export interface ColumnTreeState {
 export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeState> {
   static getDerivedStateFromProps(props: ColumnTreeProps, state: ColumnTreeState) {
     const { columnMetadata, defaultSchema, defaultTable } = props;
-
     if (columnMetadata && columnMetadata !== state.prevColumnMetadata) {
       const columnTree = groupBy(
         columnMetadata,
@@ -122,7 +157,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
                     <Menu>
                       <MenuItem
                         icon={IconNames.FULLSCREEN}
-                        text={`Show: ${table}`}
+                        text={`Select ... from ${table}`}
                         onClick={() => {
                           handleTableClick(
                             schema,
@@ -160,35 +195,72 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
                   <Popover
                     boundary={'window'}
                     position={Position.RIGHT}
+                    autoFocus={false}
+                    targetClassName={'bp3-popover-open'}
                     content={
-                      <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,
-                            );
-                          }}
-                        />
-                        <MenuItem
-                          icon={IconNames.CLIPBOARD}
-                          text={`Copy: ${columnData.COLUMN_NAME}`}
-                          onClick={() => {
-                            copyAndAlert(
-                              columnData.COLUMN_NAME,
-                              `${columnData.COLUMN_NAME} query copied to clipboard`,
-                            );
-                          }}
-                        />
-                      </Menu>
+                      <Deferred
+                        content={() => (
+                          <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()}
+                              />
+                            )}
+                            {columnData.DATA_TYPE === 'VARCHAR' && (
+                              <StringMenuItems
+                                addFunctionToGroupBy={props.addFunctionToGroupBy}
+                                addToGroupBy={props.addToGroupBy}
+                                addAggregateColumn={props.addAggregateColumn}
+                                filterByRow={props.filterByRow}
+                                columnName={columnData.COLUMN_NAME}
+                                queryAst={props.queryAst()}
+                              />
+                            )}
+                            {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()}
+                              />
+                            )}
+                            <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>
@@ -203,29 +275,28 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
       let selectedTreeIndex = -1;
       let expandedNode = -1;
       if (defaultSchema && columnTree) {
-        selectedTreeIndex = columnTree
-          .map(function(x) {
-            return x.id;
-          })
-          .indexOf(defaultSchema);
+        selectedTreeIndex = columnTree.findIndex(x => {
+          return x.id === defaultSchema;
+        });
       }
+
       if (selectedTreeIndex > -1) {
         const treeNodes = columnTree[selectedTreeIndex].childNodes;
         if (treeNodes) {
           if (defaultTable) {
-            expandedNode = treeNodes
-              .map(node => {
-                return node.id;
-              })
-              .indexOf(defaultTable);
+            expandedNode = treeNodes.findIndex(node => {
+              return node.id === defaultTable;
+            });
           }
         }
       }
+
       return {
         prevColumnMetadata: columnMetadata,
         columnTree,
         selectedTreeIndex,
         expandedNode,
+        prevGroupByStatus: props.hasGroupBy,
       };
     }
     return null;
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 e56b1d1..3cfb424 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
@@ -29,7 +29,6 @@ describe('query output', () => {
         sqlOrderBy={() => null}
         sqlFilterRow={() => null}
         sqlExcludeColumn={() => null}
-        disabled={false}
         loading={false}
         error="lol"
       />
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 9a3f4ea..ad89e4e 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
@@ -18,7 +18,7 @@
 
 import { Menu, MenuItem, Popover } from '@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
-import { HeaderRows } from 'druid-query-toolkit';
+import { HeaderRows, SqlQuery } from 'druid-query-toolkit';
 import {
   basicIdentifierEscape,
   basicLiteralEscape,
@@ -28,34 +28,40 @@ 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 {
-  aggregateColumns?: string[];
-  disabled: boolean;
   loading: boolean;
-  sqlFilterRow: (row: string, header: string, operator: '=' | '!=') => void;
-  sqlExcludeColumn: (header: string) => void;
-  sqlOrderBy: (header: string, direction: 'ASC' | 'DESC') => void;
-  sorted?: { id: string; desc: boolean }[];
-  result?: HeaderRows;
+  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;
   error?: string;
   runeMode: boolean;
 }
 
 export class QueryOutput extends React.PureComponent<QueryOutputProps> {
   render(): JSX.Element {
-    const { result, loading, error } = this.props;
+    const { queryResult, parsedQuery, loading, error } = this.props;
+
+    let aggregateColumns: string[] | undefined;
+    if (parsedQuery) {
+      aggregateColumns = parsedQuery.getAggregateColumns();
+    }
 
     return (
       <div className="query-output">
         <ReactTable
-          data={result ? result.rows : []}
+          data={queryResult ? queryResult.rows : []}
           loading={loading}
-          noDataText={!loading && result && !result.rows.length ? 'No results' : error || ''}
+          noDataText={
+            !loading && queryResult && !queryResult.rows.length ? 'No queryResults' : error || ''
+          }
           sortable={false}
-          columns={(result ? result.header : []).map((h: any, i) => {
+          columns={(queryResult ? queryResult.header : []).map((h: any, i) => {
             return {
               Header: () => {
                 return (
@@ -80,11 +86,8 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
                 }
                 return value;
               },
-              className: this.props.aggregateColumns
-                ? this.props.aggregateColumns.indexOf(h) > -1
-                  ? 'aggregate-column'
-                  : undefined
-                : undefined,
+              className:
+                aggregateColumns && aggregateColumns.includes(h) ? 'aggregate-column' : undefined,
             };
           })}
         />
@@ -92,9 +95,45 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
     );
   }
   getHeaderActions(h: string) {
-    const { disabled, sqlExcludeColumn, sqlOrderBy, runeMode } = this.props;
+    const { parsedQuery, sqlExcludeColumn, sqlOrderBy, runeMode } = this.props;
+
     let actionsMenu;
-    if (disabled) {
+    if (parsedQuery) {
+      const sorted = parsedQuery.getSorted();
+
+      const basicActions: BasicAction[] = [];
+      if (sorted) {
+        sorted.map(sorted => {
+          if (sorted.id === h) {
+            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),
+            });
+          }
+        });
+      }
+      if (!basicActions.length) {
+        basicActions.push(
+          {
+            icon: IconNames.SORT_ASC,
+            title: `Order by: ${h} ASC`,
+            onAction: () => sqlOrderBy(h, 'ASC', true),
+          },
+          {
+            icon: IconNames.SORT_DESC,
+            title: `Order by: ${h} DESC`,
+            onAction: () => sqlOrderBy(h, 'DESC', true),
+          },
+        );
+      }
+      basicActions.push({
+        icon: IconNames.CROSS,
+        title: `Remove: ${h}`,
+        onAction: () => sqlExcludeColumn(h, true),
+      });
+      actionsMenu = basicActionsToMenu(basicActions);
+    } else {
       actionsMenu = (
         <Menu>
           <MenuItem
@@ -130,48 +169,28 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
           )}
         </Menu>
       );
-    } else {
-      const { sorted } = this.props;
-      const basicActions: BasicAction[] = [];
-      if (sorted) {
-        sorted.map(sorted => {
-          if (sorted.id === h) {
-            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'),
-            });
-          }
-        });
-      }
-      if (!basicActions.length) {
-        basicActions.push(
-          {
-            icon: IconNames.SORT_ASC,
-            title: `Order by: ${h} ASC`,
-            onAction: () => sqlOrderBy(h, 'ASC'),
-          },
-          {
-            icon: IconNames.SORT_DESC,
-            title: `Order by: ${h} DESC`,
-            onAction: () => sqlOrderBy(h, 'DESC'),
-          },
-        );
-      }
-      basicActions.push({
-        icon: IconNames.CROSS,
-        title: `Remove: ${h}`,
-        onAction: () => sqlExcludeColumn(h),
-      });
-      actionsMenu = basicActionsToMenu(basicActions);
     }
     return actionsMenu ? actionsMenu : undefined;
   }
 
   getRowActions(row: string, header: string) {
-    const { disabled, sqlFilterRow, runeMode } = this.props;
+    const { parsedQuery, sqlFilterRow, runeMode } = this.props;
+
     let actionsMenu;
-    if (disabled) {
+    if (parsedQuery) {
+      actionsMenu = basicActionsToMenu([
+        {
+          icon: IconNames.FILTER_KEEP,
+          title: `Filter by: ${header} = ${row}`,
+          onAction: () => sqlFilterRow([{ row, header, operator: '=' }], true),
+        },
+        {
+          icon: IconNames.FILTER_REMOVE,
+          title: `Filter by: ${header} != ${row}`,
+          onAction: () => sqlFilterRow([{ row, header, operator: '!=' }], true),
+        },
+      ]);
+    } else {
       actionsMenu = (
         <Menu>
           <MenuItem
@@ -209,38 +228,29 @@ export class QueryOutput extends React.PureComponent<QueryOutputProps> {
           )}
         </Menu>
       );
-    } else {
-      actionsMenu = basicActionsToMenu([
-        {
-          icon: IconNames.FILTER_KEEP,
-          title: `Filter by: ${header} = ${row}`,
-          onAction: () => sqlFilterRow(row, header, '='),
-        },
-        {
-          icon: IconNames.FILTER_REMOVE,
-          title: `Filter by: ${header} != ${row}`,
-          onAction: () => sqlFilterRow(row, header, '!='),
-        },
-      ]);
     }
     return actionsMenu ? actionsMenu : undefined;
   }
 
   getHeaderClassName(h: string) {
-    const { sorted, aggregateColumns } = this.props;
+    const { parsedQuery } = this.props;
+
     const className = [];
-    className.push(
-      sorted
-        ? sorted.map(sorted => {
+    if (parsedQuery) {
+      const sorted = parsedQuery.getSorted();
+      if (sorted) {
+        className.push(
+          sorted.map(sorted => {
             if (sorted.id === h) {
               return sorted.desc ? '-sort-desc' : '-sort-asc';
             }
             return '';
-          })[0]
-        : undefined,
-    );
-    if (aggregateColumns) {
-      if (aggregateColumns.includes(h)) {
+          })[0],
+        );
+      }
+
+      const aggregateColumns = parsedQuery.getAggregateColumns();
+      if (aggregateColumns && aggregateColumns.includes(h)) {
         className.push('aggregate-header');
       }
     }
diff --git a/web-console/src/views/query-view/query-view.tsx b/web-console/src/views/query-view/query-view.tsx
index 68aa550..0694994 100644
--- a/web-console/src/views/query-view/query-view.tsx
+++ b/web-console/src/views/query-view/query-view.tsx
@@ -20,20 +20,31 @@ import { Intent } 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';
 import React from 'react';
 import SplitterLayout from 'react-splitter-layout';
 
 import { SQL_FUNCTIONS, SyntaxDescription } from '../../../lib/sql-function-doc';
 import { QueryPlanDialog } from '../../dialogs';
 import { EditContextDialog } from '../../dialogs/edit-context-dialog/edit-context-dialog';
+import {
+  QueryHistoryDialog,
+  QueryRecord,
+} from '../../dialogs/query-history-dialog/query-history-dialog';
 import { AppToaster } from '../../singletons/toaster';
 import {
   BasicQueryExplanation,
@@ -58,12 +69,20 @@ import { RunButton } from './run-button/run-button';
 
 import './query-view.scss';
 
-const parser = sqlParserFactory(
+const parserRaw = sqlParserFactory(
   SQL_FUNCTIONS.map((sql_function: SyntaxDescription) => {
     return sql_function.syntax.substr(0, sql_function.syntax.indexOf('('));
   }),
 );
 
+const parser = memoizeOne((sql: string) => {
+  try {
+    return parserRaw(sql);
+  } catch {
+    return;
+  }
+});
+
 interface QueryWithContext {
   queryString: string;
   queryContext: QueryContext;
@@ -74,8 +93,15 @@ 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;
   queryContext: QueryContext;
 
   columnMetadataLoading: boolean;
@@ -83,8 +109,7 @@ export interface QueryViewState {
   columnMetadataError?: string;
 
   loading: boolean;
-  result?: HeaderRows;
-  queryExtraInfo?: QueryExtraInfoData;
+  result?: QueryResult;
   error?: string;
 
   explainDialogOpen: boolean;
@@ -94,9 +119,12 @@ export interface QueryViewState {
 
   defaultSchema?: string;
   defaultTable?: string;
-  ast?: SqlQuery;
 
   editContextDialogOpen: boolean;
+  historyDialogOpen: boolean;
+  queryHistory: QueryRecord[];
+
+  autoRun: boolean;
 }
 
 interface QueryResult {
@@ -149,8 +177,18 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
 
   constructor(props: QueryViewProps, context: any) {
     super(props, context);
+
+    let queryString: string | undefined;
+    if (props.initQuery) {
+      queryString = props.initQuery;
+    } else if (localStorageGet(LocalStorageKeys.QUERY_KEY)) {
+      queryString = localStorageGet(LocalStorageKeys.QUERY_KEY);
+    }
+    const queryAst = queryString ? parser(queryString) : undefined;
+
     this.state = {
-      queryString: props.initQuery || localStorageGet(LocalStorageKeys.QUERY_KEY) || '',
+      queryString: queryString ? queryString : '',
+      queryAst,
       queryContext: {},
 
       columnMetadataLoading: false,
@@ -161,8 +199,11 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
       loadingExplain: false,
 
       editContextDialogOpen: false,
-    };
+      historyDialogOpen: false,
+      queryHistory: [],
 
+      autoRun: true,
+    };
     this.metadataQueryManager = new QueryManager({
       processQuery: async () => {
         return await queryDruidSql<ColumnMetadata>({
@@ -199,7 +240,6 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
         if (!(ast instanceof SqlQuery)) {
           ast = undefined;
         }
-
         if (QueryView.isJsonLike(queryString)) {
           jsonQuery = Hjson.parse(queryString);
         } else {
@@ -250,7 +290,6 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
           shouldIncludeTimestamp(jsonQuery),
           isFirstRowHeader(jsonQuery),
         );
-
         return {
           queryResult,
           queryExtraInfo: {
@@ -266,11 +305,9 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
       },
       onStateChange: ({ result, loading, error }) => {
         this.setState({
-          result: result ? result.queryResult : undefined,
-          queryExtraInfo: result ? result.queryExtraInfo : undefined,
+          result,
           loading,
           error,
-          ast: result ? result.parsedQuery : undefined,
         });
       },
     });
@@ -300,6 +337,28 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
 
   componentDidMount(): void {
     this.metadataQueryManager.runQuery(null);
+
+    const localStorageQueryHistoy = localStorageGet(LocalStorageKeys.QUERY_HISTORY);
+    let queryHistory;
+    if (localStorageQueryHistoy) {
+      try {
+        queryHistory = JSON.parse(localStorageQueryHistoy);
+      } catch {}
+      if (queryHistory) {
+        this.setState({ queryHistory });
+      }
+    }
+
+    const localStorageAutoRun = localStorageGet(LocalStorageKeys.AUTO_RUN);
+    let autoRun;
+    if (localStorageAutoRun) {
+      try {
+        autoRun = JSON.parse(localStorageAutoRun);
+      } catch {}
+      if (typeof autoRun === 'boolean') {
+        this.setState({ autoRun });
+      }
+    }
   }
 
   componentWillUnmount(): void {
@@ -311,21 +370,23 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
   handleDownload = (filename: string, format: string) => {
     const { result } = this.state;
     if (!result) return;
+    const { queryResult } = result;
+
     let lines: string[] = [];
     let separator: string = '';
 
     if (format === 'csv' || format === 'tsv') {
       separator = format === 'csv' ? ',' : '\t';
-      lines.push(result.header.map(str => QueryView.formatStr(str, format)).join(separator));
+      lines.push(queryResult.header.map(str => QueryView.formatStr(str, format)).join(separator));
       lines = lines.concat(
-        result.rows.map(r => r.map(cell => QueryView.formatStr(cell, format)).join(separator)),
+        queryResult.rows.map(r => r.map(cell => QueryView.formatStr(cell, format)).join(separator)),
       );
     } else {
       // json
-      lines = result.rows.map(r => {
+      lines = queryResult.rows.map(r => {
         const outputObject: Record<string, any> = {};
         for (let k = 0; k < r.length; k++) {
-          const newName = result.header[k];
+          const newName = queryResult.header[k];
           if (newName) {
             outputObject[newName] = r[k];
           }
@@ -347,6 +408,24 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
         explainResult={explainResult}
         explainError={explainError}
         onClose={() => this.setState({ explainDialogOpen: false })}
+        setQueryString={(queryString: string) =>
+          this.setState({ queryString, explainDialogOpen: false, queryAst: parser(queryString) })
+        }
+      />
+    );
+  }
+
+  renderHistoryDialog() {
+    const { historyDialogOpen, queryHistory } = this.state;
+    if (!historyDialogOpen) return;
+
+    return (
+      <QueryHistoryDialog
+        queryRecords={queryHistory}
+        setQueryString={queryString =>
+          this.setState({ queryString, queryAst: parser(queryString), historyDialogOpen: false })
+        }
+        onClose={() => this.setState({ historyDialogOpen: false })}
       />
     );
   }
@@ -372,13 +451,11 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
       queryContext,
       loading,
       result,
-      queryExtraInfo,
       error,
       columnMetadata,
-      ast,
+      autoRun,
     } = this.state;
     const runeMode = QueryView.isJsonLike(queryString);
-
     return (
       <SplitterLayout
         vertical
@@ -399,77 +476,194 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
           />
           <div className="control-bar">
             <RunButton
+              autoRun={autoRun}
+              setAutoRun={(autoRun: boolean) => this.setAutoRun(autoRun)}
               onEditContext={() => this.setState({ editContextDialogOpen: true })}
               runeMode={runeMode}
               queryContext={queryContext}
               onQueryContextChange={this.handleQueryContextChange}
               onRun={this.handleRun}
               onExplain={this.handleExplain}
+              onHistory={() => this.setState({ historyDialogOpen: true })}
             />
-            {queryExtraInfo && (
-              <QueryExtraInfo queryExtraInfo={queryExtraInfo} onDownload={this.handleDownload} />
+            {result && (
+              <QueryExtraInfo
+                queryExtraInfo={result.queryExtraInfo}
+                onDownload={this.handleDownload}
+              />
             )}
           </div>
         </div>
         <QueryOutput
-          aggregateColumns={ast ? ast.getAggregateColumns() : undefined}
-          disabled={!ast}
-          sorted={ast ? ast.getSorted() : undefined}
           sqlExcludeColumn={this.sqlExcludeColumn}
           sqlFilterRow={this.sqlFilterRow}
           sqlOrderBy={this.sqlOrderBy}
           runeMode={runeMode}
           loading={loading}
-          result={result}
+          queryResult={result ? result.queryResult : undefined}
+          parsedQuery={result ? result.parsedQuery : undefined}
           error={error}
         />
       </SplitterLayout>
     );
   }
 
-  private sqlOrderBy = (header: string, direction: 'ASC' | 'DESC'): void => {
-    let { ast } = this.state;
-    if (!ast) return;
-    ast = ast.orderBy(header, direction);
+  private addFunctionToGroupBy = (
+    functionName: string,
+    spacing: string[],
+    argumentsArray: (StringType | number)[],
+    preferablyRun: boolean,
+    alias: Alias,
+  ): void => {
+    const { autoRun, queryAst } = this.state;
+    if (!queryAst) return;
+    const groupedAst = queryAst.addFunctionToGroupBy(functionName, spacing, argumentsArray, alias);
+    const queryString = groupedAst.toString();
+    this.setState({
+      queryString,
+      queryAst: parser(queryString),
+    });
+    if (autoRun && preferablyRun) {
+      this.handleRun(true, queryString);
+    }
+  };
+
+  private addToGroupBy = (columnName: string, preferablyRun: boolean): void => {
+    const { autoRun, queryAst } = this.state;
+    if (!queryAst) return;
+    const groupedAst = queryAst.addToGroupBy(columnName);
+    const queryString = groupedAst.toString();
+    this.setState({
+      queryString,
+      queryAst: parser(queryString),
+    });
+    if (autoRun && preferablyRun) {
+      this.handleRun(true, queryString);
+    }
+  };
+
+  private addAggregateColumn = (
+    columnName: string | RefExpression,
+    functionName: string,
+    preferablyRun: boolean,
+    alias?: Alias,
+    distinct?: boolean,
+    filter?: FilterClause,
+  ): void => {
+    const { autoRun, queryAst } = this.state;
+    if (!queryAst) return;
+    const modifiedAst = queryAst.addAggregateColumn(
+      columnName,
+      functionName,
+      alias,
+      distinct,
+      filter,
+    );
+    const queryString = modifiedAst.toString();
+    this.setState({
+      queryString,
+      queryAst: parser(queryString),
+    });
+    if (autoRun && preferablyRun) {
+      this.handleRun(true, queryString);
+    }
+  };
+
+  private sqlOrderBy = (
+    header: string,
+    direction: 'ASC' | 'DESC',
+    preferablyRun: boolean,
+  ): void => {
+    const { autoRun, queryAst } = this.state;
+    if (!queryAst) return;
+    const modifiedAst = queryAst.orderBy(header, direction);
+    const queryString = modifiedAst.toString();
     this.setState({
-      queryString: ast.toString(),
+      queryString,
+      queryAst: parser(queryString),
     });
-    this.handleRun(true, ast.toString());
+    if (autoRun && preferablyRun) {
+      this.handleRun(true, queryString);
+    }
   };
 
-  private sqlExcludeColumn = (header: string): void => {
-    let { ast } = this.state;
-    if (!ast) return;
-    ast = ast.excludeColumn(header);
+  private sqlExcludeColumn = (header: string, preferablyRun: boolean): void => {
+    const { autoRun, queryAst } = this.state;
+    if (!queryAst) return;
+    const modifiedAst = queryAst.excludeColumn(header);
+    const queryString = modifiedAst.toString();
     this.setState({
-      queryString: ast.toString(),
+      queryString,
+      queryAst: parser(queryString),
     });
-    this.handleRun(true, ast.toString());
+    if (autoRun && preferablyRun) {
+      this.handleRun(true, queryString);
+    }
   };
 
-  private sqlFilterRow = (row: string, header: string, operator: '!=' | '='): void => {
-    let { ast } = this.state;
-    if (!ast) return;
-    ast = ast.filterRow(header, row, operator);
+  private sqlFilterRow = (filters: RowFilter[], preferablyRun: boolean): void => {
+    const { autoRun, queryAst } = this.state;
+    if (!queryAst) return;
+
+    let modifiedAst: SqlQuery = queryAst;
+    if (queryAst) {
+      for (const filter of filters) {
+        modifiedAst = modifiedAst.filterRow(filter.header, filter.row, filter.operator);
+      }
+    }
+    const queryString = modifiedAst.toString();
     this.setState({
-      queryString: ast.toString(),
+      queryString,
+      queryAst: parser(queryString),
     });
-    this.handleRun(true, ast.toString());
+    if (autoRun && preferablyRun) {
+      this.handleRun(true, queryString);
+    }
+  };
+
+  private sqlClearWhere = (): void => {
+    const { queryAst } = this.state;
+
+    if (!queryAst) return;
+    if (queryAst.whereClause) {
+      queryAst.whereClause = undefined;
+    }
   };
 
   private handleQueryStringChange = (queryString: string): void => {
-    this.setState({ queryString });
+    this.setState({ queryString, queryAst: parser(queryString) });
   };
 
   private handleQueryContextChange = (queryContext: QueryContext) => {
     this.setState({ queryContext });
   };
 
+  private setAutoRun = (autoRun: boolean) => {
+    this.setState({ autoRun });
+    localStorageSet(LocalStorageKeys.AUTO_RUN, String(autoRun));
+  };
+
   private handleRun = (wrapQuery: boolean, customQueryString?: string) => {
-    const { queryString, queryContext } = this.state;
+    const { queryString, queryContext, queryHistory } = this.state;
     if (!customQueryString) {
       customQueryString = queryString;
     }
+
+    while (queryHistory.length > 9) {
+      queryHistory.pop();
+    }
+    queryHistory.unshift({
+      version: `${new Date().toISOString()}`,
+      queryString: customQueryString,
+    });
+    let queryHistoryString;
+    try {
+      queryHistoryString = JSON.stringify(queryHistory);
+    } catch {}
+    if (queryHistoryString) {
+      localStorageSet(LocalStorageKeys.QUERY_HISTORY, queryHistoryString);
+    }
+
     if (QueryView.isJsonLike(customQueryString) && !QueryView.validRune(customQueryString)) return;
 
     localStorageSet(LocalStorageKeys.QUERY_KEY, customQueryString);
@@ -486,32 +680,47 @@ export class QueryView extends React.PureComponent<QueryViewProps, QueryViewStat
     localStorageSet(LocalStorageKeys.QUERY_VIEW_PANE_SIZE, String(secondaryPaneSize));
   };
 
-  render(): JSX.Element {
-    const {
-      columnMetadata,
-      columnMetadataLoading,
-      columnMetadataError,
-      ast,
-      queryString,
-    } = this.state;
-
+  private getGroupBySetting = () => {
+    const { queryString, queryAst } = this.state;
+    const ast = queryAst;
     let tempAst: SqlQuery | undefined;
     if (!ast) {
-      try {
-        tempAst = parser(queryString);
-      } catch {}
+      tempAst = parser(queryString);
     }
-    let defaultSchema;
+
+    let hasGroupBy = false;
     if (ast && ast instanceof SqlQuery) {
-      defaultSchema = ast.getSchema();
+      hasGroupBy = !!ast.groupByClause;
     } else if (tempAst && tempAst instanceof SqlQuery) {
-      defaultSchema = tempAst.getSchema();
+      hasGroupBy = !!tempAst.groupByClause;
+    }
+    return hasGroupBy;
+  };
+
+  private getQueryAst = () => {
+    const { queryAst } = this.state;
+    return queryAst;
+  };
+
+  private onQueryStringChange = (queryString: string) => {
+    const { autoRun } = this.state;
+
+    this.handleQueryStringChange(queryString);
+    if (autoRun) {
+      this.handleRun(true, queryString);
+    }
+  };
+
+  render(): JSX.Element {
+    const { columnMetadata, columnMetadataLoading, columnMetadataError, queryAst } = this.state;
+
+    let defaultSchema;
+    if (queryAst && queryAst instanceof SqlQuery) {
+      defaultSchema = queryAst.getSchema();
     }
     let defaultTable;
-    if (ast && ast instanceof SqlQuery) {
-      defaultTable = ast.getTableName();
-    } else if (tempAst && tempAst instanceof SqlQuery) {
-      defaultTable = tempAst.getTableName();
+    if (queryAst && queryAst instanceof SqlQuery) {
+      defaultTable = queryAst.getTableName();
     }
 
     return (
@@ -520,15 +729,23 @@ 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}
+            hasGroupBy={this.getGroupBySetting}
+            queryAst={this.getQueryAst}
             columnMetadataLoading={columnMetadataLoading}
             columnMetadata={columnMetadata}
-            onQueryStringChange={this.handleQueryStringChange}
-            defaultSchema={defaultSchema}
+            onQueryStringChange={this.onQueryStringChange}
+            defaultSchema={defaultSchema ? defaultSchema : 'druid'}
             defaultTable={defaultTable}
           />
         )}
         {this.renderMainArea()}
         {this.renderExplainDialog()}
+        {this.renderHistoryDialog()}
         {this.renderEditContextDialog()}
       </div>
     );
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 1560f7a..d61e5f7 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,6 +25,9 @@ describe('run button', () => {
   it('matches snapshot', () => {
     const runButton = (
       <RunButton
+        autoRun
+        setAutoRun={() => null}
+        onHistory={() => null}
         onEditContext={() => null}
         runeMode={false}
         queryContext={{}}
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 037f4b3..276288b 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
@@ -45,11 +45,14 @@ import { DRUID_DOCS_RUNE, DRUID_DOCS_SQL } from '../../../variables';
 
 export interface RunButtonProps {
   runeMode: boolean;
+  autoRun: boolean;
   queryContext: QueryContext;
   onQueryContextChange: (newQueryContext: QueryContext) => void;
   onRun: (wrapQuery: boolean) => void;
   onExplain: () => void;
   onEditContext: () => void;
+  onHistory: () => void;
+  setAutoRun: (autoRun: boolean) => void;
 }
 
 interface RunButtonState {
@@ -86,7 +89,16 @@ export class RunButton extends React.PureComponent<RunButtonProps, RunButtonStat
   };
 
   renderExtraMenu() {
-    const { runeMode, onExplain, queryContext, onQueryContextChange, onEditContext } = this.props;
+    const {
+      runeMode,
+      onExplain,
+      queryContext,
+      onQueryContextChange,
+      onEditContext,
+      onHistory,
+      setAutoRun,
+      autoRun,
+    } = this.props;
     const { wrapQuery } = this.state;
 
     const useCache = getUseCache(queryContext);
@@ -104,12 +116,18 @@ export class RunButton extends React.PureComponent<RunButtonProps, RunButtonStat
         {!runeMode && (
           <>
             <MenuItem icon={IconNames.CLEAN} text="Explain" onClick={onExplain} />
+            <MenuItem icon={IconNames.HISTORY} text="History" onClick={onHistory} />
             <MenuCheckbox
               checked={wrapQuery}
               label="Wrap query with limit"
               onChange={() => this.setState({ wrapQuery: !wrapQuery })}
             />
             <MenuCheckbox
+              checked={autoRun}
+              label="Auto run queries"
+              onChange={() => setAutoRun(!autoRun)}
+            />
+            <MenuCheckbox
               checked={useApproximateCountDistinct}
               label="Use approximate COUNT(DISTINCT)"
               onChange={() => {


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